Skip to content

Approval

Status: Current. Source: ~/inhuman/harness/docs/runtime/approval.md

Parents: ../canon/grain.md, ../canon/proposals.md, session-job.md. Speech: this file. Not this file: operator chords (../client/operate.md); YAML dialect (../packages/policy-processor/docs/README.md); tape paint (../client/transcript.md); MCP tools globs / toolSeverity (../client/operate.md). Session modes (act / plan) are not permission mode. Honor does not approve.

Leaves authorize. Sequences project. orchestrationResult is the PEP boolean. orchestrated is orchestrationResult !== null. BODY is phase 2 after an allow-side guard.

An ApprovableJob carries floor and remainder votes plus an optional human bit. Sequence Jobs (SessionTurn, ProgrammaticJob) are not approvable; they expose a cursor over leaves a present human can answer. The policy program always runs while policyResult is still null. approved is HITL confirmation (a pre-obligation discharge). The host does not derive it. Plexus is the write surface. No approval RPC. No Permission PEW/Proposal.

Unattended: the eval client supplies persisted auto / yolo and does not write approved (../client/shell.md). Ungranted write covers; grant write or use yolo so edits do not hang. A classifier attempt that cannot vote writes judgeFailMessage and retries; it does not flip the session, does not record a human denial, and does not seal interaction_required. An attached TUI may paint the consent prompt so a present human can beat the retry loop.


Mint is not authorization to effect. registerExecutor(Class, guard, execute) is two functions: a guard (PEP, phase 1) that returns true | false, then execution (phase 2). Completions and the remainder classifier stay PEW. ToolCallJob is-a ApprovableJob. Sequences never approve themselves.

One policy. Three permission modes. One remainder PDP. manual, auto, and yolo all run {kernel}/policy.yaml. They differ on remainder ask and floor deny (consent prompt / classifier / allow). Floor block is deny-side in all three. Floor allow is allow-side in auto. Auto cluster floor ask is allow-side when autoAllow grants that cluster and the call is not hatch. Hatch is a skip-veto on that grant, not a second PDP. Ungranted write ask and deny cover in auto; the write flag is the write classifier (skip ask, classify deny).

approved is not the PEP bit. It is a human write. policyResult / judgeResult / the live permission mode are enough to decide. The decision itself is orchestrationResult.


Two languages join here. XACML / Anderson name who decides and who enforces. Selective prediction names when a model may answer, retrieve evidence, or abstain. Code field names stay.

Speech Code Meaning
PEP registerExecutor guard; orchestrationResult Reference monitor. Named law LICENSE WHEN is the join.
PDP floor policy.yaml; remainder program Floor first. Remainder PDP on floor ask or deny (except auto allow-skip and ungranted write). Never on floor allow. Never on floor block.
PIP evidence cuts Missing-attribute fetch. Round two is not a second vote.
PAP {kernel}/policy.yaml, config.json pointers Owning a remainder program is repointing, not a seeded copy.
Permit allow Effect is legal.
Deny (soft-mandatory) deny Remainder: manual cover, auto classifier (ungranted write covers), yolo allow.
Hard-mandatory Deny block Combining cannot lift it. No remainder PDP. Yolo does not rewrite it. Path !. Never a consent prompt.
NotApplicable ask No applicable floor rule, or a rule whose effect is defer.
Remainder autoRemainder Floor ask | deny (autoResidualOf) except auto allow-skip. Not ISO residual risk. Not block. Not floor-allow.
Hatch hatchShape Computed from name + argsJson. Skip-veto on autoAllow (and persist). Floor-allow is not remainder. Not a CRDT field. Inert in manual/yolo.
Permission mode manual | auto | yolo (standingPolicy / live-head currentPolicy) How remainder is handled. Closed tri-state. Unknown → manual. CLI always-approve aliases yolo; it is not a stored id. Last-choice for a new session is defaultPolicy (project, else kernel config.json; not a kv key).
Mode SessionRoot.mode act / plan. Not this join.
Classifier judgeResult; autoModeJudge Remainder PDP. Outcome is Permit or Deny iff judgeResult is still null. Never ask, never fail.
Abstain judgeFailMessage; judgeResult stays null Indeterminate. Retry. Not a decision.
HITL bit approved Human-only. Host never derives it.
Consent prompt cover / needsHuman Remainder ask or floor deny under manual; ungranted write ask and deny in auto; looking auto floor deny (autoCoverDeny, ungranted cluster); or auto remainder with Indeterminate and no judgeResult. Floor block is not coverable.
File-read remember SessionRoot.readAllowlist Prepared path the human allowed on a read-cluster remainder. Suffix descendants. Does not lift block. Not a write grant.

