Skip to main content

Building a tool or package

Three different things: adding a built-in tool to Kaizen's closed registry, publishing an authenticated operation through Kamiwaza Core's generic broker, and shipping a package that brings capabilities with it.

Adding a tool​

The registry is closed​

AGENT_LOOP_TOOL_NAMES names every tool the loop may register. It is asserted by a boundary test that walks the source, so the cardinality check fails the build if the set changes without the trip-wire being updated.

This is deliberate friction. Every new tool expands the loop's trust surface, and that should be a decision, not a merge.

The two trip-wires​

Adding a tool means touching two boundaries:

  1. The closed set in chat_pipeline/tools/__init__.py, with a comment explaining why the tool exists and what kind it is.
  2. The boundary test that asserts registry cardinality.

If you find yourself editing one without the other, stop — that is the check working.

Conditional registration​

Registration is per turn and conditional. Follow the existing patterns:

  • run_code is built only when SANDBOX_ENABLED and offered only to an agent whose grant explicitly names it;
  • studio tools register only when Agent Studio is enabled and only for the source-defined authoring agents;
  • retired ticket tools and bespoke action-card protocols do not register;

Baseline product reads may register for every chat agent. search_about is the reference pattern: it reads a build-time copy of the canonical SPA Markdown, returns bounded excerpts, and declares tool-owned natural-language intent hints so product questions receive first-step visibility without a central intent switch. Those hints can only prioritize a tool already authorized for the run; they never grant one.

The other baseline presentation-only tool is show_chat_canvas. It accepts one bounded Markdown item only when the member explicitly asks for inline canvas output, returns it through typed chat_canvas_outputs, and does not persist or execute the content. Keep this distinct from artifact creation: adding a temporary display must not acquire a saved-artifact identity or bypass the artifact write boundary.

A tool being in the closed set does not mean it is offered. Default to not granted.

A Prompt step is one reviewed way to add a Tool beyond an Agent's defaults. The editor stores an exact structural attachment, and the backend resolves it under the running member and effective step Agent on every validation and run. Do not teach a Tool to recognize its own name in prompt prose; content is not a grant. The execution-only overlay still passes through the ordinary tool boundary and cannot exceed the Agent's capability ceiling or administrator policy.

run_code requires artifact_targets on every invocation. File-producing calls list the bounded relative output names; compute-only calls send an empty list. Keep this field required when evolving the descriptor so every runtime can emit deterministic generation placeholders without inferring filenames from model-authored code.

Publishing through the Kamiwaza broker​

A Kamiwaza application or service does not need a Kaizen adapter when Core already publishes its operation in /api/openapi.json. Give the operation a stable unique operationId, a useful tag, bounded path/query/body schemas, and normal platform authentication. Core's OpenAPI auth metadata makes the route eligible and lets Kaizen omit an explicitly incompatible role. Public or auth-exempt routes are deliberately excluded.

Declare x-kamiwaza-effect as read, write, or sensitive when HTTP method alone does not describe the effect—for example, a POST search should say read. Without it, GET/HEAD/OPTIONS are reads, DELETE is sensitive, and other methods are confirm-gated writes. Malformed or incomplete operations are omitted or conservatively classified; Kaizen does not guess a safer contract from prose, an SDK client, or an application name.

The broker supplies the current API base, identity, role envelope, and workroom. Do not add those as operation arguments. It returns bounded JSON/text, metadata for unretained binaries, or a bounded SKILL.md from a requested skill ZIP. It never installs application code, runs package scripts, or turns discovery into data ingestion. New tag groups and operations then flow automatically through administrator selection, agent grants, member narrowing, lazy tool search, approvals, audit, and Core authorization.

Keep each generated parameter schema below 64 KiB and operation names unique after provider-safe normalization. The broker omits oversized schemas and every member of an ambiguous-name collision rather than guessing which operation to invoke. Response projection redacts credential-shaped keys and signed-URL secrets, but a service should still return the least sensitive response needed for the operation.

Do not publish plaintext credentials as agent parameters. Mark secret input writeOnly and split the operation into a trusted credential-intake ceremony plus an operation that accepts an opaque credential_ref. The compiler also rejects password/secret/private-key formats, credential-shaped property names, and unstructured authentication or configuration objects. This includes NPE and mutual-TLS material: certificate bundles, private keys, and their passwords belong in a binary-safe trusted intake surface, not JSON generated by a model.

