Skip to content

Repository files navigation

underseal

English | 简体中文

Sealed-file authorization protocol for delegating work between AI agents.

underseal is an authorization protocol for delegating bounded work from one AI agent to another. Its central claim: the chat body is not an authorization channel. A worker agent's only valid mandate is a hash-sealed file that a lead agent owns in the workspace — not a system prompt, not a task description relayed through a host, not anything that arrives as conversation. If a worker's instructions live in something a host, a prior turn, or a message layer can rewrite, truncate, summarize, or reorder, then whatever can shape that channel can shape the worker's authority. underseal removes the channel from the trust path and puts authority in a file instead: a small, dependency-free, fail-closed verifier that the lead and the worker both run against the same sealed bytes.

30 seconds

The smallest delegation underseal supports is the lite ceremony: an assignment, a receipt, and a READY line.

# lead: seal the task, then install the receipt the verifier computes.
# render-receipt prints "UNDERSEAL_RECEIPT <json>"; the receipt file is the JSON
# alone, so strip the marker before redirecting.
python3 underseal.py render-receipt --assignment .underseal/assignments/my_task.assignment.json \
  | sed 's/^UNDERSEAL_RECEIPT //' > .underseal/assignments/my_task.receipt.json

# worker: verify the seal before touching a single project file
python3 underseal.py verify --workspace-root . --task-name my_task
# UNDERSEAL_ACK {...}  -- exit 0 means these bytes are exactly what the lead sealed

# worker: record READY, THEN start work -- never the other way round
printf '%s\n' '{"assignment_document_sha256":"<from the ACK above>","seq":1,"state":"READY","summary":"verified sealed assignment before project writes","task_name":"my_task"}' \
  >> .underseal-runs/my_task.events.jsonl

# lead: after the worker reports done, re-derive acceptance -- never take its word for it
python3 underseal.py validate-progress --workspace-root . --task-name my_task
# UNDERSEAL_PROGRESS_OK 1

Every underseal command prints exactly one UNDERSEAL_* marker and exits 0, or prints one stable E_* code to stderr and exits 2. There is no third outcome and no --force. See docs/quickstart-lite.md for the full walkthrough with real output, and docs/quickstart-full.md for the resumable ceremony.

Why underseal exists

Three parties are involved in a delegated task. The principal is the human who wants the work done and who ultimately accepts or rejects it. The lead is the agent that seals the task, dispatches it, and checks the result; it owns a directory in the workspace, .underseal/, that nothing else may write. The worker is the agent that performs the task; it may touch only the files the sealed scope allows, plus its own append-only evidence log under .underseal-runs/. Authority is supposed to flow from principal to lead to worker — but in most agent-delegation setups today, it actually flows through a chat message, and a chat message is not a trustworthy carrier.

Once a lead hands work to a worker — often a different model, a different harness, sometimes a different vendor entirely, coordinating only through a shared workspace and whatever text passes between them — the worker's understanding of "what am I allowed to do" is usually reconstructed from a task description sitting in a system prompt or a message body. Anything positioned between the lead's intent and the worker's action can shape that text: truncation, summarization, caching, replay, a host that rewrites task labels, or a prompt injection the worker picked up from a source file it was asked to read for an unrelated reason. The worker has no way to tell an authentic instruction from an artifact of the pipeline it is running inside, because both arrive the same way — as words.

underseal's answer is to take the instruction out of that channel entirely. The lead writes the actual mandate to a file it owns, seals it with a verifier-computed receipt, and hands the worker only a pointer: a workspace root, a task name, a role. The worker's first action, before it changes anything, is to run the same verifier against the same file and confirm the seal holds. Every later step — dispatch, resumption, progress, acceptance — chains back to that one sealed hash. A chat message can still get corrupted in transit; it just can no longer grant anything by being corrupted.

lite vs. full

underseal ships two ceremonies, sealed as the assignment's ceremony field — never chosen from the command line.

  • lite is assignment + receipt + READY, nothing else. No dispatch binding, no resumable lineage, no mutual exclusion between concurrent tasks. Use it for a worker that runs to completion in one session: the blast radius is small and what you're buying is "the worker read the real assignment," not "the worker's whole lifecycle is reconstructible."
  • full adds a dispatch binding, a fixed-role resolve step, a hash-linked chain of activation generations, and dispatch-bound progress events. Use it when a task may be interrupted and resumed (a context budget runs out, a session dies, a host restarts it), when you need a guarantee that only one worker holds the active pointer, or when the acceptance ceremony itself needs to be as auditable as the work — down to retiring the pointer only after a validated, assignment-authorized halting outcome.

If you are not sure which one you need, start with lite. Reach for full the first time a task genuinely needs to survive a restart.

Relationship to spec.md

spec.md is the normative specification: wire formats, validation algorithms, error codes, and an explicit accounting of what underseal does and does not enforce. This README and the files under docs/ are onboarding material — accurate to the best of their authors' knowledge, but descriptive, not authoritative. Where anything here disagrees with spec.md, spec.md wins, and the implementation is conformant only to the extent it matches spec.md, not this file.

Scope and maintenance stance

underseal keeps a deliberately small, frozen core: one file, the standard library only, no configuration file, no plugins, no network calls, and nine subcommands —

render-receipt · verify · render-dispatch · resolve-dispatch · retire-dispatch · validate-progress · audit-scope · verify-bases · verify-pin

— each doing exactly one check and nothing adjustable about how hard it checks. That is not an artifact of an early version; it is the maintenance stance. Every verification-strength switch (ceremony, path_profile, control_paths, progress.required) lives inside the sealed document, never on the command line, and that is treated as load-bearing, not incidental.

Contributions that grow the surface area — a new subcommand, a new sealed field, a configuration flag, an alternate output format, a "convenience" mode — are held to a high bar: does this close an actual soundness gap, or does it just make the tool more pleasant to use? Convenience loses. The entire value of underseal is that a lead and a worker, on different machines, different harnesses, different vendors, at different times, can agree on exactly what happened without trusting each other's runtime — and that property gets weaker, not stronger, every time the verifier gets bigger. The specification is the primary artifact; this implementation is a conformance target for it, not the other way around, and it is not going to grow past what the specification demands. If you need something underseal doesn't do, that is very likely correct behavior on underseal's part: build it as a layer that consumes underseal's output, not as a patch to underseal's core.

Origins

underseal is a concrete protocol for what the literature calls a software delegation contract: a machine-checkable, hash-sealed statement of scope, mode, and acceptance criteria that binds an autonomous delegate. The concept is described in arXiv:2606.17099 without a reference implementation; underseal is, to our knowledge, the first one.

License

Apache-2.0. See LICENSE.

Releases

Packages

Contributors

Languages