The agent acting as the user is the XACML subject. The tool is the action. Path / payload is the resource. Classifier input is evidence, never “subject.”

Do not say residual (say remainder); judge/quiz (say classifier); fuse (say rejection threshold (g)); license-the-PEP (say authorization; named law LICENSE WHEN stays); processor (say remainder PDP); policy-the-tri-state (say permission mode).


Geometry Meaning
ApprovableJob Leaf the PEP must authorize. Fields: policyResult, judgeResult, judgeFailMessage, approved. Completions and sequence Jobs are not Approvable.
Sequence Job SessionTurn, ProgrammaticJob. Not authorized. Expose approvalQueue / approvalQueueHead.
ToolCallJob The model’s one tool_call. Is-a ApprovableJob: emission and votes on one object. Kinds: SystemToolJob, FileMutateToolJob, MCPToolJob, WebSearchToolJob, WebExtractToolJob, AgentCallJob, AgentCallbackJob, AgentNotifyJob, AgentMessageJob, ContextNeedToolJob.
SessionTurn sequence
SystemToolJob / MCPToolJob / … Approvable leaf
ProgrammaticJob sequence (scheme bag)
ToolCallJob inner leaves

Admit of a model tool_call mints a ToolCallJob kind. scheme_repl is a bare ProgrammaticJob, not a wrap. Inner scheme-repl admits mint nested ToolCallJobs onto that bag.

There is no TurnJob class (SessionTurn is the turn). There is no ApprovableExpectation. There is no WorkDemand.


Authorization votes live on the leaf. Orchestration and cancel live on Job.

Field Type Writer
policyResult allow | deny | ask | block | null Daemon, after the policy program. null = not yet evaluated.
judgeResult allow | deny | null Daemon, after a finished auto remainder. No fail. No ask. Cancel / abort / crash leave this null.
judgeFailMessage string | null Daemon. Non-null after a failed auto judge attempt. Does not skip retry. Does not become a judgeResult.
approved true | false | null Human only. Host never derives this.
approvalComment string | null Human. Independent of approved. Tape paint reads this on the leaf. The next ReACT mints one TurnApprovalNoteChunk per nonempty comment (role: user after that round’s tool results). Not copied onto the sealed observation.
orchestrationResult true | false | null Daemon. The guard’s return. true = run execute. false = skip execute, stamp completed.
orchestrated computed boolean orchestrationResult !== null. Do not assign it.
cancelled boolean Daemon. Honest cancel flag. false → true. Not a deny, not a human approved.
completed boolean Daemon. Cancel stamps cancelled then completed and does not write orchestrationResult or approved. Deny writes orchestrationResult = false, receipt, then completed in the same action. BODY finally stamps completed.

POLICY ERROR IS AWARENESS. The daemon publishes policyError?: string on the session hub when the operator’s policy.yaml fails to load or throws. It is presence, not a leaf vote. A successful eval of the operator file clears it.

floor policyResult null → allow | deny | ask | block
residual judgeResult null → allow | deny
report judgeFailMessage null → string
human approved null → true | false
guard orchestrationResult null → true | false
cancel cancelled false → true
settle completed false → true

No reverse edge. A later machine does not rewrite an earlier one. A tool retry is a new Job. A judge retry is the same leaf only because judgeResult is still null.


The guard waits until some resolution exists. It does not wait for a consolidated approved. Code: ../packages/harness-sdk/src/models/session/license.ts licenseDecision.

when(
approved === true | false
|| policyResult === block
|| (readAllowlist hit && policyResult !== null && policyResult !== block)
|| policyResult === allow
|| (yolo && policyResult === ask | deny)
|| (auto && autoAllowSkip)
|| (policy === auto && autoRemainder && judgeResult === allow | deny)
)

HUMAN FIRST, THEN THE ALGO. Read approved first. Several guards may be true at once. The join is a disjunction — exclusive first-match is not the algebra. ONE PASS, ONE SIDE: the guard selects one side in that order and returns only that boolean, then stops.