Kamiwaza Core operations accept only the forwarded platform API base. A member can separately register an uncredentialed HTTPS OpenAPI source through the reviewed external-source adapter; the trusted backend stores its source pointer and routes calls through signed egress grants. Never reuse the caller's Kamiwaza identity headers for that external origin or accept a model-selected schema or operation origin.

request_document is an example of a composed conditional read: it appears only in a private turn where authorized search and run_code are both present. It accepts a retrieved uploaded-document chunk, rechecks the visible parent, and changes only execution-local context so the exact digest-pinned file can be mounted read-only. It is not a general identifier resolver and persists no grant.

Length bounds in a parameter schema​

Declare the real limit. A maxLength or maxItems on a tool parameter is a storage and ingest bound, enforced where the payload is actually stored, and it should say what that code enforces — a canvas body bounded at 100,000 characters declares 100,000.

Kaizen strips any bound at or above 2,000 from the descriptor it sends the runtime, so a large declared limit never reaches the model's schema. This is not cosmetic. A locally served model compiles the tool catalog into a grammar, and its parser refuses a repetition bound that reaches that threshold — then answers the turn with no grammar at all rather than reporting the refusal. One oversized field would otherwise stop structured output being enforced for every tool in the turn at once, and the only trace would be a line in the model server's log.

Two consequences for a tool author. A bound below 2,000 is passed through and can constrain the model on a locally served deployment, so it is worth declaring accurately on short fields; a hosted model treats it as advisory. A bound of 2,000 or more is advisory everywhere, and neither is a substitute for validating the argument when the tool runs.

Read versus write​

Declare the kind honestly.

KindMeaningConsequence
readNo effect outlives the turnPasses the read-dispatch path
writeEffect outlives the turnCrosses the write guard; may need approval

A tool that persists anything is a write tool even if it only touches the member's own store. Mislabelling one is the fastest way to create a hole.

What a tool implementation must do​

  • Execute as the initiating member. Never with an ambient or elevated identity.
  • Use the registered resource contract. For a delegated run, let Core classify the action, canonicalize its digest, reserve quota/approval and credential binding, and issue one sender-constrained effect capability.
  • Consume before protected code. The registered resource guard must consume the exact capability under the current run claim and fence before any model, credential, read, or mutation crosses that boundary.
  • Return a bounded, member-safe projection. Not raw system output — the projection is what stops credentials, ACL lists, or another member's content reaching model context.
  • Capture citation and presentation lineage where it produces evidence.
  • Fail closed. An unprovable authority is a denial, not a degraded call.
  • Never widen on failure. A failure returns less, never more.
  • Report failure by returning, not by raising. Return a result marked outcome="error" for anything you anticipated — a dependency that is down, an empty result, arguments that do not make sense. The model reads it and can recover. Raise only for a genuinely unexpected fault; see below for why the difference matters.
  • Declare timeout_s if your work is slow. One invocation is bounded, and a tool that overruns is cancelled so its session, connections, and any sandbox lease are released. Omit it and you get a conservative default. Registry package tools are different: Kaizen owns their runner call, automatically applies PKG_RUNNER_TIMEOUT_SECONDS to both the request and the outer tool invocation, and holds the turn's inactivity lease while that bounded call is active. Do not duplicate that deployment limit in package metadata.

Returning an error is not the same as raising​

Both report failure, but they reach the agent differently:

Read toolWrite / sensitive tool
Returns outcome="error"Model sees your message, turn continuesModel sees your message, turn continues
RaisesSettled into an error result; model sees only the exception textTurn closes — the runtime cannot tell whether your change landed

A raise from a write tool therefore costs the member their whole turn. Even from a read tool, raising throws away whatever explanation you would have written. Return the error.

Cancellation and turn-lifecycle breaches are never settled this way — those mean the turn is already over.

What a tool must not do​

  • Hold a model credential — model calls go through the gateway.
  • Accept identity from its arguments.
  • Implement its own audit path.
  • Decode or persist workload assertions, DPoP keys/proofs, Core capabilities, one-use consumption tokens, broker handles, or provider credentials.
  • Trust a resource ID, action classification, request digest, approval, quota, credential binding, or capability supplied by model/tool arguments.
  • Implement streaming, retries, salvage, or compaction — those are the shared pipeline's job.

