Kin / Maintainers

Extending kin — tools, kinds, and profiles

How to add a new tool to the kin harness. This page is the user-facing mirror of the canonical walkthrough in src/kin/harness/AGENTS.md — the source-tree version stays the single source of truth; this page exists so a…

Read as Markdown

How to add a new tool to the kin harness. This page is the user-facing mirror of the canonical walkthrough in src/kin/harness/AGENTS.md — the source-tree version stays the single source of truth; this page exists so a contributor landing on docs/ doesn’t have to know to navigate into src/kin/harness/AGENTS.md first.

Adding a tool

Tools live in src/kin/harness/tools/ (kin.harness.tools). The Tool base class + ToolContext are in tools/registry.py.

To add a tool:

  1. Subclass Tool and set the class attrs:

    • name — what the model calls ("web_fetch", "shell", etc.).
    • description — short, what the tool does. The model reads this; keep it tight.
    • parameters — JSON Schema for the args. Strict-decode opt-in via KIN_STRICT_TOOLS (see Models & providers).
    • kind — the static risk class the gate reads (READ / EDIT / SHELL / META / MCP / NETWORK / RIG / OUTPOST / PUBLISH; see Permissions). This drives the mode → permission decision. A NEW kind must land in both mode-policy dicts + the planning-freeze tuple in one change. An unmapped kind fails closed to ASK.

    A bounded multi-operation tool may override permission_kind(args) to resolve the concrete call’s risk. It must fail closed for missing/unknown operations. Four further optional call-level seams exist:

    • approval_scope(args, workdir) — reusable signature + human label;
    • approval_reusable(args) — false removes the Always tier;
    • requires_serial(args) — preserve order for a call that may open a modal or race a sibling effect;
    • snapshots_before_run(args) — request the edit snapshot safety net when the risk kind alone does not imply it.

    GitTool is the reference implementation. These methods affect generic loop behavior; do not add a one-off dispatch path for a new tool.

  2. Implement async run(args, ctx) → str (or a list[ContentBlock] for multimodal results — see tools/_media.py; the loop normalizes both via coerce_content). Use the ctx helpers:

    • ctx.emit(event) — post an Event to the UI / journal.
    • ctx.progress(text) — live-tail text in the UI (for streaming).
    • ctx.parent_tool_call_idMUST be threaded onto any tool_stream event you emit, or the UI can’t group the stream under the right tool call.
  3. Untrusted output (web / file external content) must go through frame_untrusted in tools/_util.py before entering history. See REFERENCE.md § “Untrusted content”.

  4. Numeric args should use coerce_int (in tools/_util.py) — don’t bare-int() model args; junk → default, clamps. Boolean args use coerce_bool — never raw truthiness: a hallucinated "false" string is Python-truthy but a falsy token under the contract (see REFERENCE.md critical invariant — the contract is load-bearing anywhere permission scope keys off a boolean).

  5. Register the tool in tools/__init__.py::default_registry(). Most tools register unconditionally; the default-on search_workspace tool keeps a conditional search_enabled kill-switch path. Ordinary git / github and Beam’s internal Git trio are deliberately disjoint, and child registries remove publication tools. The outpost / outpost-send pair follows the same default-off-by-absence pattern, registering only once both outpost_url and outpost_token resolve.

  6. Add coverage under tests/test_harness/ (and tests/test_web.py if it’s a web tool). The harness suite is the commit gate — a tool without a verify assertion is a silent regression waiting to happen.

Process / subprocess tools (currently just shell.py) need start_new_session=True for process groups, and cleanup in finally. See REFERENCE.md § “Process & subprocess”.

Adding a permission kind

The mode → permission gate lives in src/kin/harness/permissions.py

  • src/kin/harness/modes.py. There are nine built-in kinds: READ, EDIT, SHELL, META, MCP, NETWORK, RIG, OUTPOST, PUBLISH. To add another:
  1. Add the constant to permissions.py (e.g. EGRESS = "egress").
  2. Map it in BOTH mode-policy dictsAUTO.policy and STRICT.policy in modes.py. The default is fail-closed (policy.get(kind, perm.ASK) in Mode.decide), but an omission still changes the intended posture and fails the structural gate.
  3. Add it to loop._planning_freezes if the kind should be denied while planning (NETWORK, MCP, RIG, and PUBLISH are). All three lists must change together so policy, UX, and planning semantics cannot drift.
  4. Add a kind-row entry to docs/guide/modes-and-permissions.md with a one-line summary of the new risk class + mode behavior.
  5. Add a verify assertion under tests/test_harness/ that asserts the new kind is a key in BOTH AUTO.policy and STRICT.policy, and in _planning_freezes if applicable. The structural test is the load-bearing backstop.

Adding a subagent profile

Subagent profiles (the .md files behind task <profile> and the /critique / /security-review / /deep-research bundled workflows) live in src/kin/harness/defaults/agents/ (bundled) + .kin/agents/ + .claude/agents/ (user + project, project wins).

  1. Create <name>.md with frontmatter:

    ---
    name: <name>
    description: <one-line, model-readable>
    max-turns: 0      # optional — 0/omitted = unlimited; positive values ≤1000
    tools:            # optional — omit entirely for "all tools available"
      - read_file
      - grep
      - shell
    ---
    
    <system-prompt body — same shape as the harness `compose_system`>

    Bundled profiles omit max-turns: registered children are supervised by the MCA and run until completion unless it pauses, interrupts, restarts, or kills them. Use a positive value only as an intentional operator leash; the root-shared token budget and doom-loop guard are the normal runaway boundaries.

    tools: is a YAML list of exact tool names (not a read|write|edit shorthand) — see the names registered in tools/__init__.py::default_registry(). There’s no model: frontmatter key: a profile always inherits the parent’s model unless the dispatcher passes a per-call model argument to task(...).

  2. For a profile that ships with kin (an exemplar), drop the file in src/kin/harness/defaults/agents/. For a user-only profile, drop it in .kin/agents/<name>.md or .claude/agents/<name>.md.

  3. For a critic-style profile (read-only, rubric-based), follow the critic-code.md template — see src/kin/harness/defaults/agents/critic-code.md for the rubric + severity-sorted output contract.

  4. If the profile is dispatched by a bundled command (like /critique), pair it with a defaults/commands/<name>.md that routes the body as kind: workflow and dispatches the agent subagent via the agent() primitive.

  5. Add coveragetests/test_harness/test_skills_agents.py / test_subagents.py for profile resolution + tool-allow-list enforcement; tests/test_workflow.py for the bundled-command wiring.

See src/kin/harness/AGENTS.md agents.py row for the discovery mechanics (5-root walk + project-shadows-bundled).

  • src/kin/harness/AGENTS.md — the canonical per-file harness guide (the source of truth for everything on this page).
  • docs/concepts/permissions.md — the conceptual frame for the mode → permission gate.
  • docs/guide/tools.md — the tool catalog (what each existing tool does, the kind each carries).
  • REFERENCE.md — every gotcha, footgun, security invariant, and postmortem. Read before touching the permission gate, the planning freeze, or any tool that touches the filesystem / network.