allow-side (guard returns true → orchestrationResult = true → execute)
approved === true
|| (readAllowlist hit && policyResult !== null && policyResult !== block)
|| policyResult === allow
|| (yolo && policyResult === ask | deny)
|| (auto && autoAllowSkip)
|| (auto && autoRemainder && judgeResult === allow)
deny-side (guard returns false → orchestrationResult = false → receipt → completed; no execute)
approved === false
|| policyResult === block
|| (auto && autoRemainder && judgeResult === deny)

Once approved is false, or once orchestrationResult !== null, a later true does not start BODY. Mid-BODY approved = false is a no-op — stop is cancelled.

policyResult block → deny-side // all three modes; no remainder PDP
policyResult deny + manual → cover // yellow contour; approved stays null
policyResult deny + yolo → allow-side
policyResult deny + auto → remainder PDP // except ungranted write → cover
policyResult deny + write + auto + !write grant → cover
policyResult deny + write + auto + write grant → remainder PDP
policyResult allow → allow-side
policyResult ask + write + auto + !write grant → cover
policyResult ask + write + auto + write grant → allow-side
policyResult ask + auto + autoAllowSkip → allow-side
policyResult ask + manual → cover
policyResult ask + yolo → allow-side
policyResult ask + auto → remainder PDP (cover after fail; ungranted write covers)
policyResult null → wait

NO MANUAL SWITCH. A judge fail message does not write currentPolicy = manual. AUDIT SURVIVES OVERRIDE. A human write of approved does not clear policyResult / judgeResult / judgeFailMessage. LOOK FORWARD ONLY. The guard returns once.

HATCH. Computed hatchShape from name + argsJson (../packages/harness-sdk/src/models/session/hatch.ts). Skip-veto on autoAllow skip and persist. Floor allow is allow-side in auto. Bash: \bnpx\b, \bbunx\b. Not hatch: writes (lockfiles / Makefile / workflows are enable ! deny; .vscode / .claude / .cursor / .git/hooks are write_block; .claude/worktrees is write-ask; AGENTS.md is ordinary write-ask), npm run, cargo run, git commit, in-tree source, git add / fetch, read, MCP.

AUTO ALLOW. Cluster grants { write, bash, mcp } (flag record). Session overlay OR standing (config.json autoAllow). Fires only in auto. Floor ask for a granted cluster is allow-side unless hatchShape. MCP grant is look-ups only (stem floor). Extra-root deny and block are not skip-allow. Write flag off: floor ask and deny cover; classifier does not start (looking and unattended). Write flag on: floor ask is allow-side; floor deny classifies. Other clusters: empty standing remainder ask classifies (looking and unattended); cover after a fail message. autoCoverDeny (default true): interactive floor deny covers instead of classifying, unless that cluster is already granted — then it classifies. Unattended deny still classifies except ungranted write. Edit payload is not a vote. Manual ignores the flags. Yolo already rewrites remainder ask.


Third projection family on sequence Jobs. Not .work. Not collectors. Same children list, different cut.

needsHuman =
!completed &&
!cancelled &&
approved === null &&
!readAllowlist hit && (
(policy === manual && policyResult === ask | deny)
|| (policy === auto && autoRemainder !== null && judgeResult === null && (
judgeFailMessage !== null
|| !autoClassifierPending
))
)

policy here is SessionRoot.currentPolicy. Floor block is not coverable. Manual floor deny is coverable. Looking auto remainder ask classifies first; cover only after a fail message. Ungranted write ask and deny cover (no classifier). Looking floor deny covers when autoCoverDeny (default) unless that cluster is already granted — then it classifies. Unattended remainder classifies except ungranted write; cover only after a fail message. In-flight floor / first judge attempt is not in the queue except the looking deny cover and ungranted write. Do not drop a fail-message cover on unattended — a present human may beat retries; the eval client still does not write approved.

Walk: instanceof ApprovableJob / nested SequenceJob. Head is what chrome binds.


License is phase 1 of registerExecutor, not PEW honor.

registerExecutor(Class, guard, execute)
guard(job) → true | false // phase 1
execute(job) → await BODY // phase 2: only if guard returned true

