approval-md 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -24
- package/SPEC.md +57 -11
- package/dist/src/channels/contract.d.ts +34 -1
- package/dist/src/channels/contract.js +200 -7
- package/dist/src/channels/contract.js.map +1 -1
- package/dist/src/channels/telegram.d.ts +123 -11
- package/dist/src/channels/telegram.js +218 -23
- package/dist/src/channels/telegram.js.map +1 -1
- package/dist/src/channels/web.d.ts +9 -0
- package/dist/src/channels/web.js +17 -0
- package/dist/src/channels/web.js.map +1 -1
- package/dist/src/cli/amend.js +214 -30
- package/dist/src/cli/amend.js.map +1 -1
- package/dist/src/cli/attest.d.ts +9 -0
- package/dist/src/cli/attest.js +134 -7
- package/dist/src/cli/attest.js.map +1 -1
- package/dist/src/cli/channel-telegram.d.ts +99 -26
- package/dist/src/cli/channel-telegram.js +311 -13
- package/dist/src/cli/channel-telegram.js.map +1 -1
- package/dist/src/cli/channel.d.ts +9 -0
- package/dist/src/cli/channel.js +9 -0
- package/dist/src/cli/channel.js.map +1 -1
- package/dist/src/cli/codex-bridge.d.ts +819 -0
- package/dist/src/cli/codex-bridge.js +1607 -0
- package/dist/src/cli/codex-bridge.js.map +1 -0
- package/dist/src/cli/codex.d.ts +1 -1
- package/dist/src/cli/codex.js +304 -7
- package/dist/src/cli/codex.js.map +1 -1
- package/dist/src/cli/daemon.js +4 -1
- package/dist/src/cli/daemon.js.map +1 -1
- package/dist/src/cli/doctor.js +467 -12
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/execute.js +25 -2
- package/dist/src/cli/execute.js.map +1 -1
- package/dist/src/cli/help.d.ts +6 -2
- package/dist/src/cli/help.js +165 -60
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +49 -1
- package/dist/src/cli/hook-codex.js +60 -1
- package/dist/src/cli/hook-codex.js.map +1 -1
- package/dist/src/cli/hook.d.ts +459 -3
- package/dist/src/cli/hook.js +1062 -114
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.js +1 -1
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/main.js +5 -3
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/policy-apply.d.ts +195 -0
- package/dist/src/cli/policy-apply.js +573 -0
- package/dist/src/cli/policy-apply.js.map +1 -0
- package/dist/src/cli/policy.js +14 -1
- package/dist/src/cli/policy.js.map +1 -1
- package/dist/src/cli/preflight.d.ts +151 -13
- package/dist/src/cli/preflight.js +398 -41
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/sandbox.js +17 -1
- package/dist/src/cli/sandbox.js.map +1 -1
- package/dist/src/cli/scaffold.d.ts +1 -1
- package/dist/src/cli/scaffold.js +1 -1
- package/dist/src/cli/setup-channel.d.ts +9 -0
- package/dist/src/cli/setup-channel.js +28 -1
- package/dist/src/cli/setup-channel.js.map +1 -1
- package/dist/src/cli/setup-common.d.ts +3 -1
- package/dist/src/cli/setup-common.js +3 -2
- package/dist/src/cli/setup-common.js.map +1 -1
- package/dist/src/cli/setup.d.ts +2 -0
- package/dist/src/cli/setup.js +94 -2
- package/dist/src/cli/setup.js.map +1 -1
- package/dist/src/cli/up.js +115 -51
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/values.js +3 -4
- package/dist/src/cli/values.js.map +1 -1
- package/dist/src/cli/verb-registry.js +174 -9
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +2 -2
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/broker.d.ts +229 -0
- package/dist/src/codex/broker.js +548 -0
- package/dist/src/codex/broker.js.map +1 -0
- package/dist/src/codex/runner.d.ts +178 -0
- package/dist/src/codex/runner.js +231 -0
- package/dist/src/codex/runner.js.map +1 -0
- package/dist/src/codex/serve.d.ts +56 -0
- package/dist/src/codex/serve.js +98 -0
- package/dist/src/codex/serve.js.map +1 -0
- package/dist/src/codex/workspace-commit.d.ts +219 -0
- package/dist/src/codex/workspace-commit.js +549 -0
- package/dist/src/codex/workspace-commit.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +51 -0
- package/dist/src/core/advance-cycle.js +66 -2
- package/dist/src/core/advance-cycle.js.map +1 -1
- package/dist/src/core/agents-md.d.ts +20 -18
- package/dist/src/core/agents-md.js +33 -31
- package/dist/src/core/agents-md.js.map +1 -1
- package/dist/src/core/attest.d.ts +215 -0
- package/dist/src/core/attest.js +317 -7
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +18 -0
- package/dist/src/core/audit.js +13 -0
- package/dist/src/core/audit.js.map +1 -1
- package/dist/src/core/channel-owner.d.ts +213 -0
- package/dist/src/core/channel-owner.js +358 -0
- package/dist/src/core/channel-owner.js.map +1 -0
- package/dist/src/core/command-class.d.ts +154 -0
- package/dist/src/core/command-class.js +673 -20
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/commit-guard.d.ts +272 -0
- package/dist/src/core/commit-guard.js +424 -0
- package/dist/src/core/commit-guard.js.map +1 -0
- package/dist/src/core/daemon-actor.d.ts +45 -0
- package/dist/src/core/daemon-actor.js +54 -0
- package/dist/src/core/daemon-actor.js.map +1 -0
- package/dist/src/core/dark-session.d.ts +109 -8
- package/dist/src/core/dark-session.js +266 -82
- package/dist/src/core/dark-session.js.map +1 -1
- package/dist/src/core/decision-refusal.d.ts +23 -2
- package/dist/src/core/decision-refusal.js +24 -2
- package/dist/src/core/decision-refusal.js.map +1 -1
- package/dist/src/core/env-file.d.ts +5 -0
- package/dist/src/core/env-file.js +60 -1
- package/dist/src/core/env-file.js.map +1 -1
- package/dist/src/core/execute.d.ts +15 -2
- package/dist/src/core/execute.js +15 -2
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/gate.d.ts +86 -1
- package/dist/src/core/gate.js +81 -1
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/gesture-refusal.d.ts +166 -0
- package/dist/src/core/gesture-refusal.js +188 -0
- package/dist/src/core/gesture-refusal.js.map +1 -0
- package/dist/src/core/harness-version.d.ts +1 -1
- package/dist/src/core/harness-version.js +3 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/instance.d.ts +59 -2
- package/dist/src/core/instance.js +113 -0
- package/dist/src/core/instance.js.map +1 -1
- package/dist/src/core/log.d.ts +39 -1
- package/dist/src/core/log.js.map +1 -1
- package/dist/src/core/policy-explain.d.ts +10 -0
- package/dist/src/core/policy-explain.js +32 -0
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +41 -1
- package/dist/src/core/policy-load.js +21 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +43 -0
- package/dist/src/core/policy-match.js +52 -0
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +52 -0
- package/dist/src/core/policy-proposal.js +102 -2
- package/dist/src/core/policy-proposal.js.map +1 -1
- package/dist/src/core/protected-path-guard.d.ts +117 -4
- package/dist/src/core/protected-path-guard.js +362 -48
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/question-preempted.d.ts +141 -0
- package/dist/src/core/question-preempted.js +152 -0
- package/dist/src/core/question-preempted.js.map +1 -0
- package/dist/src/core/read-scope.d.ts +172 -0
- package/dist/src/core/read-scope.js +252 -0
- package/dist/src/core/read-scope.js.map +1 -0
- package/dist/src/core/sandbox.d.ts +81 -0
- package/dist/src/core/sandbox.js +190 -1
- package/dist/src/core/sandbox.js.map +1 -1
- package/dist/src/core/sender-identity.d.ts +476 -0
- package/dist/src/core/sender-identity.js +572 -0
- package/dist/src/core/sender-identity.js.map +1 -0
- package/dist/src/core/shlex.d.ts +102 -0
- package/dist/src/core/shlex.js +159 -0
- package/dist/src/core/shlex.js.map +1 -0
- package/dist/src/core/values.d.ts +18 -8
- package/dist/src/core/values.js +36 -1
- package/dist/src/core/values.js.map +1 -1
- package/dist/src/daemon/advance.d.ts +10 -0
- package/dist/src/daemon/advance.js +25 -4
- package/dist/src/daemon/advance.js.map +1 -1
- package/dist/src/daemon/daemon.js +9 -0
- package/dist/src/daemon/daemon.js.map +1 -1
- package/dist/src/daemon/git-evidence.d.ts +2 -2
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/mcp/server.js +8 -0
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/cli-reference.md +932 -32
- package/docs/codex-enforced-session.md +75 -2
- package/docs/codex-workspace-broker.md +118 -0
- package/package.json +3 -1
- package/schema/event.schema.json +538 -9
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
- package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
- package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
- package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
- package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
- package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
- package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
- package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
- package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
- package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
- package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
- package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
- package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
- package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
- package/schema/fixtures/policy/valid/canonical.json +1 -1
- package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
- package/schema/fixtures/policy-md/valid/canonical.md +1 -1
- package/schema/fixtures/policy-md/valid/with-values.md +5 -7
- package/schema/fixtures/values/invalid/class-shaped.json +1 -1
- package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
- package/schema/fixtures/values/invalid/non-string-item.json +1 -1
- package/schema/fixtures/values/invalid/over-cap.json +1 -1
- package/schema/fixtures/values/invalid/unknown-key.json +1 -1
- package/schema/fixtures/values/invalid/version-float.json +1 -0
- package/schema/fixtures/values/invalid/version-integer.json +1 -0
- package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
- package/schema/fixtures/values/valid/empty-lists.json +2 -3
- package/schema/fixtures/values/valid/full.json +5 -7
- package/schema/fixtures/values/valid/minimal.json +1 -1
- package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
- package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
- package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
- package/schema/fixtures/values-md/invalid/version-1.md +69 -0
- package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
- package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
- package/schema/fixtures/values-md/valid/absent.md +1 -1
- package/schema/fixtures/values-md/valid/with-values.md +5 -7
- package/schema/policy.schema.json +54 -2
- package/schema/values.schema.json +7 -11
- package/schema/fixtures/values/invalid/version-string.json +0 -1
package/dist/src/cli/hook.d.ts
CHANGED
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
* auditor holding the log alone. See `docs/claude-code-hook.md`.
|
|
73
73
|
*/
|
|
74
74
|
import { type ClassifiedSegment, type CommandClassification, type ProtectedPathEntry } from "../core/command-class.js";
|
|
75
|
+
import { type GateOptions } from "../core/gate.js";
|
|
76
|
+
import { type HarnessKind } from "../core/harness-version.js";
|
|
77
|
+
import { type HarnessLoopState } from "../core/loop.js";
|
|
78
|
+
import type { EventRecord } from "../core/log.js";
|
|
75
79
|
import type { Streams } from "./main.js";
|
|
76
80
|
import { type Style } from "./style.js";
|
|
77
81
|
/**
|
|
@@ -115,6 +119,29 @@ export declare const HOOK_DENY_CODES: readonly [
|
|
|
115
119
|
* rejection: nobody decided anything, so there is nothing to ask again.
|
|
116
120
|
*/
|
|
117
121
|
"hook-class-human-only",
|
|
122
|
+
/**
|
|
123
|
+
* A `harness.launch.*` class that no rule of this policy names (APRV-354).
|
|
124
|
+
*
|
|
125
|
+
* SPEC.md §7 says the family is never inferred autonomous; this is the
|
|
126
|
+
* stronger reading the family needs, which is that it is never inferred at
|
|
127
|
+
* all. A launch resolves only under a rule an operator wrote, and a policy
|
|
128
|
+
* that names neither `harness.launch.*` nor the specific member refuses.
|
|
129
|
+
*
|
|
130
|
+
* It exists because of the window the softer reading opens. Before the family
|
|
131
|
+
* existed, `codex …` and `muse …` were `hook-unclassified`: refused outright.
|
|
132
|
+
* Letting the new class fall to `defaults.autonomy` would have made every
|
|
133
|
+
* harness launch grantable by one approval in every project whose defaults
|
|
134
|
+
* are manual, the moment they upgraded — a capability arriving by upgrade
|
|
135
|
+
* rather than by decision. What that approval would cover is a whole second
|
|
136
|
+
* agent whose own actions this gate never sees.
|
|
137
|
+
*
|
|
138
|
+
* Distinct from `hook-unclassified`, which says the CLASSIFIER has nothing to
|
|
139
|
+
* say about the command; here the classifier was clear and the POLICY is
|
|
140
|
+
* silent. Distinct from `hook-class-human-only`, which is a policy that has
|
|
141
|
+
* spoken and reserved the class: the repair there is for a person to run the
|
|
142
|
+
* command, and the repair here is to write a line.
|
|
143
|
+
*/
|
|
144
|
+
"hook-harness-launch-unruled",
|
|
118
145
|
/** A construct whose effect cannot be read off the text (`bash -c`, `eval`). */
|
|
119
146
|
"hook-opaque",
|
|
120
147
|
/** The command line could not be tokenized at all. */
|
|
@@ -183,9 +210,221 @@ export declare const HOOK_DENY_CODES: readonly [
|
|
|
183
210
|
* merges do not reconcile hash chains (APRV-101).
|
|
184
211
|
*/
|
|
185
212
|
"hook-log-unreachable",
|
|
213
|
+
/**
|
|
214
|
+
* The harness does not tell this hook where the call will run, so no verdict
|
|
215
|
+
* over the visible bytes can bind the action (APRV-311, native evidence in
|
|
216
|
+
* APRV-310 v6/v7).
|
|
217
|
+
*
|
|
218
|
+
* Native Codex 0.152.1 honours a per-call Bash working directory that appears
|
|
219
|
+
* in no field of the event: `tool_input` carries `command` alone, and the
|
|
220
|
+
* event cwd and the hook process cwd both stay at the session root. A
|
|
221
|
+
* decision over `{command, session root}` would therefore authorize different
|
|
222
|
+
* bytes from the `{command, effective directory}` the harness executes, and a
|
|
223
|
+
* relative path in an approved command can name a protected organ in a
|
|
224
|
+
* directory the classifier never saw.
|
|
225
|
+
*
|
|
226
|
+
* Distinct from `hook-io`, which this used to borrow, and the distinction is
|
|
227
|
+
* the repair. `hook-io` says THIS event was malformed and a well-formed one
|
|
228
|
+
* would be answered; this says every event of this shape is refused on this
|
|
229
|
+
* harness version, and the fix is a harness contract that exposes the
|
|
230
|
+
* effective execution directory, not a retry, a policy edit, or an open
|
|
231
|
+
* window. Nothing appends on this path and no gate lifecycle opens.
|
|
232
|
+
*/
|
|
233
|
+
"hook-unsupported-execution-context",
|
|
234
|
+
/**
|
|
235
|
+
* The session names a Contributor-tier model, so every tool call is refused
|
|
236
|
+
* (APRV-350).
|
|
237
|
+
*
|
|
238
|
+
* Meta sells a Contributor variant of the Muse Spark family that "trades a
|
|
239
|
+
* lower price for permission to train on your prompts and completions". A
|
|
240
|
+
* session on one discloses every byte it reads, so the refusal is above the
|
|
241
|
+
* policy: no class resolution and no grant widens it, and an absent or
|
|
242
|
+
* unrecognised `model` is refused for the same reason an unparseable event is.
|
|
243
|
+
*
|
|
244
|
+
* Distinct from `hook-class-human-only`, which says a HUMAN must do this
|
|
245
|
+
* action; this says nothing may do it in this session, and the repair is to
|
|
246
|
+
* change the model in Muse's picker rather than to ask anybody. Distinct from
|
|
247
|
+
* `hook-io` because the event was perfectly well formed.
|
|
248
|
+
*
|
|
249
|
+
* What it cannot do is stated wherever it is documented: it stops tool calls,
|
|
250
|
+
* and it cannot recall a prompt the model has already been sent.
|
|
251
|
+
*/
|
|
252
|
+
"hook-muse-contributor-model",
|
|
186
253
|
/** Malformed hook input, or a log/filesystem fact that stopped the check. */
|
|
187
254
|
"hook-io"];
|
|
188
255
|
export type HookDenyCode = (typeof HOOK_DENY_CODES)[number];
|
|
256
|
+
/** Where the hook reads policy from and appends to, resolved together. */
|
|
257
|
+
export interface HookScope {
|
|
258
|
+
logPath: string;
|
|
259
|
+
/** The directory `logPath` sits under, named in the unreachable-log detail. */
|
|
260
|
+
root: string;
|
|
261
|
+
options: GateOptions;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Policy and log, resolved from the same root (APRV-101).
|
|
265
|
+
*
|
|
266
|
+
* Before this, `--dir` scoped only the policy and the log was resolved from the
|
|
267
|
+
* process cwd, so a hook invoked with `--dir <primary>` from an agent worktree
|
|
268
|
+
* read the primary's policy and wrote the worktree's copy of the log: a
|
|
269
|
+
* dead-end chain that forks from the real one. Explicit flags still win
|
|
270
|
+
* (`--policy` for the policy, `--log` for the log); otherwise both follow
|
|
271
|
+
* `--dir`, and with no flags at all both follow the primary checkout.
|
|
272
|
+
*/
|
|
273
|
+
export declare function hookScope(flags: Record<string, string | boolean>, cwd: string): HookScope;
|
|
274
|
+
/**
|
|
275
|
+
* Which harness JSON envelope to print. Never `ask`.
|
|
276
|
+
*
|
|
277
|
+
* One definition since APRV-227, in `core/harness-version.ts`: the set of
|
|
278
|
+
* harnesses this runtime speaks a protocol for is the same set it knows a
|
|
279
|
+
* binary name for, and two copies of it would be two lists to drift.
|
|
280
|
+
*/
|
|
281
|
+
interface HarnessAdapter {
|
|
282
|
+
kind: HarnessKind;
|
|
283
|
+
originApp: string;
|
|
284
|
+
defaultActor: string;
|
|
285
|
+
shellTool: string;
|
|
286
|
+
fileTools: readonly string[];
|
|
287
|
+
/**
|
|
288
|
+
* Tools that READ a named path (APRV-347).
|
|
289
|
+
*
|
|
290
|
+
* Parallel to `fileTools` and answered by a parallel gate. The two lists
|
|
291
|
+
* differ in what an empty entry means: a file tool with no path is a tool
|
|
292
|
+
* call this runtime does not understand, while a read tool with no path is
|
|
293
|
+
* the ordinary spelling of "read the workspace" and keeps the
|
|
294
|
+
* not-a-gated-tool `allow` it has always had.
|
|
295
|
+
*/
|
|
296
|
+
readTools: readonly string[];
|
|
297
|
+
/** Include the native tool name in the bytes a grant binds. */
|
|
298
|
+
bindToolName?: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* Read `toolName`/`toolInput`/`sessionId` as well as the snake_case
|
|
301
|
+
* spellings (APRV-243).
|
|
302
|
+
*
|
|
303
|
+
* Grok Build's PreToolUse envelope is Claude Code's with camelCase keys.
|
|
304
|
+
* Opt-in per adapter rather than tolerated everywhere: a Claude Code event
|
|
305
|
+
* that arrived with the wrong spelling is a malformed event, and the strict
|
|
306
|
+
* answer to a malformed event is the deny that `parseHookInput` already
|
|
307
|
+
* produces.
|
|
308
|
+
*/
|
|
309
|
+
camelCaseEnvelope?: boolean;
|
|
310
|
+
/**
|
|
311
|
+
* Tools the harness fires for its OWN bookkeeping, answered and never gated
|
|
312
|
+
* (APRV-350).
|
|
313
|
+
*
|
|
314
|
+
* Muse Code fires `PreToolUse` and `PostToolUse` for `submit_reminder_decision`
|
|
315
|
+
* continuously: 100 of the 139 events in the live capture were that one tool.
|
|
316
|
+
* It records a self-assessment and touches nothing, so gating it would put a
|
|
317
|
+
* hundred questions a turn on an approver's phone to authorize the harness
|
|
318
|
+
* thinking. It is listed rather than inferred, because a tool this runtime
|
|
319
|
+
* does not recognise must keep falling through to the ordinary path.
|
|
320
|
+
*/
|
|
321
|
+
passThroughTools?: readonly string[];
|
|
322
|
+
/**
|
|
323
|
+
* The `tool_input` key carrying the PER-CALL working directory, when the
|
|
324
|
+
* harness sends one (APRV-350).
|
|
325
|
+
*
|
|
326
|
+
* Muse's `bash` tool carries `workdir`, and it is the directory the command
|
|
327
|
+
* will actually run in, which is the fact the classifier needs. The top-level
|
|
328
|
+
* `cwd` is the session's root and can differ. Codex has neither, which is why
|
|
329
|
+
* its shell arm refuses outright; Claude Code has only the top-level one.
|
|
330
|
+
*/
|
|
331
|
+
shellCwdKey?: string;
|
|
332
|
+
/**
|
|
333
|
+
* Refuse every tool call when the envelope names a Contributor-tier model
|
|
334
|
+
* (APRV-350).
|
|
335
|
+
*
|
|
336
|
+
* Meta sells a Contributor variant that "trades a lower price for permission
|
|
337
|
+
* to train on your prompts and completions". A session on one is a session
|
|
338
|
+
* whose every read is disclosed, so the adapter refuses regardless of what
|
|
339
|
+
* the policy would otherwise allow. See {@link contributorModelRefusal}.
|
|
340
|
+
*/
|
|
341
|
+
contributorModelGuard?: boolean;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Every harness this runtime speaks a hook protocol for, by kind (APRV-358).
|
|
345
|
+
*
|
|
346
|
+
* The table is `Record<HarnessKind, HarnessAdapter>` rather than a list of
|
|
347
|
+
* consts and a switch, and the type is the point: a kind added to
|
|
348
|
+
* `HARNESS_KINDS` with no adapter beside it fails to compile, so the two lists
|
|
349
|
+
* cannot drift by forgetting. The subcommand dispatch below reads this map, so
|
|
350
|
+
* `approval hook <kind>` is answerable for exactly the kinds named here.
|
|
351
|
+
*
|
|
352
|
+
* The kinds that are enumerated OUTSIDE this module — the schema's
|
|
353
|
+
* `payload.harness` enum, the verb registry's `hook` subcommands, the MCP
|
|
354
|
+
* exclusions, the help — are pinned set-equal to `HARNESS_KINDS` by
|
|
355
|
+
* `tests/harness-enum.test.ts`, which exists because `grok` shipped an adapter
|
|
356
|
+
* in APRV-243 and reached none of them. A Grok session's manual-class
|
|
357
|
+
* registration was refused at the write boundary for eleven days and nothing
|
|
358
|
+
* failed.
|
|
359
|
+
*/
|
|
360
|
+
export declare const HARNESS_ADAPTERS: Readonly<Record<HarnessKind, HarnessAdapter>>;
|
|
361
|
+
/** The machine-readable code a Contributor-tier session is refused with. */
|
|
362
|
+
export declare const MUSE_CONTRIBUTOR_REFUSAL = "hook-muse-contributor-model";
|
|
363
|
+
export interface HookInput {
|
|
364
|
+
sessionId: string;
|
|
365
|
+
/** Whether the event supplied the session id, distinct from the strict unknown bucket. */
|
|
366
|
+
sessionIdPresent: boolean;
|
|
367
|
+
cwd: string;
|
|
368
|
+
toolName: string;
|
|
369
|
+
toolInput: Record<string, unknown>;
|
|
370
|
+
toolUseId: string | null;
|
|
371
|
+
/**
|
|
372
|
+
* `hook_event_name`, verbatim, or `null` when the event carries none
|
|
373
|
+
* (APRV-145).
|
|
374
|
+
*
|
|
375
|
+
* Read at last. Until this, nothing in this module looked at it and
|
|
376
|
+
* `runHarnessHook` assumed a pre-execution event unconditionally, so an
|
|
377
|
+
* operator who registered this same command for the post-execution event would
|
|
378
|
+
* have gated every command a second time and doubled every prompt on the
|
|
379
|
+
* approver's phone.
|
|
380
|
+
*/
|
|
381
|
+
hookEventName: string | null;
|
|
382
|
+
/**
|
|
383
|
+
* The model the session reports running, or `null` (APRV-350).
|
|
384
|
+
*
|
|
385
|
+
* Muse sends it on every event. Read for one purpose only, the contributor
|
|
386
|
+
* guard, and that guard can only ever refuse: a self-reported field raises
|
|
387
|
+
* scrutiny and never lowers it (SPEC §11.1). It is never logged, because it
|
|
388
|
+
* is untrusted third-party text and §11.1 invariant 3 has no provenance
|
|
389
|
+
* exception.
|
|
390
|
+
*/
|
|
391
|
+
model: string | null;
|
|
392
|
+
/**
|
|
393
|
+
* `tool_response`, when the event carries one as an object.
|
|
394
|
+
*
|
|
395
|
+
* Present only on a post-execution event; the pre-execution path never reads
|
|
396
|
+
* it, because the tool has not run. Its SHAPE is all that is ever read (see
|
|
397
|
+
* {@link readReportedOutcome}) — never the text inside it.
|
|
398
|
+
*/
|
|
399
|
+
toolResponse: Record<string, unknown> | null;
|
|
400
|
+
/** `tool_response` verbatim, including strings, for harness-specific readers. */
|
|
401
|
+
toolResponseRaw: unknown;
|
|
402
|
+
/**
|
|
403
|
+
* `is_interrupt`, the post-execution events' own word for "a person stopped
|
|
404
|
+
* this" (APRV-303).
|
|
405
|
+
*
|
|
406
|
+
* `PostToolUseFailure` carries it beside `error`; `PostToolUse` carries the
|
|
407
|
+
* same fact as `tool_response.interrupted`. Read only to make an outcome
|
|
408
|
+
* UNREADABLE, never to establish one, so nothing about it can lower scrutiny.
|
|
409
|
+
*/
|
|
410
|
+
interrupted: boolean;
|
|
411
|
+
/**
|
|
412
|
+
* `version`, when the harness states its own (APRV-227).
|
|
413
|
+
*
|
|
414
|
+
* Claude Code's event may carry it; Cursor's does not, and neither did any
|
|
415
|
+
* Claude Code release before it. So this is a preference and never a
|
|
416
|
+
* requirement: `core/harness-version.ts` falls back to `<binary> --version`
|
|
417
|
+
* and then to absence, and a hook that can establish nothing records nothing.
|
|
418
|
+
*
|
|
419
|
+
* SELF-REPORTED, and read at all only because it cannot buy the reporter
|
|
420
|
+
* anything. Nothing in this module branches on it; it reaches exactly one
|
|
421
|
+
* payload field whose one reader is a doctor row that can only ADD a red
|
|
422
|
+
* line, so §11.1 invariant 4 holds by construction rather than by care. A
|
|
423
|
+
* harness that states a false version defeats a check that would have asked a
|
|
424
|
+
* human to look, and gains no verdict it did not already have.
|
|
425
|
+
*/
|
|
426
|
+
harnessVersion: string | null;
|
|
427
|
+
}
|
|
189
428
|
/** A classification plus a human-readable note for every segment refined. */
|
|
190
429
|
export interface RefinedClassification {
|
|
191
430
|
result: CommandClassification;
|
|
@@ -240,14 +479,41 @@ export declare function resolveScratchRoots(cwd: string, env?: NodeJS.ProcessEnv
|
|
|
240
479
|
*/
|
|
241
480
|
export declare function refineScratchDelete(result: CommandClassification, roots: readonly string[]): RefinedClassification;
|
|
242
481
|
/**
|
|
243
|
-
* The
|
|
482
|
+
* The read roots this process may vouch for, resolved.
|
|
483
|
+
*
|
|
484
|
+
* The gate root is the directory the hook resolved its POLICY from, never the
|
|
485
|
+
* harness-supplied `cwd`: a scope the subject of the gate could choose is not a
|
|
486
|
+
* scope (SPEC.md §11.1, self-reported fields never reduce scrutiny). The
|
|
487
|
+
* scratchpad and temp roots are the ones `resolveScratchRoots` already computes
|
|
488
|
+
* and already guards, so the two rules cannot disagree about where the agent's
|
|
489
|
+
* own scratch is. `declared` is `read_scope.roots` out of the loaded policy,
|
|
490
|
+
* which may only widen this set.
|
|
491
|
+
*/
|
|
492
|
+
export declare function resolveReadRoots(cwd: string, gateRoot: string, declared?: readonly string[]): string[];
|
|
493
|
+
/**
|
|
494
|
+
* Tighten a `read.shell` segment to `read.file.out_of_scope` wherever the disk
|
|
495
|
+
* disagrees with the text.
|
|
496
|
+
*
|
|
497
|
+
* IMPURE by design and by contract. `roots` empty means the caller asked for no
|
|
498
|
+
* read scoping at all, and every segment is returned untouched — the same
|
|
499
|
+
* "absent yields today's answer" the classifier context promises.
|
|
500
|
+
*/
|
|
501
|
+
export declare function refineReadScope(result: CommandClassification, roots: readonly string[], cwd: string): RefinedClassification;
|
|
502
|
+
/**
|
|
503
|
+
* The classifier, its context, and all three impure refinements, in the one
|
|
244
504
|
* order every caller must use.
|
|
245
505
|
*
|
|
246
506
|
* `hook classify` printing a different class from the one `hook claude-code`
|
|
247
507
|
* decides would make the explainer a different program (APRV-108's note), and
|
|
248
|
-
* that stays true now there are
|
|
508
|
+
* that stays true now there are three refinements in the chain.
|
|
509
|
+
*
|
|
510
|
+
* `readRoots` is the one argument whose ABSENCE is the loose answer rather than
|
|
511
|
+
* the strict one (APRV-347), so it is passed explicitly at every call site: an
|
|
512
|
+
* empty list means "do not scope reads", which is what every caller outside a
|
|
513
|
+
* resolved gate scope wants and what this classifier did before the field
|
|
514
|
+
* existed.
|
|
249
515
|
*/
|
|
250
|
-
export declare function classifyForHook(command: string, protectedPaths: readonly ProtectedPathEntry[], cwd: string): RefinedClassification;
|
|
516
|
+
export declare function classifyForHook(command: string, protectedPaths: readonly ProtectedPathEntry[], cwd: string, readRoots?: readonly string[]): RefinedClassification;
|
|
251
517
|
/**
|
|
252
518
|
* What the classifier made of a command (APRV-91 #9).
|
|
253
519
|
*
|
|
@@ -257,6 +523,145 @@ export declare function classifyForHook(command: string, protectedPaths: readonl
|
|
|
257
523
|
* that the `json` veto on colour is the answer this process memoizes.
|
|
258
524
|
*/
|
|
259
525
|
export declare function renderClassification(result: CommandClassification, json: boolean, st?: Style): string;
|
|
526
|
+
interface HookRun {
|
|
527
|
+
logPath: string;
|
|
528
|
+
options: GateOptions;
|
|
529
|
+
actor: string;
|
|
530
|
+
timeoutMs: number;
|
|
531
|
+
intervalMs: number;
|
|
532
|
+
/**
|
|
533
|
+
* How long a request outlives the wait before this hook takes it back
|
|
534
|
+
* (APRV-287, `--retry-grace`).
|
|
535
|
+
*
|
|
536
|
+
* `core/harness-wait.ts` holds the default and the reasoning. Zero withdraws
|
|
537
|
+
* at the moment the wait expires, which is what the tests drive.
|
|
538
|
+
*/
|
|
539
|
+
graceMs: number;
|
|
540
|
+
/** `defaults.approval_ttl`, or `null` when the policy declares none. */
|
|
541
|
+
ttlMs: number | null;
|
|
542
|
+
harness: HarnessKind;
|
|
543
|
+
originApp: string;
|
|
544
|
+
/** Exact native command bytes required in a Codex allow's identity update. */
|
|
545
|
+
codexCommand?: string;
|
|
546
|
+
/**
|
|
547
|
+
* The version the hook event stated, or `null` (APRV-227).
|
|
548
|
+
*
|
|
549
|
+
* Carried rather than resolved here: resolving it means a `spawnSync` of
|
|
550
|
+
* `<binary> --version`, and a hook process exists per gated tool call. The
|
|
551
|
+
* resolution happens at the one place that is about to WRITE a record and
|
|
552
|
+
* nowhere else, so the pass-through verdict and the autonomous verdict pay
|
|
553
|
+
* nothing for it. See {@link registrationProvenance}.
|
|
554
|
+
*/
|
|
555
|
+
eventVersion: string | null;
|
|
556
|
+
/**
|
|
557
|
+
* The channel names this policy configures, sorted (APRV-281).
|
|
558
|
+
*
|
|
559
|
+
* Read off the policy the caller already loaded, and used for ONE thing: the
|
|
560
|
+
* line this hook prints when it appends a request, so the agent and the
|
|
561
|
+
* operator watching its error stream are told where the question went. It
|
|
562
|
+
* resolves nothing and reaches no verdict. An empty list is a fact worth
|
|
563
|
+
* printing rather than a default to fill in: a request under a policy that
|
|
564
|
+
* configures no channel is a question nothing is delivering.
|
|
565
|
+
*/
|
|
566
|
+
channels: readonly string[];
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* The gated half: find what is already open for these bytes, request whatever
|
|
570
|
+
* is not, wait for the decisions, spend the grants. Returns the exit code of
|
|
571
|
+
* whatever verdict it printed.
|
|
572
|
+
*
|
|
573
|
+
* ## Requests are keyed by bytes, not by invocation (APRV-117)
|
|
574
|
+
*
|
|
575
|
+
* The action key is still `hook:<session>:<tool-use id>:<class>` and is still
|
|
576
|
+
* unique per invocation — what changed is that intake LOOKS for an earlier
|
|
577
|
+
* request about the same `{command, cwd}` before opening a new one, matching on
|
|
578
|
+
* the `payload_hash` recorded on `approval.requested`. Three outcomes per class,
|
|
579
|
+
* decided by `core/gate.ts`'s `findHarnessCarry`:
|
|
580
|
+
*
|
|
581
|
+
* - nothing to carry: register and request, exactly as before;
|
|
582
|
+
* - a pending request: **adopt** it — wait out the remainder of this
|
|
583
|
+
* invocation's window on somebody else's key, opening nothing. The approver's
|
|
584
|
+
* phone never shows two prompts for one command, because there is only ever
|
|
585
|
+
* one question;
|
|
586
|
+
* - an unspent grant inside the TTL: **carry** it — no wait, no prompt, and
|
|
587
|
+
* the grant is spent (once) before the allow is printed.
|
|
588
|
+
*
|
|
589
|
+
* ## Why the wait no longer ends in a withdrawal (APRV-106, revised)
|
|
590
|
+
*
|
|
591
|
+
* APRV-106 retracted the request when the wait elapsed, because a retried tool
|
|
592
|
+
* call was a new request with a new key and a late tap therefore authorized
|
|
593
|
+
* nothing: the human spent attention on a question whose asker had left. The
|
|
594
|
+
* carryover above removes the premise. A late tap now authorizes the retry, so
|
|
595
|
+
* the request stays open for the policy's TTL and the timeout says so.
|
|
596
|
+
*
|
|
597
|
+
* What still withdraws is every path where nothing can adopt the question: a
|
|
598
|
+
* SIGTERM or SIGINT (the session is going away), a thrown failure, and an intake
|
|
599
|
+
* refusal partway through a multi-class command (the command cannot proceed on
|
|
600
|
+
* any retry, so the classes already opened are noise in a human's queue). The
|
|
601
|
+
* signal handlers are installed for the duration of the wait ONLY, and removed
|
|
602
|
+
* in `finally`: a hook process is short-lived and borrowing the harness's
|
|
603
|
+
* signal disposition for longer than the loop would be a side effect nobody
|
|
604
|
+
* asked for.
|
|
605
|
+
*/
|
|
606
|
+
/**
|
|
607
|
+
* What the gate decided about one harness tool call, before anything is printed
|
|
608
|
+
* (APRV-361).
|
|
609
|
+
*
|
|
610
|
+
* {@link gateHarnessCall} produces it and {@link gateAndWait} renders it in the
|
|
611
|
+
* harness's own dialect. The split exists because a second caller answers in a
|
|
612
|
+
* protocol rather than on stdout: `cli/codex-bridge.ts` replies
|
|
613
|
+
* `{id, result: {decision}}` over the app-server's JSON-RPC connection, and it
|
|
614
|
+
* has to reach that decision through the SAME classify, register, request and
|
|
615
|
+
* wait this function runs. Two implementations of that sequence would be two
|
|
616
|
+
* gates, and the second one would be the one nobody reviewed.
|
|
617
|
+
*
|
|
618
|
+
* `code` and `detail` are kept apart rather than pre-joined, because the bridge
|
|
619
|
+
* records the code as a code (§11.1 invariant 7) where the hook prints the pair
|
|
620
|
+
* as one reason string.
|
|
621
|
+
*/
|
|
622
|
+
export type HarnessVerdict = {
|
|
623
|
+
permission: "allow";
|
|
624
|
+
reason: string;
|
|
625
|
+
} | {
|
|
626
|
+
permission: "deny";
|
|
627
|
+
code: string;
|
|
628
|
+
detail: string;
|
|
629
|
+
};
|
|
630
|
+
export declare function gateHarnessCall(streams: Streams, run: HookRun, classes: string[],
|
|
631
|
+
/**
|
|
632
|
+
* The bytes the grant binds to: `{command, cwd}` for a Bash call, the change
|
|
633
|
+
* itself for a file tool (APRV-124). Whatever this is, it is what reaches the
|
|
634
|
+
* approver's FULL PAYLOAD block, complete — the summary below is a headline
|
|
635
|
+
* and is the only thing here that may be shortened.
|
|
636
|
+
*/
|
|
637
|
+
payload: unknown, headline: string,
|
|
638
|
+
/**
|
|
639
|
+
* The task id this invocation acts under, minted once by the caller
|
|
640
|
+
* (APRV-139) so the loop-escalation check and the registration it may lead to
|
|
641
|
+
* name the same task. Deriving it twice would mint two ids whenever
|
|
642
|
+
* `tool_use_id` is absent and the random fallback runs.
|
|
643
|
+
*/
|
|
644
|
+
task: string,
|
|
645
|
+
/** The history-rewrite refinement's own words, or `""` (APRV-108). */
|
|
646
|
+
note?: string,
|
|
647
|
+
/**
|
|
648
|
+
* The harness streak that floors the SIDE-EFFECTING classes of this
|
|
649
|
+
* invocation to `manual` (APRV-145, narrowed by APRV-297), or `null` where
|
|
650
|
+
* policy alone sent it here.
|
|
651
|
+
*
|
|
652
|
+
* Passed into `request` as a boolean rather than acted on here, so the floored
|
|
653
|
+
* action takes the identical path a manual class takes — same records, same
|
|
654
|
+
* order, same wait — and nothing below knows how it got there. What the STATE
|
|
655
|
+
* adds (APRV-280) is the deny text: an agent whose commands are all suddenly
|
|
656
|
+
* on the phone is owed the reason and the way out in the same breath, and
|
|
657
|
+
* before APRV-280 the nine-minute wait ended in a bare `hook-timeout` that
|
|
658
|
+
* said neither.
|
|
659
|
+
*
|
|
660
|
+
* Since APRV-297 the caller passes `null` for a command whose classes are all
|
|
661
|
+
* reads, and {@link floorApplies} below carves the read classes out of a mixed
|
|
662
|
+
* one, so a floor never puts a question about looking on a human's phone.
|
|
663
|
+
*/
|
|
664
|
+
floor?: HarnessLoopState | null): HarnessVerdict;
|
|
260
665
|
/**
|
|
261
666
|
* Every line the counterpart can print, closed and machine-readable (§11.1
|
|
262
667
|
* invariant 7).
|
|
@@ -328,4 +733,55 @@ export declare const REQUIRE_SANDBOX_ENV = "APPROVAL_HOOK_REQUIRE_SANDBOX";
|
|
|
328
733
|
* invariant 4).
|
|
329
734
|
*/
|
|
330
735
|
export declare function sandboxRequirement(segments: readonly ClassifiedSegment[] | undefined, autonomies: readonly string[], env?: NodeJS.ProcessEnv): string | null;
|
|
736
|
+
/**
|
|
737
|
+
* What {@link decideHarnessCall} needs to reach a verdict (APRV-361).
|
|
738
|
+
*
|
|
739
|
+
* Everything here is established by the CALLER: which harness asked, where the
|
|
740
|
+
* policy and the log are, who is acting, and how long this decision may take.
|
|
741
|
+
* Nothing in it is authored by the party under oversight beyond `input`, which
|
|
742
|
+
* is the harness's own event and is treated as such throughout.
|
|
743
|
+
*/
|
|
744
|
+
export interface DecideInput {
|
|
745
|
+
streams: Streams;
|
|
746
|
+
input: HookInput;
|
|
747
|
+
adapter: HarnessAdapter;
|
|
748
|
+
/** The directory a relative path in the call resolves against. */
|
|
749
|
+
cwd: string;
|
|
750
|
+
logPath: string;
|
|
751
|
+
/** The scope root, named in the unreachable-log detail. */
|
|
752
|
+
root: string;
|
|
753
|
+
options: GateOptions;
|
|
754
|
+
actor: string;
|
|
755
|
+
timeoutMs: number;
|
|
756
|
+
intervalMs: number;
|
|
757
|
+
graceMs: number;
|
|
758
|
+
/** Exact native command bytes a Codex allow must carry back, where there are any. */
|
|
759
|
+
codexCommand?: string | undefined;
|
|
760
|
+
/**
|
|
761
|
+
* The verified records an open-window lookup already read, or `null`.
|
|
762
|
+
*
|
|
763
|
+
* Passed rather than re-read so the floor and the unattended guard are
|
|
764
|
+
* decided from the same read the window was. A caller that performed no
|
|
765
|
+
* lookup passes `null`, and both of them read the log themselves.
|
|
766
|
+
*/
|
|
767
|
+
windowRecords: EventRecord[] | null;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Classify, resolve, gate and wait: one harness tool call, from the event to a
|
|
771
|
+
* verdict (APRV-361).
|
|
772
|
+
*
|
|
773
|
+
* Extracted from the hook's own verb so a SECOND caller can reach a decision
|
|
774
|
+
* through exactly this sequence. `cli/codex-bridge.ts` answers Codex's
|
|
775
|
+
* app-server approval requests over JSON-RPC rather than on stdout, and the
|
|
776
|
+
* thing it must not do is re-implement any of what is below: the human-only
|
|
777
|
+
* refusal, the unruled `harness.launch.*` refusal, the sandbox requirement, the
|
|
778
|
+
* loop floor, the unattended guard, the autonomous charge, and the register,
|
|
779
|
+
* request and wait that follow. Two implementations of that sequence would be
|
|
780
|
+
* two gates, and the second one would be the one nobody reviewed.
|
|
781
|
+
*
|
|
782
|
+
* It returns a verdict and prints none. `streams.err` still carries the
|
|
783
|
+
* progress and withdrawal lines, which are a report rather than a decision.
|
|
784
|
+
*/
|
|
785
|
+
export declare function decideHarnessCall(decide: DecideInput): HarnessVerdict;
|
|
331
786
|
export declare function commandHook(argv: string[], streams: Streams, cwd: string, readStdin?: () => string): number;
|
|
787
|
+
export {};
|