approval-md 0.1.0 → 0.2.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 +584 -553
- package/SPEC.md +42 -13
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +119 -5
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +173 -51
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +186 -80
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +155 -5
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +294 -7
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +4 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +176 -8
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +13 -1
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +43 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +44 -6
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +68 -13
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +2 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +31 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +15 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +20 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +514 -35
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +9 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +389 -36
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +12 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +2 -1
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/policy.schema.json +21 -1
- package/templates/codex/README.md +9 -0
package/dist/src/cli/hook.js
CHANGED
|
@@ -86,6 +86,7 @@ import { abandonedAfterMs, HOOK_DEFAULT_WAIT, HOOK_RETRY_GRACE_MS, } from "../co
|
|
|
86
86
|
import { harnessLoopFloor, isLoopEscalated, isSideEffectingClass, loopClearance, UNKNOWN_SESSION, } from "../core/loop.js";
|
|
87
87
|
import { drawSocketPathFor, drawSocketUsable } from "../core/live-draw.js";
|
|
88
88
|
import { payloadHash } from "../core/payload.js";
|
|
89
|
+
import { classifyApplyPatch, parseApplyPatch } from "../core/apply-patch.js";
|
|
89
90
|
import { loadPolicy, parseDuration } from "../core/policy-load.js";
|
|
90
91
|
import { humanOnlyRefusal, resolve as resolvePolicy } from "../core/policy-match.js";
|
|
91
92
|
import { payloadOf, readVerifiedRecords, requestState, useVerifiedSnapshots, } from "../core/state.js";
|
|
@@ -96,6 +97,7 @@ import { HOOK_HELP } from "./help.js";
|
|
|
96
97
|
import { DEFAULT_LOG_PATH } from "./paths.js";
|
|
97
98
|
import { refusal as renderRefusal, style, table } from "./style.js";
|
|
98
99
|
import { usageErrorText } from "./usage.js";
|
|
100
|
+
import { checkCodexHookInput, codexBinding, CODEX_POST_TOOL_EVENT, readCodexReportedOutcome, } from "./hook-codex.js";
|
|
99
101
|
/** Identity accepted for the proposing side: a person or an agent. */
|
|
100
102
|
const PRINCIPAL_ACTOR = /^(human|agent):.+/u;
|
|
101
103
|
/**
|
|
@@ -290,6 +292,14 @@ const CURSOR_ADAPTER = {
|
|
|
290
292
|
shellTool: "Shell",
|
|
291
293
|
fileTools: ["Write", "Delete"],
|
|
292
294
|
};
|
|
295
|
+
const CODEX_ADAPTER = {
|
|
296
|
+
kind: "codex",
|
|
297
|
+
originApp: "codex-hook",
|
|
298
|
+
defaultActor: "agent:codex",
|
|
299
|
+
shellTool: "Bash",
|
|
300
|
+
fileTools: ["apply_patch"],
|
|
301
|
+
bindToolName: true,
|
|
302
|
+
};
|
|
293
303
|
/**
|
|
294
304
|
* The decision object the harness reads from stdout.
|
|
295
305
|
*
|
|
@@ -297,7 +307,7 @@ const CURSOR_ADAPTER = {
|
|
|
297
307
|
* `{permission, user_message, agent_message}`. One construction site per
|
|
298
308
|
* harness, still never `ask`.
|
|
299
309
|
*/
|
|
300
|
-
function decision(permission, reason, harness) {
|
|
310
|
+
function decision(permission, reason, harness, codexCommand) {
|
|
301
311
|
if (harness === "cursor") {
|
|
302
312
|
return `${JSON.stringify({
|
|
303
313
|
permission,
|
|
@@ -305,16 +315,22 @@ function decision(permission, reason, harness) {
|
|
|
305
315
|
agent_message: reason,
|
|
306
316
|
})}\n`;
|
|
307
317
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
318
|
+
const hookSpecificOutput = {
|
|
319
|
+
hookEventName: "PreToolUse",
|
|
320
|
+
permissionDecision: permission,
|
|
321
|
+
permissionDecisionReason: reason,
|
|
322
|
+
};
|
|
323
|
+
if (harness === "codex" && permission === "allow") {
|
|
324
|
+
if (codexCommand !== undefined)
|
|
325
|
+
hookSpecificOutput["updatedInput"] = { command: codexCommand };
|
|
326
|
+
}
|
|
327
|
+
return `${JSON.stringify({ hookSpecificOutput })}\n`;
|
|
315
328
|
}
|
|
316
|
-
function allow(streams, reason, harness) {
|
|
317
|
-
|
|
329
|
+
function allow(streams, reason, harness, codexCommand) {
|
|
330
|
+
if (harness === "codex" && codexCommand === undefined) {
|
|
331
|
+
return deny(streams, "hook-io", "the Codex allow lost its exact bound tool_input.command", harness);
|
|
332
|
+
}
|
|
333
|
+
streams.out(decision("allow", reason, harness, codexCommand));
|
|
318
334
|
return EXIT_OK;
|
|
319
335
|
}
|
|
320
336
|
function deny(streams, code, detail, harness) {
|
|
@@ -359,13 +375,15 @@ function parseHookInput(raw) {
|
|
|
359
375
|
? toolInputValue
|
|
360
376
|
: {};
|
|
361
377
|
const responseValue = fields["tool_response"];
|
|
378
|
+
const sessionId = readString(fields, "session_id");
|
|
362
379
|
return {
|
|
363
380
|
ok: true,
|
|
364
381
|
input: {
|
|
365
382
|
// The ONE shared bucket for an unreadable session (`core/loop.ts`'s
|
|
366
383
|
// `UNKNOWN_SESSION`): absence accrues faster than a readable id and never
|
|
367
384
|
// slower, which is the fail-closed direction.
|
|
368
|
-
sessionId:
|
|
385
|
+
sessionId: sessionId ?? UNKNOWN_SESSION,
|
|
386
|
+
sessionIdPresent: sessionId !== null,
|
|
369
387
|
cwd: readString(fields, "cwd") ?? "",
|
|
370
388
|
toolName,
|
|
371
389
|
toolInput,
|
|
@@ -376,6 +394,7 @@ function parseHookInput(raw) {
|
|
|
376
394
|
toolResponse: typeof responseValue === "object" && responseValue !== null && !Array.isArray(responseValue)
|
|
377
395
|
? responseValue
|
|
378
396
|
: null,
|
|
397
|
+
toolResponseRaw: responseValue,
|
|
379
398
|
},
|
|
380
399
|
};
|
|
381
400
|
}
|
|
@@ -1037,13 +1056,12 @@ const WORKSPACE_WRITE_CLASS = "files.write.workspace";
|
|
|
1037
1056
|
/**
|
|
1038
1057
|
* What a non-Bash tool call asks for, or `null` when it names no file at all.
|
|
1039
1058
|
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1044
|
-
* latency to reach a foregone conclusion.
|
|
1059
|
+
* Every file edit is a gate question. Protected targets take their derived
|
|
1060
|
+
* protected class; every other target is `files.write.workspace`. The policy
|
|
1061
|
+
* decides the autonomy of either class, so changing the ordinary-file rule to
|
|
1062
|
+
* manual, supervised or human-only changes the hook verdict too (APRV-304).
|
|
1045
1063
|
*
|
|
1046
|
-
* ## The ordinary edit
|
|
1064
|
+
* ## The ordinary edit gets a class and follows it (APRV-303, APRV-304)
|
|
1047
1065
|
*
|
|
1048
1066
|
* It used to get none: an unprotected target returned `null` here, and
|
|
1049
1067
|
* `describeToolCall` answered `allow` from a branch that sits ABOVE the loop
|
|
@@ -1053,12 +1071,10 @@ const WORKSPACE_WRITE_CLASS = "files.write.workspace";
|
|
|
1053
1071
|
* eight edits to the same file, under a standing floor, none of them routed and
|
|
1054
1072
|
* none of them counted.
|
|
1055
1073
|
*
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
1058
|
-
*
|
|
1059
|
-
*
|
|
1060
|
-
* establishment. The floor predicate is now one predicate over one class for
|
|
1061
|
-
* every tool kind, which is what amended SPEC.md §10.2 asks for.
|
|
1074
|
+
* APRV-303 made the floor predicate one predicate over one class for every tool
|
|
1075
|
+
* kind. APRV-304 carries the same class through the rest of the shared path:
|
|
1076
|
+
* policy resolution, human-only refusal, budgets, registration and execution
|
|
1077
|
+
* accounting. An open window records the bypass before allowing the edit.
|
|
1062
1078
|
*
|
|
1063
1079
|
* ## The payload is the change (APRV-124)
|
|
1064
1080
|
*
|
|
@@ -1639,7 +1655,7 @@ floor = null) {
|
|
|
1639
1655
|
const floorApplies = (cls) => floor !== null && isSideEffectingClass(cls);
|
|
1640
1656
|
const hash = payloadHash(payload);
|
|
1641
1657
|
const summary = truncate(headline, SUMMARY_LIMIT);
|
|
1642
|
-
const sayAllow = (reason) => allow(streams, reason, run.harness);
|
|
1658
|
+
const sayAllow = (reason) => allow(streams, reason, run.harness, run.codexCommand);
|
|
1643
1659
|
/**
|
|
1644
1660
|
* Every deny this function can print, with the floor's own sentence appended
|
|
1645
1661
|
* when a floor is what routed the command here (APRV-280). One wrapper rather
|
|
@@ -2077,7 +2093,9 @@ function report(streams, code, detail, extra = {}) {
|
|
|
2077
2093
|
* whether two enumerated fields are present and what kind of value they hold.
|
|
2078
2094
|
* No text from any of them reaches the log or this function's return.
|
|
2079
2095
|
*/
|
|
2080
|
-
function readReportedOutcome(input) {
|
|
2096
|
+
function readReportedOutcome(input, adapter) {
|
|
2097
|
+
if (adapter.kind === "codex")
|
|
2098
|
+
return readCodexReportedOutcome(input);
|
|
2081
2099
|
const event = input.hookEventName;
|
|
2082
2100
|
if (event !== "PostToolUse" && event !== "PostToolUseFailure") {
|
|
2083
2101
|
return {
|
|
@@ -2124,7 +2142,7 @@ function runPostToolUse(flags, streams, cwd, input, actor, adapter) {
|
|
|
2124
2142
|
// `approval status` rather than closed against a guess.
|
|
2125
2143
|
return report(streams, "post-tool-unidentified", "the event carries no tool_use_id, so the task id the pre-execution hook minted cannot be reconstructed; nothing was appended");
|
|
2126
2144
|
}
|
|
2127
|
-
const reading = readReportedOutcome(input);
|
|
2145
|
+
const reading = readReportedOutcome(input, adapter);
|
|
2128
2146
|
if (!reading.ok) {
|
|
2129
2147
|
return report(streams, "post-tool-unreadable-outcome", `${reading.detail}; nothing was appended`);
|
|
2130
2148
|
}
|
|
@@ -2133,8 +2151,8 @@ function runPostToolUse(flags, streams, cwd, input, actor, adapter) {
|
|
|
2133
2151
|
return report(streams, "post-tool-log-unreachable", `no log at ${logPath}; the hook writes to an existing log and never creates one. Run \`approval init\` in ${root}`);
|
|
2134
2152
|
}
|
|
2135
2153
|
const finished = finishHarnessExecution(logPath, {
|
|
2136
|
-
sessionId: input.sessionId,
|
|
2137
|
-
toolUseId: input.toolUseId,
|
|
2154
|
+
sessionId: adapter.kind === "codex" ? codexBinding(input, cwd).finishSessionId : input.sessionId,
|
|
2155
|
+
toolUseId: adapter.kind === "codex" ? codexBinding(input, cwd).finishToolUseId : input.toolUseId,
|
|
2138
2156
|
outcome: reading.outcome,
|
|
2139
2157
|
// The one member of the closed set at v0.1. It names the untrusted
|
|
2140
2158
|
// reporter and reduces nothing.
|
|
@@ -2146,6 +2164,25 @@ function runPostToolUse(flags, streams, cwd, input, actor, adapter) {
|
|
|
2146
2164
|
return report(streams, POST_TOOL_REPORTED, `recorded ${reading.outcome} for ${String(finished.records.length)} delegated execution(s) of ${finished.task}`, { task: finished.task, outcome: reading.outcome, appended: finished.records.length });
|
|
2147
2165
|
}
|
|
2148
2166
|
function describeToolCall(input, adapter, protectedPaths, cwd) {
|
|
2167
|
+
if (adapter.kind === "codex" && input.toolName === "apply_patch") {
|
|
2168
|
+
const raw = readString(input.toolInput, "command");
|
|
2169
|
+
if (raw === null) {
|
|
2170
|
+
return { kind: "deny", code: "hook-io", detail: "apply_patch tool_input carries no command string" };
|
|
2171
|
+
}
|
|
2172
|
+
const parsed = parseApplyPatch(raw);
|
|
2173
|
+
if (!parsed.ok)
|
|
2174
|
+
return { kind: "deny", code: "hook-io", detail: parsed.detail };
|
|
2175
|
+
const classified = classifyApplyPatch(parsed, cwd, protectedPaths);
|
|
2176
|
+
if (!classified.ok)
|
|
2177
|
+
return { kind: "deny", code: "hook-io", detail: classified.detail };
|
|
2178
|
+
return {
|
|
2179
|
+
kind: "gated",
|
|
2180
|
+
classes: classified.classes,
|
|
2181
|
+
payload: codexBinding(input, cwd).payload,
|
|
2182
|
+
headline: `apply_patch ${classified.operations.length} operation(s)`,
|
|
2183
|
+
notes: classified.targets.map((target) => `${target.role} ${target.path} (${target.classes.join(", ")})`),
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2149
2186
|
if (input.toolName === adapter.shellTool) {
|
|
2150
2187
|
const raw = readString(input.toolInput, "command");
|
|
2151
2188
|
if (raw === null) {
|
|
@@ -2158,7 +2195,9 @@ function describeToolCall(input, adapter, protectedPaths, cwd) {
|
|
|
2158
2195
|
// Unchanged since APRV-117, deliberately: the payload is the WHOLE command
|
|
2159
2196
|
// and the directory it runs in, so the FULL PAYLOAD block on the phone
|
|
2160
2197
|
// carries every byte the harness will execute. Only `summary` is shortened.
|
|
2161
|
-
const payload =
|
|
2198
|
+
const payload = adapter.bindToolName
|
|
2199
|
+
? codexBinding(input, cwd).payload
|
|
2200
|
+
: { command: raw, cwd: input.cwd };
|
|
2162
2201
|
// APRV-108: a local rewrite of history this checkout never published is a
|
|
2163
2202
|
// commit. APRV-267: a delete confined to the agent's own scratch is not a
|
|
2164
2203
|
// decision. Both run in the hook's own cwd, after classification and never
|
|
@@ -2172,9 +2211,82 @@ function describeToolCall(input, adapter, protectedPaths, cwd) {
|
|
|
2172
2211
|
detail: `${classified.detail} (segment: ${classified.segment}). Rewrite it as a command the classifier can read, or run the effect through \`approval run\` with a granted token.`,
|
|
2173
2212
|
};
|
|
2174
2213
|
}
|
|
2214
|
+
const classes = classified.classes.filter((cls) => cls !== GATE_SELF_CLASS);
|
|
2215
|
+
if (adapter.kind === "codex") {
|
|
2216
|
+
// The pure shell classifier sees each segment independently. Preserve
|
|
2217
|
+
// Codex hook organs when an earlier simple `cd` changes the directory or
|
|
2218
|
+
// when the hook itself runs inside an organ directory by resolving every
|
|
2219
|
+
// later side-effecting segment's words from the effective directory.
|
|
2220
|
+
const possibleCwds = new Set([cwd]);
|
|
2221
|
+
for (const segment of classified.segments) {
|
|
2222
|
+
const parsedWords = commandSegmentWords(segment.text)?.[0];
|
|
2223
|
+
if (parsedWords?.bin === "cd" && parsedWords.args.length !== 1) {
|
|
2224
|
+
return {
|
|
2225
|
+
kind: "deny",
|
|
2226
|
+
code: "hook-io",
|
|
2227
|
+
detail: "Codex Bash cwd changes must use exact `cd <directory>` with no additional words",
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
if (parsedWords?.bin === "cd" &&
|
|
2231
|
+
(parsedWords.args[0] === "-" ||
|
|
2232
|
+
(!isAbsolute(parsedWords.args[0] ?? "") &&
|
|
2233
|
+
parsedWords.args[0] !== "." &&
|
|
2234
|
+
parsedWords.args[0] !== ".." &&
|
|
2235
|
+
!(parsedWords.args[0] ?? "").startsWith("./") &&
|
|
2236
|
+
!(parsedWords.args[0] ?? "").startsWith("../")))) {
|
|
2237
|
+
return {
|
|
2238
|
+
kind: "deny",
|
|
2239
|
+
code: "hook-io",
|
|
2240
|
+
detail: "Codex Bash cwd changes must name an absolute path, `.`, `..`, `./...`, or `../...`; OLDPWD and CDPATH-dependent operands are unsupported",
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
if (isSideEffectingClass(segment.class)) {
|
|
2244
|
+
for (const possibleCwd of possibleCwds) {
|
|
2245
|
+
const cwdSegments = possibleCwd.split(/[/\\]+/u);
|
|
2246
|
+
const cwdClass = cwdSegments.includes(".codex")
|
|
2247
|
+
? "policy.core"
|
|
2248
|
+
: protectedPathClass(possibleCwd, protectedPaths);
|
|
2249
|
+
if (cwdClass !== null && !classes.includes(cwdClass))
|
|
2250
|
+
classes.push(cwdClass);
|
|
2251
|
+
for (const word of parsedWords === undefined ? [] : [parsedWords.bin, ...parsedWords.args]) {
|
|
2252
|
+
const cls = protectedPathClass(resolvePathSegments(possibleCwd, word), protectedPaths);
|
|
2253
|
+
if (cls !== null && !classes.includes(cls))
|
|
2254
|
+
classes.push(cls);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (parsedWords?.bin === "cd" && parsedWords.args.length === 1) {
|
|
2259
|
+
// Lists and conditionals may skip a cd. Retain every prior directory
|
|
2260
|
+
// and add each directory the cd could establish; later writes are
|
|
2261
|
+
// checked against their union.
|
|
2262
|
+
const priorCwds = Array.from(possibleCwds);
|
|
2263
|
+
for (const possibleCwd of priorCwds) {
|
|
2264
|
+
const lexical = resolvePathSegments(possibleCwd, parsedWords.args[0] ?? "");
|
|
2265
|
+
possibleCwds.add(lexical);
|
|
2266
|
+
try {
|
|
2267
|
+
possibleCwds.add(realpathSync(lexical));
|
|
2268
|
+
}
|
|
2269
|
+
catch {
|
|
2270
|
+
return {
|
|
2271
|
+
kind: "deny",
|
|
2272
|
+
code: "hook-io",
|
|
2273
|
+
detail: `Codex Bash cd target ${JSON.stringify(parsedWords.args[0])} could not be resolved`,
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
if (possibleCwds.size > 64) {
|
|
2277
|
+
return {
|
|
2278
|
+
kind: "deny",
|
|
2279
|
+
code: "hook-io",
|
|
2280
|
+
detail: "Codex Bash command has more than 64 possible working directories",
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2175
2287
|
return {
|
|
2176
2288
|
kind: "gated",
|
|
2177
|
-
classes
|
|
2289
|
+
classes,
|
|
2178
2290
|
payload,
|
|
2179
2291
|
headline: raw,
|
|
2180
2292
|
notes: refined.notes,
|
|
@@ -2185,16 +2297,6 @@ function describeToolCall(input, adapter, protectedPaths, cwd) {
|
|
|
2185
2297
|
if (gated === null) {
|
|
2186
2298
|
return { kind: "allow", reason: `${input.toolName} names no file, so there is nothing to gate` };
|
|
2187
2299
|
}
|
|
2188
|
-
if (!gated.protectedPath) {
|
|
2189
|
-
return {
|
|
2190
|
-
kind: "gated",
|
|
2191
|
-
classes: [gated.cls],
|
|
2192
|
-
payload: gated.payload,
|
|
2193
|
-
headline: gated.summary,
|
|
2194
|
-
notes: [],
|
|
2195
|
-
passthrough: `${input.toolName} is not a gated edit`,
|
|
2196
|
-
};
|
|
2197
|
-
}
|
|
2198
2300
|
return {
|
|
2199
2301
|
kind: "gated",
|
|
2200
2302
|
classes: [gated.cls],
|
|
@@ -2202,7 +2304,7 @@ function describeToolCall(input, adapter, protectedPaths, cwd) {
|
|
|
2202
2304
|
headline: gated.summary,
|
|
2203
2305
|
// The tier rides in the verdict's note as well as in the payload, so an
|
|
2204
2306
|
// `allow` says which checkout it authorized (APRV-124).
|
|
2205
|
-
notes: [fileTierNote(gated)],
|
|
2307
|
+
notes: gated.protectedPath ? [fileTierNote(gated)] : [],
|
|
2206
2308
|
};
|
|
2207
2309
|
}
|
|
2208
2310
|
/** The environment variable that turns the sandbox requirement on (APRV-193). */
|
|
@@ -2328,6 +2430,7 @@ function runBypass(streams, input, adapter, cwd, logPath, flags, actor, window,
|
|
|
2328
2430
|
* shape.
|
|
2329
2431
|
*/
|
|
2330
2432
|
decidedOn) {
|
|
2433
|
+
const codexCommand = adapter.kind === "codex" ? codexBinding(input, cwd).payload.command : undefined;
|
|
2331
2434
|
const scope = hookScope(flags, cwd);
|
|
2332
2435
|
const load = loadPolicy(scope.options.policy?.file === undefined
|
|
2333
2436
|
? { dir: scope.options.policy?.dir ?? cwd }
|
|
@@ -2340,16 +2443,8 @@ decidedOn) {
|
|
|
2340
2443
|
if (described.kind === "deny") {
|
|
2341
2444
|
return deny(streams, described.code, `${described.detail} The open window does not reach this: a command the classifier cannot read is a command nothing here can establish is safe to run unapproved.`, adapter.kind);
|
|
2342
2445
|
}
|
|
2343
|
-
if (described.kind === "allow")
|
|
2344
|
-
return allow(streams, described.reason, adapter.kind);
|
|
2345
|
-
if (described.passthrough !== undefined) {
|
|
2346
|
-
// APRV-303. An ordinary workspace edit is allowed by the policy on its own
|
|
2347
|
-
// merits, so there is nothing here for the window to suspend and nothing
|
|
2348
|
-
// for a `gate.bypassed` record to say. The only thing that would have made
|
|
2349
|
-
// this call a question is a §10.2 floor, and a window bypasses the floor
|
|
2350
|
-
// outright. Answered here rather than below so the bypass log stays a
|
|
2351
|
-
// record of calls the window actually let through.
|
|
2352
|
-
return allow(streams, described.passthrough, adapter.kind);
|
|
2446
|
+
if (described.kind === "allow") {
|
|
2447
|
+
return allow(streams, described.reason, adapter.kind, codexCommand);
|
|
2353
2448
|
}
|
|
2354
2449
|
const classes = described.classes;
|
|
2355
2450
|
if (classes.length === 0) {
|
|
@@ -2357,7 +2452,7 @@ decidedOn) {
|
|
|
2357
2452
|
// record for the reason it is allowed outside a window: gating the gate
|
|
2358
2453
|
// with the gate recurses, and a window that recorded its own closing verb
|
|
2359
2454
|
// would be recording the act that ends it.
|
|
2360
|
-
return allow(streams, "the approval CLI is the gate itself and is not gated by it", adapter.kind);
|
|
2455
|
+
return allow(streams, "the approval CLI is the gate itself and is not gated by it", adapter.kind, codexCommand);
|
|
2361
2456
|
}
|
|
2362
2457
|
const mutation = classes.find((cls) => cls === "log.mutate");
|
|
2363
2458
|
if (mutation !== undefined) {
|
|
@@ -2400,9 +2495,10 @@ decidedOn) {
|
|
|
2400
2495
|
}
|
|
2401
2496
|
streams.err(bypassBanner(window, classes, recorded.record.seq));
|
|
2402
2497
|
const notes = [...described.notes, ...(policyNote === null ? [] : [policyNote])];
|
|
2403
|
-
return allow(streams, `gate-open: ${classes.join(", ")} bypassed by the window opened at seq ${String(window.seq)} by ${window.openedBy} (expires ${window.expiresAt}); recorded as gate.bypassed seq ${String(recorded.record.seq)}${notes.length === 0 ? "" : ` (${notes.join("; ")})`}`, adapter.kind);
|
|
2498
|
+
return allow(streams, `gate-open: ${classes.join(", ")} bypassed by the window opened at seq ${String(window.seq)} by ${window.openedBy} (expires ${window.expiresAt}); recorded as gate.bypassed seq ${String(recorded.record.seq)}${notes.length === 0 ? "" : ` (${notes.join("; ")})`}`, adapter.kind, codexCommand);
|
|
2404
2499
|
}
|
|
2405
2500
|
function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
2501
|
+
const configurationError = (message) => adapter.kind === "codex" ? deny(streams, "hook-io", message, adapter.kind) : usageError(streams, message);
|
|
2406
2502
|
const parsed = parseFlags(argv, {
|
|
2407
2503
|
...COMMON_FLAGS,
|
|
2408
2504
|
...POLICY_FLAGS,
|
|
@@ -2413,29 +2509,29 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
|
2413
2509
|
"--retry-grace": "string",
|
|
2414
2510
|
});
|
|
2415
2511
|
if (!parsed.ok)
|
|
2416
|
-
return
|
|
2512
|
+
return configurationError(parsed.message);
|
|
2417
2513
|
if (boolFlag(parsed.flags, "--help") || boolFlag(parsed.flags, "-h")) {
|
|
2418
2514
|
streams.out(`${HOOK_HELP}\n`);
|
|
2419
2515
|
return EXIT_OK;
|
|
2420
2516
|
}
|
|
2421
2517
|
const extra = parsed.positionals[0];
|
|
2422
2518
|
if (extra !== undefined) {
|
|
2423
|
-
return
|
|
2519
|
+
return configurationError(`unexpected argument ${JSON.stringify(extra)}`);
|
|
2424
2520
|
}
|
|
2425
2521
|
const asFlag = stringFlag(parsed.flags, "--as");
|
|
2426
2522
|
const actor = asFlag ?? adapter.defaultActor;
|
|
2427
2523
|
if (!PRINCIPAL_ACTOR.test(actor)) {
|
|
2428
|
-
return
|
|
2524
|
+
return configurationError(`--as expects agent:<id> or human:<id>, got ${JSON.stringify(asFlag)}`);
|
|
2429
2525
|
}
|
|
2430
2526
|
const timeoutText = stringFlag(parsed.flags, "--timeout") ?? DEFAULT_TIMEOUT;
|
|
2431
2527
|
const timeoutMs = parseDuration(timeoutText);
|
|
2432
2528
|
if (timeoutMs === null) {
|
|
2433
|
-
return
|
|
2529
|
+
return configurationError(`--timeout expects a duration like 30s, 9m, got ${JSON.stringify(timeoutText)}`);
|
|
2434
2530
|
}
|
|
2435
2531
|
const intervalText = stringFlag(parsed.flags, "--interval");
|
|
2436
2532
|
const intervalMs = intervalText === null ? DEFAULT_INTERVAL_MS : parseDuration(intervalText);
|
|
2437
2533
|
if (intervalMs === null) {
|
|
2438
|
-
return
|
|
2534
|
+
return configurationError(`--interval expects a duration like 500ms, 2s, got ${JSON.stringify(intervalText)}`);
|
|
2439
2535
|
}
|
|
2440
2536
|
// APRV-287. How long the question outlives the wait, for the retry that
|
|
2441
2537
|
// adopts it. The duration grammar has no zero, so the shortest window is
|
|
@@ -2443,12 +2539,18 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
|
2443
2539
|
const graceText = stringFlag(parsed.flags, "--retry-grace");
|
|
2444
2540
|
const graceMs = graceText === null ? HOOK_RETRY_GRACE_MS : parseDuration(graceText);
|
|
2445
2541
|
if (graceMs === null) {
|
|
2446
|
-
return
|
|
2542
|
+
return configurationError(`--retry-grace expects a duration like 5m, 30s, 1ms, got ${JSON.stringify(graceText)}`);
|
|
2447
2543
|
}
|
|
2448
2544
|
const parsedInput = parseHookInput(readStdin());
|
|
2449
2545
|
if (!parsedInput.ok)
|
|
2450
2546
|
return deny(streams, "hook-io", parsedInput.detail, adapter.kind);
|
|
2451
2547
|
const input = parsedInput.input;
|
|
2548
|
+
if (adapter.kind === "codex") {
|
|
2549
|
+
const checked = checkCodexHookInput(input, cwd);
|
|
2550
|
+
if (!checked.ok)
|
|
2551
|
+
return deny(streams, "hook-io", checked.detail, adapter.kind);
|
|
2552
|
+
}
|
|
2553
|
+
const codexCommand = adapter.kind === "codex" ? codexBinding(input, cwd).payload.command : undefined;
|
|
2452
2554
|
// APRV-145: WHICH EVENT THIS IS, read first and read at all. One command is
|
|
2453
2555
|
// registered for two events, and they do opposite things — one answers before
|
|
2454
2556
|
// the tool runs, the other records how it went — so the dispatch is the first
|
|
@@ -2459,7 +2561,10 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
|
2459
2561
|
// harness whose event this runtime does not recognize is a harness about to
|
|
2460
2562
|
// run a command, and treating an unknown name as a no-op would be an ungated
|
|
2461
2563
|
// one.
|
|
2462
|
-
|
|
2564
|
+
const postToolEvent = adapter.kind === "codex"
|
|
2565
|
+
? input.hookEventName === CODEX_POST_TOOL_EVENT
|
|
2566
|
+
: input.hookEventName !== null && POST_TOOL_EVENTS.includes(input.hookEventName);
|
|
2567
|
+
if (postToolEvent) {
|
|
2463
2568
|
// APRV-303. `commandHarnessHook`'s catch turns a throw into a DENY, which is
|
|
2464
2569
|
// the right answer for a call that has not run yet and exactly the wrong one
|
|
2465
2570
|
// here: it would print a verdict object about a tool call the harness has
|
|
@@ -2473,8 +2578,17 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
|
2473
2578
|
return report(streams, "post-tool-io", `the counterpart failed: ${cause instanceof Error ? cause.message : String(cause)}; nothing was appended, so the start this event would have closed is still open`);
|
|
2474
2579
|
}
|
|
2475
2580
|
}
|
|
2581
|
+
// Codex 0.152.1 can execute Bash in a per-call working directory that is
|
|
2582
|
+
// absent from tool_input while both the event cwd and this hook process stay
|
|
2583
|
+
// at the session root (APRV-310 native v6). A decision over the visible
|
|
2584
|
+
// `{command, cwd}` would therefore bind different bytes from the action the
|
|
2585
|
+
// harness executes. Refuse before the open-window, gate-self, carry, or
|
|
2586
|
+
// registration paths; none of those can supply the missing directory fact.
|
|
2587
|
+
if (adapter.kind === "codex" && input.toolName === "Bash") {
|
|
2588
|
+
return deny(streams, "hook-io", "Codex Bash is disabled because the native hook contract does not expose the effective per-call working directory; no policy or open window can authorize bytes the hook cannot bind", adapter.kind);
|
|
2589
|
+
}
|
|
2476
2590
|
if (input.toolName !== adapter.shellTool && !adapter.fileTools.includes(input.toolName)) {
|
|
2477
|
-
return allow(streams, `${input.toolName} is not a gated tool`, adapter.kind);
|
|
2591
|
+
return allow(streams, `${input.toolName} is not a gated tool`, adapter.kind, codexCommand);
|
|
2478
2592
|
}
|
|
2479
2593
|
// APRV-188. From here on this process may resume a verified read behind the
|
|
2480
2594
|
// snapshot the daemon published, instead of walking the chain from genesis:
|
|
@@ -2534,13 +2648,14 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
|
2534
2648
|
if (described.kind === "deny") {
|
|
2535
2649
|
return deny(streams, described.code, described.detail, adapter.kind);
|
|
2536
2650
|
}
|
|
2537
|
-
if (described.kind === "allow")
|
|
2538
|
-
return allow(streams, described.reason, adapter.kind);
|
|
2651
|
+
if (described.kind === "allow") {
|
|
2652
|
+
return allow(streams, described.reason, adapter.kind, codexCommand);
|
|
2653
|
+
}
|
|
2539
2654
|
const { classes, payload, headline } = described;
|
|
2540
2655
|
/** What the history-rewrite refinement did, for the decision reason. */
|
|
2541
2656
|
const notes = [...described.notes];
|
|
2542
2657
|
if (classes.length === 0) {
|
|
2543
|
-
return allow(streams, "the approval CLI is the gate itself and is not gated by it", adapter.kind);
|
|
2658
|
+
return allow(streams, "the approval CLI is the gate itself and is not gated by it", adapter.kind, codexCommand);
|
|
2544
2659
|
}
|
|
2545
2660
|
// Every path from here needs the log, the fast paths included (APRV-139):
|
|
2546
2661
|
// attestation and loop-escalation are facts about the log, so the
|
|
@@ -2553,7 +2668,9 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
|
2553
2668
|
}
|
|
2554
2669
|
// Minted once, here, and carried into `gateAndWait`: the loop-escalation
|
|
2555
2670
|
// check below and any registration that follows must name the same task.
|
|
2556
|
-
const task =
|
|
2671
|
+
const task = adapter.kind === "codex"
|
|
2672
|
+
? codexBinding(input, cwd).task
|
|
2673
|
+
: `hook:${input.sessionId}:${input.toolUseId ?? randomBytes(8).toString("hex")}`;
|
|
2557
2674
|
const run = {
|
|
2558
2675
|
logPath,
|
|
2559
2676
|
options,
|
|
@@ -2564,6 +2681,7 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
|
2564
2681
|
ttlMs: load.durations.approvalTtlMs,
|
|
2565
2682
|
harness: adapter.kind,
|
|
2566
2683
|
originApp: adapter.originApp,
|
|
2684
|
+
...(codexCommand === undefined ? {} : { codexCommand }),
|
|
2567
2685
|
eventVersion: input.harnessVersion,
|
|
2568
2686
|
// Off the policy this function already loaded and validated, so the names
|
|
2569
2687
|
// printed are the names a channel process would serve (APRV-281). Sorted
|
|
@@ -2659,20 +2777,6 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
|
2659
2777
|
* refinement's own words, and the loop floor's when one applied.
|
|
2660
2778
|
*/
|
|
2661
2779
|
const note = notes.length === 0 ? "" : ` (${notes.join("; ")})`;
|
|
2662
|
-
// APRV-303, and the last thing that can answer without touching the log: an
|
|
2663
|
-
// ordinary workspace edit, which the policy allows on its own merits and which
|
|
2664
|
-
// is a question only while a floor stands.
|
|
2665
|
-
//
|
|
2666
|
-
// The order is the whole fix. Until APRV-303 this allow was printed from
|
|
2667
|
-
// `describeToolCall`'s own branch, several hundred lines above the floor
|
|
2668
|
-
// lookup, so a session whose Bash calls were all being routed to a human went
|
|
2669
|
-
// on editing files unrouted and uncounted. Now the same allow is printed, in
|
|
2670
|
-
// the same words, from BELOW the floor: the fast path is as fast as it was,
|
|
2671
|
-
// and the floored path routes an Edit exactly as it routes an `echo >`,
|
|
2672
|
-
// because both are `files.write.workspace` and one predicate decides.
|
|
2673
|
-
if (described.passthrough !== undefined && floor === null) {
|
|
2674
|
-
return allow(streams, `${described.passthrough}${note}`, adapter.kind);
|
|
2675
|
-
}
|
|
2676
2780
|
/** No class here needs a human, so nothing downstream will ask for one. */
|
|
2677
2781
|
const unattended = floor === null && autonomies.every((autonomy) => autonomy !== "manual");
|
|
2678
2782
|
if (unattended) {
|
|
@@ -2690,7 +2794,7 @@ function runHarnessHook(argv, streams, cwd, readStdin, adapter) {
|
|
|
2690
2794
|
if (charged !== null) {
|
|
2691
2795
|
return deny(streams, `hook-gate-refused:${charged.code}`, charged.message, adapter.kind);
|
|
2692
2796
|
}
|
|
2693
|
-
return allow(streams, `autonomous: ${classes.join(", ")}${note}`, adapter.kind);
|
|
2797
|
+
return allow(streams, `autonomous: ${classes.join(", ")}${note}`, adapter.kind, codexCommand);
|
|
2694
2798
|
}
|
|
2695
2799
|
// Past here the hook appends. It writes to a log that already exists and
|
|
2696
2800
|
// creates none: a log the hook scaffolded where it happened to be standing
|
|
@@ -2734,6 +2838,8 @@ export function commandHook(argv, streams, cwd, readStdin = defaultStdin) {
|
|
|
2734
2838
|
return commandHarnessHook(rest, streams, cwd, readStdin, CLAUDE_ADAPTER);
|
|
2735
2839
|
case "cursor":
|
|
2736
2840
|
return commandHarnessHook(rest, streams, cwd, readStdin, CURSOR_ADAPTER);
|
|
2841
|
+
case "codex":
|
|
2842
|
+
return commandHarnessHook(rest, streams, cwd, readStdin, CODEX_ADAPTER);
|
|
2737
2843
|
case "classify":
|
|
2738
2844
|
return commandClassify(rest, streams, cwd);
|
|
2739
2845
|
default:
|