The starter does not await. It starts the guard while orchestrationResult === null && !completed && !cancelled. When the guard returns: orchestrationResult = that boolean; false → receipt + completed, skip execute; true → await execute. Bootstrap orchestrated && !completed skips the guard and runs execute. A torn deny (orchestrationResult === false && !completed) only stamps completed.

COMM FIRST. AgentCallJob, AgentMessageJob, AgentCallbackJob, AgentNotifyJob, and side-channel tools (update_metadata, context_need) return true without the program. policyResult stays null. Spawn is not a grant — the child’s tools still run the floor.

ONE DENY WRITER. The guard returning false is the only deny settler. Host rejectTool does not write approved. Cancel writes cancelled, not a deny.

POLICY PROGRAM RUNS ONCE. Skip when policyResult !== null. Fail-open allow is forbidden. None of the three permission modes skip the program. Yolo is not allow: *. Manual is not ask-everything.

DEFAULT FLOOR NEVER THROWS. Missing / unloadable / throwing operator file → awareness policyError + dist default (../packages/policy-processor/reasonable-defaults.yaml). Leaving policyResult null and re-execing the broken file is forbidden.

IN-FLIGHT IS PROCESS-LOCAL. Cap = KernelRoot.speculativePolicyProcessingTools (default 2). Cap is floor produce only. Judging, backoff, human-wait, and BODY do not hold slots.

AUTO TRIGGER. Remainder PDP starts under auto when autoClassifierPending. Never floor block. Granted cluster floor ask is not remainder unless hatch. Looking remainder ask classifies firsthand except ungranted write (cover). Cover after a fail message. Unattended remainder classifies except ungranted write. Looking floor deny covers when autoCoverDeny (default); classifies when off, or when that cluster is already granted. Ungranted write deny covers in every auto seat. Dist ask is config.json autoModeJudge / autoModeAsk (default @inhuman.tools/policy-processor/auto-mode-ask.scm), loaded onto SessionRoot.judgeAskProgram. Classifier wire is session kvs.judgeConnection / judgeModel, then kernel kv.json last-choice (live overlay — mint does not re-seed), then that connection’s settings.model, then session chat connection / model. A failed attempt writes judgeFailMessage and retries unbounded (min(5 min, max(1 s, 1 s × n × ln(n)))). Empty connection gates the classifier the same way: no vote, retry. Identity allow / deny still votes without a completion.

CLASSIFIER CACHE. Remainder skip only. Floor first. Process-local on the session’s LicenseScope. Key is cluster+payload (bash command; read/write path; mcp name+args). Stores live remainder allow | deny. Never fail / abstain / human approved. Drop when judgeAskProgram changes. SessionRoot.judgeCache is a leftover document field; it does not authorize.

EVIDENCE. Three named PIP cuts, assembled by the host: privileged (user messages on the owning turn and its tail ancestry + this {name, arguments}), prior-turn (privileged + previous assistant response), quarantined (prior-turn + prior tool results, untrusted as instruction). Continue mints prompt: ""; the cut walks tail — empty privileged is no user text in that ancestry, not “this turn’s field is blank.” Unattended: the owning ProgrammaticJob.expr stands in for missing user messages. decision is the only switch; uncertainty / severity are judgements; retrieve selects a PIP cut. Rejection threshold (g) fires on an allow when uncertainty * severity exceeds a host constant (initial 50); it does not intercept a deny.

TIRITH COVERAGE. Host inspection, not a fence. analysis_incomplete is three facts: intel-gap (offline package lookup) is not a finding — it does not join, does not write policyMessage, and does not enter the classifier PIP; nested-incomplete-only is remainder ask (NotApplicable), not unliftable block; concrete rules (curl_pipe_shell, …) stay block / ask from Tirith’s action. Classifier policy is concrete findings only. Bash remainder kind incomplete is host-routed like write writeKind. Scanner error (the binary did not run) stays block. Hatch is unchanged.

DENY IS AN ANSWER. One outcomeJson shape for every deny-side guard:

{ ok: false, text: string, code?: string, note?: string }
human false → { ok: false, text: "User refused this tool use" }
human false + comment → same receipt; comment is a following user-role `TurnApprovalNoteChunk`
classifier deny → { ok: false, text: "Classifier marked this tool use as dangerous; ask user to confirm this action", code: "policy_denied" }
floor block / machine deny → { ok: false, text: "Policy prohibits this tool use. Do not try to bypass it; if this is crucial, ask user to do it", code: "policy_denied" }
cancel (not a decision) → { ok: false, text: "cancelled" }
timeout abort → { ok: false, text: "timed out after …ms" | abort reason }