Artifact range contract​

read_artifact and edit_artifact are core-owned adapters, not package tools. A ranged read returns at most 400 numbered lines plus the exact artifact version and content tag. An edit accepts at most 32 non-overlapping ranged PUT or CUT hunks against that immutable base. Packages may describe when their agent should use these tools, but must not replace owner resolution, room-version authorization, WriteToolGuard, optimistic concurrency, or artifact-kind validation.

Graph artifact contract​

create_artifact and update_artifact accept kind=graph as a strict JSON wire format. The root requires schema_version: 1, nodes, and edges; it may also carry title, status, scope, and sources.

  • A node requires id, label, and type.
  • An edge requires id, source, target, and label; type is optional.
  • A source requires id and may carry title, source, excerpt, and target_path.
  • Every edge endpoint must name an existing node. Every value in a node or edge's source_ids must name an entry in sources.

These names are literal. Do not emit node_type, relation_type, source_id, url, or evidence; map them to node type, edge type plus a readable label, source id, target_path, and excerpt. A structural rejection returns this complete contract in retry.expected_schema, and no artifact version is written.

Renderable artifact envelope​

Use the versioned renderable-artifact envelope when a tool or reviewed skill needs to retain structured remote data as a durable Artifact without turning producer data into HTML or script. The machine-readable v1 contract is docs/contracts/renderable-artifact-v1.schema.json. It carries a deterministic JSON payload, explicit renderer name, text fallback, security markings, source cluster/dataset/job references, bounded source context, and a safe JSON download filename.

Draft 2020-12 schema validation is necessary but not sufficient. Generic validators ignore the contract's x-kamiwaza-* annotations, while Kaizen's runtime also enforces canonical envelope and context sizes, JSON depth, nonblank trimmed required text, canonical table-column keys and map-form security-marking portion names, and uniqueness for those names. List-form security markings trim each value before checking uniqueness. Run every candidate through the shared runtime normalizer; docs/contracts/README.md lists these runtime-only checks, and Limits & quotas lists the fixed values.

payload contains a semantic type plus exactly one authorized value. Use data for the complete governed JSON result. If duplicating that result is not appropriate, use result_ref={id,content_digest,version?} instead. id is an opaque server-side identifier, never a URL, and content_digest is the exact sha256: binding. This release validates and preserves result_ref, but does not resolve it. The UI uses the producer's text_fallback; the JSON view and download retain the opaque reference. A future reviewed server-side resolver must use the current member and room authority, fail closed when the result is missing or its digest does not match, and never resubmit the source job or reconstruct fields that the governed result omitted. The browser must not fetch the reference directly.

security.markings accepts either a nonempty list or a nonempty map of portion name to marking. Use the map when the governed result distinguishes a banner, summary, rows, geometry, or other portions; do not flatten those values. A producer-side markings_error is a hard failure: do not create the renderable artifact and do not infer or default a replacement marking.

Provenance and security markings are immutable across Artifact revisions. If a new result has a different cluster/dataset/job binding, source context, or markings, create a new Artifact instead of updating the old one.

An instruction-only skill cannot author this envelope through the model-callable create_artifact or update_artifact tools. Those tools reject the reserved kamiwaza-renderable-artifact language so model output cannot fabricate authoritative markings or provenance. The skill must call a reviewed producer tool whose trusted result carries the complete envelope. An internal tool places it under ToolResult.presentation.artifact_outputs[].envelope; it reaches the same owner-scoped audited writer. A vendored package is not one of those producer forms. The package boundary rebuilds every declared artifact from allowlisted fields, so a package's envelope, its materialization_key, and a declared kamiwaza-renderable-artifact language are dropped before the declaration reaches the materializer — package output is saved as an ordinary artifact, never as an attested one. A generic MCP producer declares _meta["io.kamiwaza/artifact"]={"version":2,"source":"/<json-pointer>"}, where source selects the complete envelope from the successful result. The shared runtime validates and canonicalizes either trusted form, establishes a call-scoped server attestation that model arguments cannot set, and only then invokes the existing writer. Direct model create/update attempts fail before a version is written. Every declared durable output is included in the materialization result, including invalid declarations and declarations beyond the four-output persistence ceiling. Kaizen persists only the validated outputs, strips every raw declaration from the returned tool result, and reports saved and failed counts through the bounded _artifact_output notice. When any declaration cannot be saved, the notice uses a server-owned failure instruction; a producer's custom response instruction cannot conceal that failure or cause raw content to be repeated in chat.

