Execution
Who may call a command and what the runtime does with the call, from the exec envelope through the fail-closed pipeline and wrap argv to the result.
Every Invoker reaches a command through one exec envelope and one fail-closed pipeline, which ends in a result an agent can branch on. Build your runtime's exec path against this page; Specification: Execute holds the normative text.
Invoker#
An Invoker is whoever runs a command, and HCP treats all four the same:
| Invoker | Typical face | Example |
|---|---|---|
| Human | CLI | ledger transfer 2.80 --to demo-transit --confirm --json at a terminal |
| Agent | CLI or JSON | A tool loop calling hcpx exec or Hcp.exec() |
| Product API | JSON | A backend posting { pack_id, verb_key, args } |
| UI | JSON | A button that fires the same envelope |
Invokers never redefine Policy or Binding
(L11). An Invoker carries a
Principal and adds no auth, confirm logic or
policy of its own, because with two policies on one command the weaker one wins.
Nor does its type pick the Binding mode: an agent acting for a human uses user
Binding for that human's mail, an operator job uses mothership Binding for
fleet wraps, and "it's an agent, give it the service token" is how elevation
bugs start.
Agent harness ─┐
Product API ─┼─► same command ─► Policy + grants ─► Binding mint ─► Adapter
UI gesture ─┤
Human CLI ─┘Routing agent calls through bash and UI calls through HTTP is a runtime detail,
as long as both reach the same command, Policy row and Binding under a
Principal. An agent Invoker runs unattended, so it stays non-interactive (no
prompts, -y on installers), asks for --json, reads availability before
planning, surfaces writes as approvals and passes confirm: true only after
agreement, and loads a Skill's lead first and the full text on demand. A UI Invoker fires the JSON envelope, shows
the confirm gate as an approval dialog and never pre-sets confirm: true, takes
badges and "credential stale" banners from the runtime's Sense sink without
owning mint, and never embeds credentials in the client.
The envelope#
{
"pack_id": "ledger",
"verb_key": "transfer",
"args": { "amount": 10, "confirm": true }
}| Field | Meaning |
|---|---|
pack_id | The pack. surface_id is accepted as a legacy alias |
verb_key | The command name |
args | Command arguments. confirm: true or apply: true satisfies a confirm gate. mode: "mock" requests explicit mock Binding |
The CLI face (ledger transfer 2.80 --to demo-transit --confirm --json) is the
same command, and
hcpx exec ledger transfer --json -- amount=2.80 payee=demo-transit confirm=true
is how the CLI sends the envelope.
The pipeline#
The steps run in order, and nothing runs after a denial.
| # | Step | Denies with |
|---|---|---|
| 1 | Resolve the pack, its activation on this session and the command's Policy row, which a linted pack always has | unknown pack <id>, pack <id> not activated, no policy for <verb_key> |
| 2 | Authenticate the Principal and take the tenant from the session. Land brain needs a project in scope, and land local binds the sandbox cwd | privacy.land brain requires project_id |
| 3 | Assert the capability: verb.exec.read for operation: read, verb.exec.write for operation: write, pack.connect for a connect command, with grants scoped by pack_id and verb_keys | denied: missing verb.exec.write for ledger.transfer |
| 4 | Enforce confirm: a Policy row with confirm: true needs confirm: true or apply: true in the envelope | confirm required |
| 5 | Mint Binding from the hub for this Principal, pack and family (user, local, mothership). public skips the step and mock must be explicit in the envelope | The mint failure, never a fallback to an environment token |
| 6 | Run the Adapter in the sandbox: a wrap renders argv, checks the allow-list and spawns, and a composite calls in-process | sandbox: bin "x" not allowlisted (exit 127) |
| 7 | Return a structured result and audit writes with the attribution tuple | None |
Wrap rendering#
For kind: wrap, the runtime builds argv from the command's cli[]:
cli[] ["ledger", "transfer", "{{amount}}", "--to", "{{payee}}", "--memo", "{{memo}}", "--confirm"]
args { amount: "2.80", payee: "demo-transit", confirm: true }
argv ["transfer", "2.80", "--to", "demo-transit", "--confirm", "--json"]
spawn <resolved wrap.bin> transfer 2.80 --to demo-transit --confirm --json (cwd = pack dir)- The first
cli[]token is the bin name and is dropped, sincewrap.binis what spawns; a relativewrap.binresolves against the pack directory. {{name}}renders asString(args.name)and--flag={{name}}as--flag=<value>. A missing value renders empty and drops out with the flag in front of it (--memoabove).- Without
cli[],wrap.argv_templaterenders with{{verb}}set to the command tail (setup.applybecomesapply). - The JSON flag (
--jsonunlessjson_flagsays otherwise) is appended, and the arguments go to the process as an array, with no shell. - The working directory follows
wrap.workspace.mode: the pack directory by default, the caller's project forproject, or a private directory forworkspaceandephemeral(Where the CLI runs). wrap.agent_envis merged into the child environment, andHCP_SANDBOXis set tosandboxornative-local.
Result envelope#
{
"ok": true,
"pack_id": "transit",
"verb_key": "plan",
"result": { "ok": true, "verb": "plan", "from": "A", "to": "B", "optionId": "opt-1" },
"stdout": "…",
"stderr": ""
}| Field | Meaning |
|---|---|
ok | true on exit 0 and no denial |
result | Parsed JSON stdout if it parses, else the raw string |
error | Present when ok is false: a denial reason, confirm required, stderr, or exit N |
stdout / stderr | Raw streams (wrap only) |
A denial comes back as a value, never a thrown error, so agents can branch on it. Errors has the full list.
Composite and other adapters#
composite returns an in-process result. openapi and sense_only are not
executable in the reference-local runtime, and a proprietary runtime implements
them behind the same exec signature. The bootstrap pack hcp-cli is composite:
its read commands (hcp.doctor, hcp.list, hcp.availability, hcp.skill)
return directly, and commands that change the store answer with
deferred_to_cli: true and a hint, because the CLI process owns the durable
store.