Product law: BODY runs after orchestrationResult === true. Floor allow, yolo remainder, classifier allow, and a covering readAllowlist leave approved null.

Current intern: ../packages/harness-sdk/src/models/session/jobs/MCPToolJob.ts returns unless this.approved === true before minting McpToolExpectation. The Job executor does call ensureWork after an allow-side guard, so a licensed MCP leaf with approved === null intern’s nothing. Web-search / web-extract intern after BODY stamps connectionKey and do not check approved. System-tool BODY is the toolkit executor; it does not intern PEW.

Do not paper over this. Do not document ready: approved === true as the PEP — that is the intern bug, not LICENSE WHEN.


Permission mode After policy.yaml
manual Follow the program. Remainder ask and floor deny wait for a human. Floor block is deny-side. Floor allow is allow-side (hatch inert). HITL.
auto Follow the program. Granted autoAllow cluster floor ask is allow-side unless hatch. Ungranted write ask and deny cover (no classifier); persist may grant write. Other ungranted remainder ask: classifier (looking and unattended); cover after fail (persist may grant the cluster). Interactive floor deny covers (autoCoverDeny default) unless that cluster is granted — then it classifies. Unattended deny classifies except ungranted write. Floor allow is allow-side. Floor block is deny-side immediately. Indeterminate: stay auto, write judgeFailMessage, consent prompt + retry forever.
yolo Follow the program. Remainder ask and floor deny are allow-side. Hatch inert. block still holds.

Unattended uses the same guards. evalSpec is admission (auto | yolo only). Observer currentPolicy must not replace it. Unknown interactive tokens are manual. plan / act as permission-mode values: catalog error.

POLICY FOLLOWS HEAD. currentPolicy is a turn overlay; standingPolicy on SessionRoot is pre-head and first-turn attribute. LICENSE WHEN reads SessionRoot.currentPolicy (live-head inherit). Assignment writes standing and, when a head exists, the overlay. Last-choice persist is policy/set onto ProjectRoot.defaultPolicy + config.json defaultPolicy. Ontology: session-job.md.

{kernel}/policy.yaml floor. Always. Dist default on miss / throw.
config.json autoModeJudge auto remainder. Never floor block.
default: "@inhuman.tools/policy-processor/auto-mode-ask.scm"
config.json autoAllow standing cluster flags { write, bash, mcp }
config.json autoCoverDeny interactive floor deny covers (default true; persist false to classify). A granted cluster still classifies looking deny.
config.json defaultPolicy last-choice permission mode for a new session
ProjectRoot.defaultPolicy project last-choice; empty inherits kernel
SessionRoot.autoAllow this-session overlay
SessionRoot.standingAutoAllow hydrated from kernel
SessionRoot.policyProgram loaded floor body
SessionRoot.judgeAskProgram loaded remainder body
KernelRoot.speculativePolicyProcessingTools default 2; floor produce cap
KernelRoot.approvalAskThrottleMs default 2000; chrome, not the PEP
KernelRoot.superKey persist-mod; chrome
SessionRoot.readAllowlist extra-root read grant

The dist floor and remainder program live in ../packages/policy-processor/docs/README.md. Path jail after an allow-side decision stays the toolkit executor. yolo does not skip it.


The human writes the path. Chrome may offer a parent. The host never infers a grant.

In-workspace read is already silent ("." enable). Extra-root miss is floor deny (remainder). A human allow of a read-cluster remainder stores that prepared path on SessionRoot.readAllowlist (session). Suffix descendants are implied. ! block still holds. Writes are not this list.

Write Who Key stored
Once human allow of this leaf (approved === true) the leaf’s prepared path
Allow this folder human accept of a suggested parent that parent

LICENSE WHEN and needsHuman already honor a covering key. A comment cover does not write the list. There is no Always-this-folder on this list.