For the generic MCP form, administrator approval and registration of the server and tool—not schema validation—is the producer trust root. Kaizen's attestation means that output from that selected producer passed the envelope checks and the model could not substitute writer arguments. It does not independently verify the truth of the producer's source references, provenance, or markings. Review how the producer derives those fields before enabling it; a careless or compromised producer can mint false metadata that is still structurally valid, and operators must disable or revoke that producer when its integrity is in doubt.

Only the reviewed native table renderer is active in v1. Other names are portable extension points, not executable authority: Kaizen displays text_fallback and preserves the payload for JSON download until a reviewed UI adapter supports that exact name. Never put credentials, bearer tokens, or an unfiltered provider response in payload, result references, or provenance context. Security markings describe handling; they do not grant access or replace the member ACL.

An extension renderer claim is the exact triple (renderer.type, payload.type, payload.version). Producers should emit a payload.version integer from 1 through 65,535. Omit the field for an unversioned payload; explicit null, booleans, fractions, and values outside that range are invalid. Integral JSON numbers such as 1.0 normalize to an integer before persistence and select the same renderer claim as 1. An unversioned payload remains portable data but can never select an installed pack. The envelope cannot name a module, package, asset path, URL, or pack version.

Renderer code belongs in a separately versioned pack repository. Its backend entry point exports host API version 1, its canonical pack id and version, the ordered receipt claims, and raw plus normalized validators. Its browser ESM exports the same identity and mountRendererV1({ root, envelope, assets, signal }). Browser assets are addressed only by logical id through assets.get(id), which returns verified bytes and media type; renderer code must not fetch a URL. Each declared asset may be requested only once per renderer session. Cache and reuse its returned bytes; a duplicate request terminates the renderer session.

Kaizen loads a pack only from immutable derived API and web images whose exact install receipts match. The browser module runs in an opaque, authority-minimized frame and receives only the validated envelope and declared bytes over a private channel. On every Artifact detail read, the active backend pack must readmit the exact stored content and bind that decision to its SHA-256 before the browser may mount. Unknown or disabled claims, receipt skew, validation failure, and renderer failure all preserve the Artifact and use text_fallback. See Artifact renderer packs for the build, review, rollout, security, and rollback contract.

When security.markings is portioned, banner is the exact top-level marking owned by Kaizen's outer Artifact chrome. It must be present when the governed result requires a top-level banner. Renderer code may show subordinate summary, window, or geometry portions, but must not duplicate, translate, hide, or replace the host banner.

Building a package​

Packages bundle tools and may include third-party code.

The security property you inherit​

Approved packaged tools run in the package-tool-runner: a separate, secretless process, deliberately without the tokens or keys the trusted backend holds. Proposal testing is a different phase: authored files are validated in the existing isolated sandbox worker before an administrator can approve them.

An active package Tool can also be attached to one Prompt step without adding the package to the stored Agent. The attachment records the active package UUID and exact Tool name. At execution, Kaizen reloads that pin, rejects disabled or missing packages, clamps the Tool to the step Agent's capability ceiling, checks name collisions, and uses the same posture-resolved package dispatcher. This path does not bypass package admission, the secretless runner, approval, collaboration classification, or audit.

An untrusted package that executes successfully still cannot reach anything sensitive, because there is nothing sensitive in its process to reach.

The runner also treats every package and materialized workspace filename as untrusted. Use relative paths without parent segments or control characters. Before a package resource is read, Kaizen proves lexical containment, opens it without following a final symlink, and rechecks the opened descriptor against the content-addressed package root. A malformed path or symlink escape is a terminal package-load error.

Design to that boundary. If your package seems to need a backend credential to work, the design is wrong — the capability belongs on the backend side of the callback, with the package asking for it.

Lifecycle​

Members can draft, test, and submit package proposals. Administrators review, approve, upgrade, disable, and delete.

The review exists because packages carry code. Creating an agent for yourself needs no approval; shipping code that others will run does.