OFFER, NEVER INSTALL. The cover may suggest one directory up. Only a human chord writes it. Offer when the current extra-root read shares that parent with another observed extra-root read this session (readAllowlist key or another in-flight read on approvalQueue). A lone file does not offer. Denials are not evidence. Generalization is one directory up from the current ask, not LCA of the whole list — disjoint trees stay two grants, not $HOME.

Fences — fileReadParent returns nothing: . (workspace), /, one-segment (/tmp, /Users), $HOME (/Users/<name>, /home/<name>), volume roots. Relative prepared paths are in-workspace; they do not widen.

Chrome chords: ../client/operate.md. Matchers: ../packages/harness-sdk/src/models/session/read-persist.ts, ../packages/harness-sdk/src/models/session/read-widen.ts. Host rememberFileRead writes the leaf path after human-true; chrome writes a suggested parent then the bit.


An OS execute fence is possible. The cost is a second product. That cost is not paid.

Shell is /bin/sh -c. Remainder allow is host-uid. Steer is the floor — policy, Tirith, classifier — on the inspectable request, before BODY.

Rejected: named Grant.sandbox; a project-root walker as capture; bundling a native LSM CLI (nono / bwrap / Seatbelt) as a shipping default. Paying for a fence means owning a platform TCB, capture that sees outside the project without a 10k-file walk around every shell, fail-closed that does not refuse ordinary npm test on a machine without the LSM, and a Windows story that is not “unenforced.” Until those four are true, do not name a missing execute fence, do not add a wrapper argv, and do not walk the tree for a receipt. Path policy stays software (../packages/policy-processor/docs/path-policy.md).


Seat Owns
plexus-expectations Job geometry. registerExecutor(Class, guard, execute). PEW honor has no approve hook. Only Job in the package.
harness-sdk SessionTurn, ToolCallJob, ProgrammaticJob, ApprovableJob. needsHuman / licenseDecision / hatchShape.
harness-server Host guards and execute bodies. Publishes policyError.
policy-processor Match language. Dist never-throwing floor. Dist auto-mode-ask.scm. Not the tri-state.

Alternative Failure
Permission PEW/Proposal or approval RPC Plexus is the write. Actor does not consider UAC.
Host derives approved from the two votes Invents a second writer on the confirmation bit.
ready: approved === true as the PEP Yolo / floor / judge allow leave approved null. (MCP intern still has this bug — §8.)
manual / yolo skip the policy program Manual becomes ask-everything. Yolo becomes allow: *. block must hold in all three.
Crash flips currentPolicy to manual A network blip changes session policy. Stay auto, retry.
Crash writes judgeResult = fail Bootstrap then skips the classifier. There is no classifier fail state.
Auto remainder PDP on floor block block is the unliftable arm.
Hatch as a stored vote / rewrite policyResult to ask Hatch is computed. Floor allow stays allow.
Floor-allow remainder via autoHatch Auto stricter than manual for dist-approved npm run. Hatch is skip-veto, not a second PDP.
Named execute fence (Grant.sandbox) Remainder allow is host-uid. §11.
Auto-widen extra-root at k = 2 siblings The cover decides. The human does not.
LCA of the whole allowlist $HOME / / the moment two extra-root trees appear.
Folder grant ⇒ extra-root write Read remember is not a write grant.
Sequence Job is Approvable (SessionTurn.approved) Chrome answers the effector leaf.
License on PEW (ApprovableExpectation) Expectation exists before license. Pending asks pollute honor.
{ pickup: true } / stored orchestrated boolean Dual book. Two functions on registerExecutor already split guard from execute.
Cancel writes approved = false approved is human-only.
/approval auto as a product path The tri-state is SessionRoot.currentPolicy.
Plan / goal as deny-side guards in this join Those are session modes.
Remainder classifier on every in-tree write Ungranted write covers. Granted write skip is enable-hit ask only; deny still classifies. Payload is not a vote.
Write enable polarity approve (like read) Manual loses the Diff cover. Bash arg_rest_type: write_path would allow-side rm / tee / mkdir.

Doc Role
../canon/grain.md Causal spine; ToolCallJob is-a ApprovableJob
../canon/proposals.md Cancel / Question. Permission is not a Proposal
session-job.md POLICY FOLLOWS HEAD; collectors are not the cursor
../client/operate.md Cover chords, /mcp persist
../client/shell.md Unattended eval; cover shadows draft
../packages/policy-processor/docs/README.md Floor YAML; bash/path match language