Package Creator keeps a draft recoverable across turns. Reload the proposal before changing it and send its current optimistic version with every patch. Manifest patches are closed and preserve omitted fields. Local-package file patches operate on named relative POSIX paths, support preview before apply, and validate the entire resulting tree atomically. The current limits are 64 files, 128 KiB per file, 1 MiB across the tree, and a required package.json. Traversal, absolute paths, reserved roots, duplicate removals, and oversized trees fail without changing the draft. Changing any byte clears stale test evidence. A native Pi package also clears its receipt when any reviewed manifest field, ref, security posture, or package format changes.

Shared lifecycle extensions are product code​

Do not use a member-authored package extension to change the shared agent lifecycle. Member-authored extensions load only during disposable sandbox validation. At runtime, Kaizen composes their reviewed tools, skills, prompts, and themes through the existing package boundary. It does not activate their Pi lifecycle hooks in the trusted sidecar.

A shared lifecycle extension requires a source change under sidecar/pi. Add one statically imported factory and one descriptor with an exact ID, version, unique order, host-protocol range, and allowed hook set. Use only context, before_provider_request, agent_start, agent_end, agent_settled, turn_start, turn_end, tool_call, or tool_result. The registry loads the complete enabled set into temporary state, inspects Pi's registrations, and publishes one immutable chain only when every descriptor passes.

If the extension declares a tool, its descriptor must name an existing reviewed package identity. The declared name and parameter schema must match Python's frozen authorized tool snapshot exactly. The extension executor is discarded. The existing authenticated callback remains the only path to first-party execution or the secretless package runner.

Do not register commands, flags, shortcuts, renderers, provider adapters, headers, models, session actions, UI, shell commands, messages, journal entries, or direct tool execution. Add or change a hook only with shared-host tests that prove ordering, timeout, rollback, output validation, and identical behavior across every pipeline controller.

Native Pi packages​

Set package_format to pi only for a locally authored package. This selects Kaizen's native Pi authoring profile; it does not select a second runtime or version. Package Creator accepts no Pi-version field. The sandbox image is built from the same canonical Pi lock as Kaizen's Pi sidecar, and the validation receipt records the runtime version that actually loaded the proposal.

The authored package.json must:

  • use the same name and exact version as the proposal;
  • include pi-package in keywords;
  • declare resources under pi.extensions, pi.skills, pi.prompts, or pi.themes, using package-relative files, directories, or globs;
  • put Pi host packages such as @earendil-works/pi-coding-agent in peerDependencies with *, never in bundled dependencies;
  • exact-pin every ordinary dependency and development dependency; and
  • omit install and publish lifecycle scripts. Dependency installation always uses --ignore-scripts as a second boundary.

Kaizen composes only Pi tools, skills, prompts, and themes. Commands, flags, events, shortcuts, custom renderers, and other host hooks are unsupported. The real loader reports them and validation fails; omitting them from the reviewed manifest does not make them acceptable. Tool names and parameter schemas, and skill names and paths, must exactly match the surface Kaizen reviews. When a member explicitly names one of those authorized tools, the runtime gives that exact descriptor first-step visibility so the model can call it directly. The hint is intersected with the frozen run catalog and cannot grant an omitted or unauthorized package tool. An unambiguous imperative—“must call tool” or “use/call tool exactly once”—also binds that one already-authorized name into the signed Pi run policy. Substitutes and an early answer are held until the call is attempted; ordinary mentions, negative instructions, ambiguous multi-tool directives, and unauthorized names do not bind. Package authors do not implement this gate. Keep it in the shared runtime so package and first-party tools receive the same semantics and Python remains the execution authority.

test_package performs pure metadata, dependency, path, and declaration checks before dispatch. A skill-only package goes directly through Pi's real skill loader and does not need a fake Node test. A package with extensions must first pass its real node:test suite. The worker then discards that test tree, creates a fresh exact tree from the server-supplied files, and reinstalls dependencies using the time remaining in the same validation deadline. Pi's extension loader therefore cannot observe files rewritten by tests.

Authored extensions load in a disposable child process. The trusted validator parent imports no authored module; it accepts only a bounded child observation, resolves static resources itself, and owns the inherited descriptor carrying the final result. Do not make package validation depend on stdout or on mutations performed by a test. Both skill and extension paths execute in the deployment-selected sandbox worker; neither runs in the backend or falls back to the package runner.

The bounded result includes an artifact hash, reviewed-contract digest, observed runtime profile and version, whether Node tests ran, the discovered surface, unsupported registrations, and actionable diagnostics. Repair only the reported proposal fields or files, reload the current version, and retest. Submission and approval require a current passing receipt for the exact bytes and reviewed contract even when the general package scan policy is set to warn.

Testing is a property of a saved draft, not a step on the way to submitting one. Package Creator runs test_package once a saved proposal has files, and always when you ask it to test, so a draft you deliberately leave unsubmitted still carries a receipt a reviewer can read. Asking for a package "without submitting it" therefore still produces tested bytes.

Instruction-only packages​

A package can contribute reviewed instructions without executable tools. Put those instructions in top-level manifest skills; each authored skill has a name, optional description and safe path, and a bounded non-empty body. That is the runtime source composed into an agent's persona after approval.

metadata.pi_ui_resources.skills is only presentation metadata. It can make a skill discoverable in the UI, but it is not a substitute for top-level skills and does not activate instructions by itself. Package Creator mirrors authored skill name, description, and path into that display projection while keeping one shared runtime body.

For a native Pi instruction-only package, the authored skill file must be in the pi.skills resource set and its body must exactly match the reviewed top-level skill body. Pi discovery and Kaizen review must identify the same skill name and path.

When another agent draft is waiting for the package, return the typed proposal handoff. Do not invent a package grant. Agent Builder persists the exact same-owner proposal as a dependency and resumes only after administrator approval produces an active package with the same name and ref.

Capability ceilings still apply​

A package's tools are subject to the same two ceilings as any other: the agent's capability set, and the running member's permissions. A package cannot grant itself reach.

For delegated Tasks there are additional non-bypassable ceilings: the active automation grant, registered workload role/revision, Core run claim and fence, immutable authority deadline, protected-resource descriptor/guard, member and workload quota ledgers, exact approval, and current cancellation/readiness. These checks belong to the trusted backend callback. The secretless package runner receives only the bounded input and projection needed for approved package code; it never receives delegated or brokered authority material.

Each package tool also has a collaboration_class. Omit it, or set it to unknown, when the tool must remain unavailable in durable shared rooms. The package manifest accepts only actor_bound_read, actor_private_read, actor_bound_write, and unknown. This is reviewed admission metadata, not a field package code can return at runtime. Actor-bound classes keep execution on the initiating participant; actor-private reads are still removed from shared rooms. Packages cannot declare audience-intersection or conversation-resource authority because those scopes require a Kaizen-owned adapter that can prove the complete room audience.

Delegation and member capability selection can only narrow this surface. The request-scoped agent specification is intersected again when runtime package wrappers are built, so a stored registry grant cannot reintroduce a tool that a parent ceiling or the current selection removed.

MCP​

Kaizen can record MCP tool descriptors on an agent package, but the current version does not host or execute them. They are declared for forward compatibility.

Do not build a package that depends on live MCP execution, and do not present MCP tools to members as working capability.

Testing​

  • Unit the tool's projection: given raw output, what does the model see?
  • Boundary the registry: cardinality and closed-set membership.
  • Integration the authority path: does an unauthorized call deny rather than degrade?
  • Failure the failure mode: does a timeout return the original authorized result and continue the turn?

For a brokered Core operation, also validate the emitted OpenAPI document: authentication metadata is present, operationId is stable and unique, schemas are bounded, the tag is meaningful, and the effect annotation is honest.

Checklist​

  • Closed set updated and the boundary trip-wire updated.
  • Kind declared honestly — anything persistent is write.
  • Registration conditional; ungranted by default.
  • Executes as the initiating member.
  • Protected operations use a registered Core resource contract and guard.
  • Exact effects are reserved and consumed under the current claim/fence.
  • Returns a bounded, member-safe projection.
  • Fails closed; never widens on failure.
  • Anticipated failures return outcome="error" rather than raising.
  • timeout_s declared where the work legitimately runs long.
  • No model credential; no ambient identity; no bespoke audit path.
  • Packaged code assumes a secretless process.
  • Native Pi packages select package_format=pi, not a Pi version.
  • Pi metadata, exact dependency pins, supported resources, Node tests where required, loader surface, and current validation receipt all pass.