@sabaiway/agent-workflow-kit 5.4.0 → 5.6.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/CHANGELOG.md +130 -0
- package/README.md +1 -0
- package/SKILL.md +5 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +1 -1
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +51 -4
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +616 -24
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +700 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +1 -1
- package/bridges/codex-cli-bridge/capability.json +15 -10
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/dispatch.md +29 -0
- package/references/modes/gates.md +6 -3
- package/references/modes/procedures.md +2 -0
- package/references/modes/receipt-deadline.md +3 -3
- package/references/modes/recommendations.md +1 -1
- package/references/modes/velocity.md +1 -0
- package/tools/commands.mjs +7 -0
- package/tools/core-evidence.mjs +37 -3
- package/tools/detect-backends.mjs +5 -4
- package/tools/dispatch-record.mjs +10 -3
- package/tools/dispatch-store.mjs +392 -0
- package/tools/dispatch.mjs +1779 -0
- package/tools/doc-parity.mjs +10 -2
- package/tools/exec-producer.mjs +483 -0
- package/tools/exec-receipt.mjs +263 -0
- package/tools/flow-check-cores.mjs +253 -0
- package/tools/flow-check-git-lane.mjs +56 -0
- package/tools/flow-check-rungs.mjs +330 -0
- package/tools/flow-check.mjs +23 -611
- package/tools/flow-store.mjs +111 -462
- package/tools/gates-declaration.mjs +13 -1
- package/tools/gates-init.mjs +134 -22
- package/tools/procedures.mjs +64 -5
- package/tools/receipt-deadline.mjs +25 -3
- package/tools/recommendations.mjs +108 -7
- package/tools/release-scan.mjs +33 -0
- package/tools/source-size-check.mjs +320 -0
- package/tools/source-size-config.mjs +244 -0
- package/tools/source-size-core.mjs +53 -0
- package/tools/source-size-gate-cmd.mjs +55 -0
- package/tools/source-size-judge.mjs +114 -0
- package/tools/source-size-refusal.mjs +70 -0
- package/tools/source-size-report.mjs +254 -0
- package/tools/source-size-scope.mjs +145 -0
- package/tools/store-append.mjs +444 -0
- package/tools/velocity-profile.mjs +24 -3
|
@@ -50,8 +50,8 @@ case "${1:-}" in
|
|
|
50
50
|
codex-exec — delegate plan/instruction EXECUTION to the OpenAI Codex CLI (subscription-only; workspace-write sandbox, network OFF, git writes blocked — the orchestrator commits).
|
|
51
51
|
|
|
52
52
|
Usage:
|
|
53
|
-
codex-exec <plan-file|->
|
|
54
|
-
codex-exec <plan-file|-> -- <extra codex flags...>
|
|
53
|
+
codex-exec [--nonce <n>] <plan-file|->
|
|
54
|
+
codex-exec [--nonce <n>] <plan-file|-> -- <extra codex flags...>
|
|
55
55
|
|
|
56
56
|
Grounding:
|
|
57
57
|
automatic — the root AGENTS.md (Hard Constraints) is auto-merged into codex's
|
|
@@ -59,14 +59,62 @@ Grounding:
|
|
|
59
59
|
grounding flags
|
|
60
60
|
|
|
61
61
|
Round-2 / resume:
|
|
62
|
-
codex-exec --resume-last <plan-file|->
|
|
63
|
-
codex-exec --resume <session-id> <plan-file|->
|
|
62
|
+
codex-exec --resume-last [--nonce <n>] <plan-file|->
|
|
63
|
+
codex-exec --resume <session-id> [--nonce <n>] <plan-file|->
|
|
64
64
|
(resume continues the recorded session without re-sending context; takes no '--' passthrough)
|
|
65
65
|
|
|
66
66
|
Guarded passthrough after '--':
|
|
67
67
|
blocked always: -c* --config* -s* --sandbox* --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust --full-auto --oss --local-provider* -p* --profile* -m* --model* -o* --output-last-message* --json* --color* --output-schema* --ephemeral*
|
|
68
68
|
relaxed only under CODEX_PROBE=1: --add-dir* -C* --cd* --skip-git-repo-check --ignore-rules --enable* --disable*
|
|
69
69
|
|
|
70
|
+
Receipt:
|
|
71
|
+
side effect — a NONCED run mints ONE exec receipt beside the delegation store: the dispatch nonce
|
|
72
|
+
seam is the AW_DISPATCH_NONCE environment value or its plain-argument equivalent --nonce <n>,
|
|
73
|
+
recognised ONLY before the prompt operand (after the operand or a literal '--' it is passthrough
|
|
74
|
+
payload, never a flag), under the safe grammar [A-Za-z0-9._-]{1,64} — anything else, a duplicate,
|
|
75
|
+
or a flag disagreeing with a non-empty env value refuses PRE-SPEND. The store directory resolves
|
|
76
|
+
exactly as the kit's delegation store does: the dirname of an ABSOLUTE AW_DELEGATION_STORE
|
|
77
|
+
(a relative one, or one ending in a path separator, refuses), else the git common dir. The
|
|
78
|
+
artifact is agent-workflow-exec-receipt-<backendLength>-<backend>-<nonce>.json in two states:
|
|
79
|
+
'reserved' is written atomically and NO-CLOBBER immediately before the CLI runs — that write IS
|
|
80
|
+
the nonce reservation, so a second dispatch on the same nonce, or an already-taken report name,
|
|
81
|
+
refuses BEFORE any spend — and 'terminal' replaces it in place at exit. A nonced run also refuses
|
|
82
|
+
pre-spend when no timeout/gtimeout binary can cap it (an accounted dispatch that cannot be capped
|
|
83
|
+
can never honour the terminal-exit rule; a nonce-LESS run still warns and runs uncapped), when
|
|
84
|
+
node is missing (the mint core), and when the prompt rides on stdin instead of a contract FILE —
|
|
85
|
+
contractDigest is computed BY THIS WRAPPER from the dispatch file it was actually handed, so the
|
|
86
|
+
kit can refuse a run that executed a different contract than the one it opened. That digest is
|
|
87
|
+
taken from the SAME bytes already read as the prompt, never a second open of the path: two reads
|
|
88
|
+
leave a window in which the file can be swapped, and the run would then execute one contract while
|
|
89
|
+
its receipt claimed the digest of another. The header's own nonce must EQUAL the dispatch nonce —
|
|
90
|
+
'dispatch open' copies the nonce FROM the header, so a disagreeing --nonce could only reserve an
|
|
91
|
+
identity no return would ever absorb, and it refuses pre-spend. A contract file edited BETWEEN
|
|
92
|
+
'dispatch open' and the run is caught at ABSORB by the contractDigest comparison, not pre-spend:
|
|
93
|
+
the wrapper never reads the ledger, and that boundary is what the whole lane rests on. At exit the
|
|
94
|
+
wrapper FIRST re-reads its reservation and verifies its own opaque owner token — a foreign owner
|
|
95
|
+
refuses having published NOTHING, neither report nor receipt — THEN writes the delegate's final
|
|
96
|
+
message atomically to agent-workflow-exec-report-<backendLength>-<backend>-<nonce>.txt, THEN
|
|
97
|
+
re-verifies the owner and REPLACES the reservation with the terminal receipt {schema, kind, state,
|
|
98
|
+
backend, nonce, owner, contractDigest, wrapperVersion, posture {model, effort, tier}, capS,
|
|
99
|
+
killGraceS, sessionId, exitStatus, outcome, reportDigest, reportLength, timestamp}: the report is
|
|
100
|
+
complete on disk before any artifact says the run arrived. capS and killGraceS are the cap the run
|
|
101
|
+
ACTUALLY applied. outcome is the wrapper's own SUBSET of the ledger's vocabulary — exit 0 with a
|
|
102
|
+
session id -> success, exit 0 without one -> missing-identity, ANY nonzero exit including the
|
|
103
|
+
timeout codes 124 and 137 -> transport-failure; every orchestrator judgment is recorded at absorb
|
|
104
|
+
time, never claimed here. The session id is captured BEFORE outcome branching, so a FAILED run
|
|
105
|
+
records one too; in resume mode it is the validated resume id. FAIL-CLOSED, deliberately NOT the
|
|
106
|
+
review lane's warn-only receipt: a publication that cannot complete exits nonzero with a DISTINCT
|
|
107
|
+
status, and the message states only what the run can still prove. 70: the reservation could not be
|
|
108
|
+
verified BEFORE any publication — NOTHING was published, not the report and not the receipt, and
|
|
109
|
+
because the artifact found there belongs to another run it is never a '--no-receipt' source. 71: a
|
|
110
|
+
publication stopped after that point — either the report write failed (nothing beyond the
|
|
111
|
+
reservation was published; the '--no-receipt' absorb then records reportLength 0, ineligible by the
|
|
112
|
+
name empty-report) or the report IS on disk and the terminal receipt was not completed (the absorb
|
|
113
|
+
reads it, report-if-present). The post-report lane never claims the reservation still stands,
|
|
114
|
+
because after that point its fate is no longer something this run observed. Every lane names the
|
|
115
|
+
tree as partial/dirtied rather than untouched. A nonce-LESS invocation is byte-unchanged: no
|
|
116
|
+
reservation, no receipt, no artifact, no node.
|
|
117
|
+
|
|
70
118
|
Notes:
|
|
71
119
|
nested-sandbox limit: codex-exec ships its OWN OS sandbox (bwrap workspace-write) and cannot run
|
|
72
120
|
nested inside a harness sandbox (the FS turns read-only) — route it OUTSIDE the harness sandbox
|
|
@@ -104,7 +152,7 @@ Settings file (KEY=VALUE, parsed never sourced; env wins over file, file wins ov
|
|
|
104
152
|
CODEX_SERVICE_TIER — service tier: 'priority' (Fast — ~1.5x speed at a 2.5x credit rate on gpt-5.6-sol); a consented SPEND knob, default off (standard tier)
|
|
105
153
|
CODEX_HARD_TIMEOUT — hard wall-clock cap, integer seconds 1..86400 (built-in default 3600)
|
|
106
154
|
|
|
107
|
-
Environment: CODEX_HARD_TIMEOUT (seconds, default 3600), CODEX_PROBE=1 (throwaway probe only).
|
|
155
|
+
Environment: CODEX_HARD_TIMEOUT (seconds, default 3600), CODEX_PROBE=1 (throwaway probe only), AW_DISPATCH_NONCE (delegation dispatch nonce — mints the exec receipt; the --nonce <n> flag is its plain-argument equivalent), AW_DELEGATION_STORE (absolute delegation-store path; its dirname is where the receipt lands).
|
|
108
156
|
Requires at run time: the codex CLI on PATH, a ChatGPT-subscription login, a git work tree with a root AGENTS.md (--help needs none of these).
|
|
109
157
|
HELP
|
|
110
158
|
exit 0
|
|
@@ -295,6 +343,17 @@ for _posture_pair in "CODEX_MODEL=$CODEX_MODEL" "CODEX_EFFORT=$CODEX_EFFORT" "CO
|
|
|
295
343
|
exit 2
|
|
296
344
|
fi
|
|
297
345
|
done
|
|
346
|
+
# The delegation dispatch nonce seam (delegation Plan 2 / D11): ONE seam, the AW_DISPATCH_NONCE
|
|
347
|
+
# environment value and its plain-argument twin --nonce <n> (parsed below, after the mode selector).
|
|
348
|
+
# Validated pre-spend under the SAFE grammar — a nonce that would escape the derived artifact name
|
|
349
|
+
# refuses before any CLI run. The bracket expression ENUMERATES the ASCII set (no ranges): a range
|
|
350
|
+
# like A-Z is locale-collation-dependent and could admit a non-ASCII nonce the kit's JS reader then
|
|
351
|
+
# refuses. Byte-identical rule to codex-review.sh's AW_REVIEW_NONCE screen — one grammar, two lanes.
|
|
352
|
+
AW_NONCE_RE='^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-]{1,64}$'
|
|
353
|
+
if [[ -n "${AW_DISPATCH_NONCE:-}" && ! "${AW_DISPATCH_NONCE}" =~ $AW_NONCE_RE ]]; then
|
|
354
|
+
echo "error: AW_DISPATCH_NONCE fails the safe nonce grammar ([A-Za-z0-9._-]{1,64}) — the derived receipt name would be unsafe; fix the nonce and re-run." >&2
|
|
355
|
+
exit 2
|
|
356
|
+
fi
|
|
298
357
|
CODEX_HARD_TIMEOUT="$(aw_effective_timeout CODEX_HARD_TIMEOUT 3600)"
|
|
299
358
|
if [[ -n "$CODEX_SERVICE_TIER" ]] && ! aw_settings_valid CODEX_SERVICE_TIER "$CODEX_SERVICE_TIER"; then
|
|
300
359
|
echo "warning: CODEX_SERVICE_TIER='$CODEX_SERVICE_TIER' is not a supported service tier ('priority') — running on the standard tier." >&2
|
|
@@ -306,6 +365,19 @@ if [[ -n "$CODEX_SERVICE_TIER" ]]; then
|
|
|
306
365
|
fi
|
|
307
366
|
CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
|
|
308
367
|
|
|
368
|
+
# --- Exec-receipt identity (delegation Plan 2) --------------------------------
|
|
369
|
+
# AW_RECEIPT_BACKEND is the ledger's backend id (the value `dispatch open --backend` records), not
|
|
370
|
+
# the wrapper name — the artifact name is a function of {backend, nonce} alone on both sides.
|
|
371
|
+
# AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json version and is stamped into every
|
|
372
|
+
# receipt this wrapper mints; scripts/release/version-sync.mjs bumps it under the one-anchor-per-file
|
|
373
|
+
# rule, so a release can never leave it behind (the AD-053 drift class).
|
|
374
|
+
AW_RECEIPT_BACKEND="codex"
|
|
375
|
+
AW_BRIDGE_VERSION="3.5.0" # aw-version-anchor
|
|
376
|
+
# The kill grace handed to timeout(1) as --kill-after, and recorded in the receipt as killGraceS:
|
|
377
|
+
# ONE constant, so the number the ledger checks against the dispatch deadline is the number the run
|
|
378
|
+
# actually applied.
|
|
379
|
+
CODEX_KILL_GRACE_S=15
|
|
380
|
+
|
|
309
381
|
# --- Quality-first guard: refuse a silent model/effort downgrade ---------------
|
|
310
382
|
# Real delegated runs must use the frontier model at max effort. A throwaway probe
|
|
311
383
|
# (effort-independent result) may opt out with CODEX_PROBE=1, announced loudly.
|
|
@@ -443,6 +515,45 @@ case "${1:-}" in
|
|
|
443
515
|
;;
|
|
444
516
|
esac
|
|
445
517
|
|
|
518
|
+
# --- --nonce <n> — the plain-argument lane onto the AW_DISPATCH_NONCE seam (D11) ---------------
|
|
519
|
+
# Recognised at exactly ONE position: AFTER the mode selector and BEFORE the prompt operand, so the
|
|
520
|
+
# three accepted forms are `codex-exec [--nonce <n>] <plan-file|->`,
|
|
521
|
+
# `codex-exec --resume-last [--nonce <n>] <plan-file|->` and
|
|
522
|
+
# `codex-exec --resume <session-id> [--nonce <n>] <plan-file|->`. A GLOBAL strip (the codex-review.sh
|
|
523
|
+
# shape) is wrong here: this wrapper's remaining args are the prompt operand and the `--` passthrough
|
|
524
|
+
# payload, and consuming a `--nonce` out of them would move that boundary — after the operand or a
|
|
525
|
+
# literal `--`, `--nonce` is payload, never a flag.
|
|
526
|
+
# Semantics are codex-review.sh's, exactly: the flag and a non-empty env value must AGREE (two
|
|
527
|
+
# disagreeing sources would mint an ambiguous dispatch identity — fail closed), a duplicate refuses
|
|
528
|
+
# (one dispatch carries one nonce), and the NEXT argument is taken unconditionally (only end-of-args
|
|
529
|
+
# refuses here) — the closed grammar is the single validity door, so a grammar-valid leading-dash
|
|
530
|
+
# value is legal, and an EMPTY value still hits the grammar screen.
|
|
531
|
+
nonce_flag=""
|
|
532
|
+
nonce_flag_set=0
|
|
533
|
+
while [[ "${1:-}" == "--nonce" ]]; do
|
|
534
|
+
if [[ "$nonce_flag_set" == "1" ]]; then
|
|
535
|
+
echo "error: duplicate --nonce — one dispatch carries one nonce." >&2
|
|
536
|
+
exit 2
|
|
537
|
+
fi
|
|
538
|
+
if [[ $# -lt 2 ]]; then
|
|
539
|
+
echo "error: --nonce needs a value; got '<end of args>'." >&2
|
|
540
|
+
exit 2
|
|
541
|
+
fi
|
|
542
|
+
nonce_flag="$2"; nonce_flag_set=1; shift 2
|
|
543
|
+
done
|
|
544
|
+
if [[ "$nonce_flag_set" == "1" ]]; then
|
|
545
|
+
if [[ ! "$nonce_flag" =~ $AW_NONCE_RE ]]; then
|
|
546
|
+
echo "error: --nonce fails the safe nonce grammar ([A-Za-z0-9._-]{1,64}) — the derived receipt name would be unsafe; fix the nonce and re-run." >&2
|
|
547
|
+
exit 2
|
|
548
|
+
fi
|
|
549
|
+
if [[ -n "${AW_DISPATCH_NONCE:-}" && "${AW_DISPATCH_NONCE}" != "$nonce_flag" ]]; then
|
|
550
|
+
echo "error: --nonce disagrees with the AW_DISPATCH_NONCE environment value — one dispatch carries one nonce; drop one source and re-run." >&2
|
|
551
|
+
exit 2
|
|
552
|
+
fi
|
|
553
|
+
AW_DISPATCH_NONCE="$nonce_flag"
|
|
554
|
+
fi
|
|
555
|
+
aw_nonce="${AW_DISPATCH_NONCE:-}"
|
|
556
|
+
|
|
446
557
|
if [[ $# -lt 1 ]]; then
|
|
447
558
|
echo "error: missing <plan-file|-> (the instruction to send)." >&2
|
|
448
559
|
exit 2
|
|
@@ -553,6 +664,176 @@ if [[ -z "${task//[[:space:]]/}" ]]; then
|
|
|
553
664
|
exit 2
|
|
554
665
|
fi
|
|
555
666
|
|
|
667
|
+
# --- The ACCOUNTED lane: everything below fires only for a NONCED run ---------
|
|
668
|
+
# A nonce-LESS invocation is byte-unchanged — no reservation, no receipt, no artifact, no node —
|
|
669
|
+
# which is what keeps every existing caller working. The whole mint core rides ONE node script per
|
|
670
|
+
# step (the family floor, codex-review.sh's finding-manifest precedent): a bash re-implementation of
|
|
671
|
+
# atomic no-clobber publication, JSON composition and canonical digesting would be a second, drifting
|
|
672
|
+
# implementation of contracts the kit owns.
|
|
673
|
+
aw_owner=""
|
|
674
|
+
aw_contract_digest=""
|
|
675
|
+
aw_artifact_dir=""
|
|
676
|
+
|
|
677
|
+
# Each mint core rides a BUILTIN heredoc read — `IFS= read -r -d '' … <<'AW_JS'`, the idiom this
|
|
678
|
+
# wrapper already uses for its two prompt directives — rather than a `$(cat <<'AW_JS' …)` command
|
|
679
|
+
# substitution. That is not style: a failed `cat` (gone from PATH, shadowed, or replaced) makes the
|
|
680
|
+
# substitution EMPTY, `node -e ""` exits 0, and the wrapper would report a successful accounted run
|
|
681
|
+
# having published no report and no terminal receipt at all — silent, on the success path. The
|
|
682
|
+
# builtin cannot fail that way because there is no external binary left to fail. The assertion below
|
|
683
|
+
# is the belt: an empty core never runs, it refuses by name.
|
|
684
|
+
aw_require_core() {
|
|
685
|
+
[[ -n "$1" ]] && return 0
|
|
686
|
+
echo "error: $2 is EMPTY — refusing to run an empty program rather than reporting a run that published nothing." >&2
|
|
687
|
+
return 1
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
# The delegation store's directory, resolved EXACTLY as the kit resolves it
|
|
691
|
+
# (agent-workflow-kit/tools/dispatch-store.mjs): an ABSOLUTE AW_DELEGATION_STORE wins (its dirname),
|
|
692
|
+
# a relative one or one ending in a path separator refuses, otherwise the git common dir. Mirrored
|
|
693
|
+
# through node's own path primitives rather than re-derived in bash, so the two answers cannot drift
|
|
694
|
+
# on `..` segments, duplicate separators or a trailing dot — the cross-package parity test pins it.
|
|
695
|
+
aw_resolve_artifact_dir() {
|
|
696
|
+
local common=""
|
|
697
|
+
if [[ -z "${AW_DELEGATION_STORE:-}" ]]; then
|
|
698
|
+
if [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" != "true" ]]; then
|
|
699
|
+
echo "error: a nonced dispatch resolves its receipt beside the delegation store, and there is no store outside a git work tree (set AW_DELEGATION_STORE to an absolute path, or run from the repository)." >&2
|
|
700
|
+
return 1
|
|
701
|
+
fi
|
|
702
|
+
common="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)"
|
|
703
|
+
if [[ -z "$common" ]]; then
|
|
704
|
+
echo "error: could not resolve the git common dir — the receipt has nowhere to land; refusing before any spend." >&2
|
|
705
|
+
return 1
|
|
706
|
+
fi
|
|
707
|
+
fi
|
|
708
|
+
local aw_js=""
|
|
709
|
+
IFS= read -r -d '' aw_js <<'AW_JS' || true
|
|
710
|
+
const { isAbsolute, normalize, sep, join, dirname } = require("node:path");
|
|
711
|
+
const common = process.argv[1] || "";
|
|
712
|
+
const override = process.env.AW_DELEGATION_STORE || "";
|
|
713
|
+
let storePath;
|
|
714
|
+
if (override) {
|
|
715
|
+
if (!isAbsolute(override)) {
|
|
716
|
+
process.stderr.write("error: AW_DELEGATION_STORE must be an ABSOLUTE path (got \"" + override + "\") — a relative override resolves a different ledger from each worktree/cwd; refusing before any spend.\n");
|
|
717
|
+
process.exit(2);
|
|
718
|
+
}
|
|
719
|
+
const normalized = normalize(override);
|
|
720
|
+
if (normalized.endsWith(sep) || normalized.endsWith("/")) {
|
|
721
|
+
process.stderr.write("error: AW_DELEGATION_STORE must not end with a path separator (got \"" + override + "\") — a store is a file, not a directory; refusing before any spend.\n");
|
|
722
|
+
process.exit(2);
|
|
723
|
+
}
|
|
724
|
+
storePath = normalized;
|
|
725
|
+
} else {
|
|
726
|
+
storePath = join(common, "agent-workflow-delegation.jsonl");
|
|
727
|
+
}
|
|
728
|
+
process.stdout.write(dirname(storePath));
|
|
729
|
+
AW_JS
|
|
730
|
+
aw_require_core "$aw_js" "the store-directory resolver" || return 1
|
|
731
|
+
NODE_OPTIONS= node -e "$aw_js" "$common"
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
# The contract header this run is accountable for: its `nonce` and its `contractDigest`, both read
|
|
735
|
+
# from the bytes ALREADY loaded as the prompt (fed on stdin) rather than from a second open of the
|
|
736
|
+
# path. That single read is load-bearing: two reads leave a window in which the file can be swapped,
|
|
737
|
+
# and the run would then execute contract A while its receipt claimed the digest of B — a return the
|
|
738
|
+
# ledger would accept. Dropping the trailing newlines (`$( )` does) cannot move the digest: the digest
|
|
739
|
+
# is taken over the PARSED object and the fence walk is line-based, so anything after the closing
|
|
740
|
+
# fence is outside the block either way.
|
|
741
|
+
#
|
|
742
|
+
# contractDigest is sha256 over the CANONICAL serialization (recursively key-sorted JSON, no trailing
|
|
743
|
+
# newline) of the ONE top-level ```aw-dispatch-contract block. Independently produced HERE: without
|
|
744
|
+
# it the ledger would compare a dispatch record against values derived from itself, and a run that
|
|
745
|
+
# executed a DIFFERENT contract would correlate cleanly. The fence walk mirrors the kit's
|
|
746
|
+
# extractContractBlock — nesting-aware, so a contract marker inside another fenced block is example
|
|
747
|
+
# text; both line endings accepted. FORM validation is deliberately NOT duplicated here: the kit's
|
|
748
|
+
# `open` is the single form door, and a second one would drift. The ONE field validated is `nonce`,
|
|
749
|
+
# because this wrapper COMPARES it — a value outside the safe grammar could not be compared or named.
|
|
750
|
+
aw_compute_contract_header() {
|
|
751
|
+
local aw_js=""
|
|
752
|
+
IFS= read -r -d '' aw_js <<'AW_JS' || true
|
|
753
|
+
const fs = require("node:fs");
|
|
754
|
+
const { createHash } = require("node:crypto");
|
|
755
|
+
const INFO = "aw-dispatch-contract";
|
|
756
|
+
const FENCE = /^(`{3,})(.*)$/;
|
|
757
|
+
const SAFE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
758
|
+
const die = (m) => { process.stderr.write("error: dispatch contract — " + m + ".\n"); process.exit(2); };
|
|
759
|
+
let text;
|
|
760
|
+
try { text = fs.readFileSync(0, "utf8"); } catch (err) { die("cannot read the dispatch contract bytes (" + (err && err.code) + ")"); }
|
|
761
|
+
const lines = text.split(/\r?\n/);
|
|
762
|
+
const blocks = [];
|
|
763
|
+
let openTicks = 0, openInfo = "", openAt = -1;
|
|
764
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
765
|
+
const fence = FENCE.exec(lines[i]);
|
|
766
|
+
if (fence === null) continue;
|
|
767
|
+
const ticks = fence[1].length;
|
|
768
|
+
const info = fence[2].trim();
|
|
769
|
+
if (openTicks === 0) { openTicks = ticks; openInfo = info; openAt = i; continue; }
|
|
770
|
+
if (info === "" && ticks >= openTicks) {
|
|
771
|
+
if (openInfo === INFO) blocks.push(lines.slice(openAt + 1, i).join("\n"));
|
|
772
|
+
openTicks = 0; openInfo = ""; openAt = -1;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (openTicks !== 0 && openInfo === INFO) die("the ```" + INFO + " block is never closed");
|
|
776
|
+
if (blocks.length === 0) die("no top-level ```" + INFO + " block found — an accounted dispatch runs a contract");
|
|
777
|
+
if (blocks.length > 1) die(blocks.length + " ```" + INFO + " blocks found — a dispatch file carries exactly one");
|
|
778
|
+
let contract;
|
|
779
|
+
try { contract = JSON.parse(blocks[0]); } catch { die("the ```" + INFO + " block body is not valid JSON"); }
|
|
780
|
+
if (contract === null || typeof contract !== "object" || Array.isArray(contract)) die("the ```" + INFO + " block body must be ONE JSON object");
|
|
781
|
+
if (typeof contract.nonce !== "string" || !SAFE.test(contract.nonce)) die("the header carries no nonce in the safe grammar ([A-Za-z0-9._-]{1,64}) — an accounted dispatch is identified by the nonce the ledger copied from this header");
|
|
782
|
+
const ser = (v) => (Array.isArray(v)
|
|
783
|
+
? "[" + v.map(ser).join(",") + "]"
|
|
784
|
+
: (v !== null && typeof v === "object")
|
|
785
|
+
? "{" + Object.keys(v).sort().map((k) => JSON.stringify(k) + ":" + ser(v[k])).join(",") + "}"
|
|
786
|
+
: JSON.stringify(v));
|
|
787
|
+
process.stdout.write(contract.nonce + "\n" + createHash("sha256").update(ser(contract), "utf8").digest("hex"));
|
|
788
|
+
AW_JS
|
|
789
|
+
aw_require_core "$aw_js" "the contract-header reader" || return 1
|
|
790
|
+
printf '%s' "$1" | NODE_OPTIONS= node -e "$aw_js"
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if [[ -n "$aw_nonce" ]]; then
|
|
794
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
795
|
+
echo "error: a nonced dispatch mints an exec receipt, and its mint core needs 'node' on PATH — refusing before any spend." >&2
|
|
796
|
+
echo " (a nonce-less run needs no node at all)" >&2
|
|
797
|
+
exit 2
|
|
798
|
+
fi
|
|
799
|
+
# The digest binds the FILE the kit's `dispatch open --contract` read. A prompt arriving on stdin
|
|
800
|
+
# is not that file — there would be nothing to bind — so the accounted lane refuses it by name
|
|
801
|
+
# rather than digesting a stream nobody can re-read.
|
|
802
|
+
if [[ "$prompt_src" == "-" ]]; then
|
|
803
|
+
echo "error: a nonced dispatch runs a contract FILE, not stdin — contractDigest binds the dispatch file this run was handed, and '-' leaves nothing for the ledger to bind; pass the same file 'dispatch open --contract' read." >&2
|
|
804
|
+
exit 2
|
|
805
|
+
fi
|
|
806
|
+
# The resolved directory crosses back through a SENTINEL, not a bare command substitution: `$( )`
|
|
807
|
+
# strips every trailing newline, while the kit's own reader strips exactly ONE (git's terminator,
|
|
808
|
+
# flow-store-read.mjs:32-35). For a directory whose name ends in a newline the two sides would then
|
|
809
|
+
# resolve different paths, and the wrapper would write beside a ledger the kit never reads. Appending
|
|
810
|
+
# `x` inside the substitution makes the last byte a non-newline; only that final sentinel is
|
|
811
|
+
# stripped, so a directory whose own name ends in `x` still round-trips. A refusing resolver prints
|
|
812
|
+
# nothing on stdout, so the missing sentinel IS the failure signal.
|
|
813
|
+
aw_dir_raw=""
|
|
814
|
+
aw_dir_raw="$(aw_resolve_artifact_dir && printf x)" || true
|
|
815
|
+
if [[ "$aw_dir_raw" != *x ]]; then
|
|
816
|
+
echo " (the receipt has nowhere to land — refusing before any spend)" >&2
|
|
817
|
+
exit 2
|
|
818
|
+
fi
|
|
819
|
+
aw_artifact_dir="${aw_dir_raw%x}"
|
|
820
|
+
aw_header=""
|
|
821
|
+
if ! aw_header="$(aw_compute_contract_header "$task")"; then
|
|
822
|
+
echo "error: the prompt '$prompt_src' carries no readable dispatch contract (see above) — an accounted dispatch is refused before any spend." >&2
|
|
823
|
+
exit 2
|
|
824
|
+
fi
|
|
825
|
+
aw_contract_nonce="${aw_header%%$'\n'*}"
|
|
826
|
+
aw_contract_digest="${aw_header#*$'\n'}"
|
|
827
|
+
# The ledger's nonce IS the header's — `dispatch open` COPIES it — so a --nonce disagreeing with the
|
|
828
|
+
# header can only mint an artifact no return will ever absorb, after paying for the run. Refuse it
|
|
829
|
+
# here, where it is still free. Only the accounted lane compares: a nonce-LESS run of an ordinary
|
|
830
|
+
# plan file that happens to carry a contract block is none of this seam's business.
|
|
831
|
+
if [[ "$aw_contract_nonce" != "$aw_nonce" ]]; then
|
|
832
|
+
echo "error: the dispatch nonce '$aw_nonce' does not match the contract header's nonce '$aw_contract_nonce' — 'dispatch open' copies the nonce FROM the header, so this run would reserve an identity no return could absorb; nothing was reserved and nothing was spent." >&2
|
|
833
|
+
exit 2
|
|
834
|
+
fi
|
|
835
|
+
fi
|
|
836
|
+
|
|
556
837
|
# --- Enforced git-write boundary (physical shim file) ------------------------
|
|
557
838
|
# codex spawns `git` via execve, which BYPASSES exported shell functions — so the
|
|
558
839
|
# boundary MUST be a physical executable on PATH. We write a `git` shim into a temp
|
|
@@ -563,11 +844,20 @@ fi
|
|
|
563
844
|
# (codex could still call git by an absolute path — the contract + review are the
|
|
564
845
|
# real guard); it removes the trivial PATH-level write vector.
|
|
565
846
|
shim_dir=""
|
|
847
|
+
out_dir=""
|
|
566
848
|
out=""
|
|
567
849
|
trace=""
|
|
568
|
-
trap 'rm -rf "$shim_dir" 2>/dev/null; rm -f "$
|
|
850
|
+
trap 'rm -rf "$shim_dir" "$out_dir" 2>/dev/null; rm -f "$trace" 2>/dev/null; true' EXIT
|
|
569
851
|
shim_dir="$(mktemp -d)"
|
|
570
|
-
|
|
852
|
+
# `-o` gets a path that does NOT exist yet, inside a private directory. A pre-created file (the old
|
|
853
|
+
# `mktemp`) made "the delegate produced no final message" indistinguishable from "the delegate
|
|
854
|
+
# produced an EMPTY one" — both read back as zero bytes — so an accounted run could publish a
|
|
855
|
+
# `success` receipt describing a report the run never wrote, while its own stderr said there was no
|
|
856
|
+
# final message. The real CLI CREATES this path when it has something to write (probed against
|
|
857
|
+
# codex-cli directly: exit 0, the file appeared with the answer in it) — the fake accepts any argv, so
|
|
858
|
+
# only the real one could answer that.
|
|
859
|
+
out_dir="$(mktemp -d)"
|
|
860
|
+
out="$out_dir/final-message.txt"
|
|
571
861
|
trace="$(mktemp)"
|
|
572
862
|
{
|
|
573
863
|
printf '#!/usr/bin/env bash\n'
|
|
@@ -676,6 +966,16 @@ run_env=(env "PATH=$shim_dir:$PATH")
|
|
|
676
966
|
# absolute path the banner rendered from (banner and run never make independent conclusions).
|
|
677
967
|
timeout_bin="$(aw_resolve_timeout_bin)"
|
|
678
968
|
if [[ -z "$timeout_bin" ]]; then
|
|
969
|
+
# D8 — an ACCOUNTED dispatch that cannot be capped can never honour the terminal-exit rule: the
|
|
970
|
+
# ledger records capS + killGraceS and refuses a dispatch whose deadline is below their sum, so a
|
|
971
|
+
# run with no cap at all would put an unbounded run under a bounded deadline, and the waiter would
|
|
972
|
+
# report an expiry while the run was still legitimately alive. A nonce-LESS run keeps the existing
|
|
973
|
+
# behaviour exactly — warn loudly and run uncapped.
|
|
974
|
+
if [[ -n "$aw_nonce" ]]; then
|
|
975
|
+
echo "error: no 'timeout'/'gtimeout' on PATH — a nonced dispatch refuses to run uncapped (an accounted run that cannot be capped can never honour the terminal-exit rule)." >&2
|
|
976
|
+
echo " Install coreutils, or dispatch without a nonce; nothing was reserved and nothing was spent." >&2
|
|
977
|
+
exit 2
|
|
978
|
+
fi
|
|
679
979
|
echo "warning: no 'timeout'/'gtimeout' on PATH — running codex WITHOUT a hard wall-clock cap" >&2
|
|
680
980
|
echo " (install coreutils to enable CODEX_HARD_TIMEOUT=$CODEX_HARD_TIMEOUT)." >&2
|
|
681
981
|
fi
|
|
@@ -689,6 +989,99 @@ aw_session_label="fresh"
|
|
|
689
989
|
[[ -n "$resume_mode" ]] && aw_session_label="resume:$resume_id"
|
|
690
990
|
echo "exec posture: model=$CODEX_MODEL effort=$CODEX_EFFORT tier=${CODEX_SERVICE_TIER:-standard} sandbox=workspace-write session=$aw_session_label timeout=$aw_timeout_banner" >&2
|
|
691
991
|
|
|
992
|
+
# --- The PRE-SPEND reservation (delegation Plan 2 / D1) ------------------------
|
|
993
|
+
# Written immediately before the CLI runs — after EVERY preflight and after the posture banner, so a
|
|
994
|
+
# refused run leaves no reservation behind — and it is the reservation itself, not a separate lock,
|
|
995
|
+
# that makes one nonce mean one dispatch: the publish is atomic and NO-CLOBBER, so a second dispatch
|
|
996
|
+
# on the same nonce refuses BEFORE any spend. Both derived names are checked, because the kit's
|
|
997
|
+
# `dispatch open` refuses on either too: a leftover report would otherwise be absorbed later as this
|
|
998
|
+
# run's own evidence.
|
|
999
|
+
#
|
|
1000
|
+
# The mint core is codex-review.sh's finding-manifest shape: an UNPREDICTABLE sibling temp opened
|
|
1001
|
+
# "wx" (O_CREAT|O_EXCL — a planted node at the name refuses, and ONLY a temp we provably created is
|
|
1002
|
+
# ever unlinked), then a hard-link publish (atomic, no-clobber). The owner token is minted here and
|
|
1003
|
+
# returned on stdout: it is what the terminal publication verifies it still holds.
|
|
1004
|
+
aw_write_reservation() {
|
|
1005
|
+
local aw_js=""
|
|
1006
|
+
IFS= read -r -d '' aw_js <<'AW_JS' || true
|
|
1007
|
+
const fs = require("node:fs");
|
|
1008
|
+
const { join, dirname, basename } = require("node:path");
|
|
1009
|
+
const { randomBytes } = require("node:crypto");
|
|
1010
|
+
const [dir, backend, nonce, contractDigest, wrapperVersion, model, effort, tier, capS, killGraceS] = process.argv.slice(1);
|
|
1011
|
+
const name = (prefix, suffix) => join(dir, prefix + backend.length + "-" + backend + "-" + nonce + suffix);
|
|
1012
|
+
const receipt = name("agent-workflow-exec-receipt-", ".json");
|
|
1013
|
+
const report = name("agent-workflow-exec-report-", ".txt");
|
|
1014
|
+
for (const path of [receipt, report]) {
|
|
1015
|
+
try {
|
|
1016
|
+
fs.lstatSync(path);
|
|
1017
|
+
process.stderr.write("error: an exec artifact for this {backend, nonce} already exists at " + path + " — one nonce, one dispatch: refusing PRE-SPEND (remove it, or dispatch under a fresh nonce).\n");
|
|
1018
|
+
process.exit(3);
|
|
1019
|
+
} catch (err) {
|
|
1020
|
+
if (!err || err.code !== "ENOENT") {
|
|
1021
|
+
process.stderr.write("error: could not probe " + path + " (" + (err && err.code) + ") — refusing PRE-SPEND.\n");
|
|
1022
|
+
process.exit(3);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const owner = randomBytes(16).toString("hex");
|
|
1027
|
+
const bytes = Buffer.from(JSON.stringify({
|
|
1028
|
+
schema: 1,
|
|
1029
|
+
kind: "exec-receipt",
|
|
1030
|
+
state: "reserved",
|
|
1031
|
+
backend,
|
|
1032
|
+
nonce,
|
|
1033
|
+
owner,
|
|
1034
|
+
contractDigest,
|
|
1035
|
+
wrapperVersion,
|
|
1036
|
+
posture: { model, effort, tier: tier || null },
|
|
1037
|
+
capS: Number(capS),
|
|
1038
|
+
killGraceS: Number(killGraceS),
|
|
1039
|
+
sessionId: null,
|
|
1040
|
+
exitStatus: null,
|
|
1041
|
+
outcome: null,
|
|
1042
|
+
reportDigest: null,
|
|
1043
|
+
reportLength: null,
|
|
1044
|
+
timestamp: new Date().toISOString(),
|
|
1045
|
+
}) + "\n");
|
|
1046
|
+
let tmp = null;
|
|
1047
|
+
let code = 1;
|
|
1048
|
+
try {
|
|
1049
|
+
const candidate = join(dirname(receipt), "." + basename(receipt) + "." + process.pid + "." + randomBytes(8).toString("hex") + ".tmp");
|
|
1050
|
+
const fd = fs.openSync(candidate, "wx", 0o600);
|
|
1051
|
+
tmp = candidate;
|
|
1052
|
+
try { fs.writeFileSync(fd, bytes); } finally { fs.closeSync(fd); }
|
|
1053
|
+
fs.linkSync(tmp, receipt);
|
|
1054
|
+
code = 0;
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
process.stderr.write("error: could not publish the nonce reservation at " + receipt + " (" + (err && err.code) + ") — refusing PRE-SPEND.\n");
|
|
1057
|
+
code = err && err.code === "EEXIST" ? 3 : 1;
|
|
1058
|
+
}
|
|
1059
|
+
// An orphan temp never blocks the run — but it is never silent either: it sits in the store
|
|
1060
|
+
// directory looking like family state, and only the run that made it knows it is debris.
|
|
1061
|
+
if (tmp !== null) {
|
|
1062
|
+
try { fs.unlinkSync(tmp); } catch {
|
|
1063
|
+
process.stderr.write("warning: the reservation was published, but its temporary sibling could not be removed — orphan left at: " + tmp + " — remove it by hand.\n");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
if (code === 0) process.stdout.write(owner);
|
|
1067
|
+
process.exit(code);
|
|
1068
|
+
AW_JS
|
|
1069
|
+
aw_require_core "$aw_js" "the reservation mint core" || return 1
|
|
1070
|
+
NODE_OPTIONS= node -e "$aw_js" "$aw_artifact_dir" "$AW_RECEIPT_BACKEND" "$aw_nonce" "$aw_contract_digest" "$AW_BRIDGE_VERSION" \
|
|
1071
|
+
"$CODEX_MODEL" "$CODEX_EFFORT" "${CODEX_SERVICE_TIER:-}" "$CODEX_HARD_TIMEOUT" "$CODEX_KILL_GRACE_S"
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if [[ -n "$aw_nonce" ]]; then
|
|
1075
|
+
if ! aw_owner="$(aw_write_reservation)"; then
|
|
1076
|
+
echo " (the reservation is the pre-spend half of the dispatch identity — nothing was spent)" >&2
|
|
1077
|
+
exit 2
|
|
1078
|
+
fi
|
|
1079
|
+
if [[ -z "$aw_owner" ]]; then
|
|
1080
|
+
echo "error: the nonce reservation published no owner token — refusing PRE-SPEND rather than running a dispatch nothing can claim." >&2
|
|
1081
|
+
exit 2
|
|
1082
|
+
fi
|
|
1083
|
+
fi
|
|
1084
|
+
|
|
692
1085
|
# --- Nested-sandbox evidence scan: ONE entry point, TWO policies ---------------
|
|
693
1086
|
# The class: codex ships its OWN OS sandbox (bwrap); run nested inside a harness sandbox the FS is
|
|
694
1087
|
# read-only and codex's sandbox setup fails. The scan runs on EVERY completed run, not only a failed
|
|
@@ -814,38 +1207,237 @@ aw_scan_nested_sandbox() { # $1 = rc, $2 = trace path
|
|
|
814
1207
|
# including the evidence scan — is genuinely shared instead of mode-dependent.
|
|
815
1208
|
set +e
|
|
816
1209
|
if [[ -n "$timeout_bin" ]]; then
|
|
817
|
-
printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after=
|
|
1210
|
+
printf '%s' "$full_prompt" | "${run_env[@]}" "$timeout_bin" --kill-after="${CODEX_KILL_GRACE_S}s" "$CODEX_HARD_TIMEOUT" "${codex_cmd[@]}" >"$trace" 2>&1
|
|
818
1211
|
else
|
|
819
1212
|
printf '%s' "$full_prompt" | "${run_env[@]}" "${codex_cmd[@]}" >"$trace" 2>&1
|
|
820
1213
|
fi
|
|
821
1214
|
rc=$?
|
|
822
1215
|
set -e
|
|
823
1216
|
|
|
1217
|
+
# --- The session id, captured BEFORE any outcome branching (D3) ----------------
|
|
1218
|
+
# It used to be extracted only on the SUCCESS path, after the timeout and failure exits, so a run
|
|
1219
|
+
# that failed or timed out recorded no identity at all — and the session id is exactly the handle an
|
|
1220
|
+
# operator needs to find such a run in the backend's own history. In resume mode it is the resume id,
|
|
1221
|
+
# validated pre-spend. A fresh run that emitted no thread.started leaves it empty, which is the
|
|
1222
|
+
# receipt's `missing-identity` outcome, never a guess.
|
|
1223
|
+
session_id=""
|
|
1224
|
+
if [[ -n "$resume_mode" ]]; then
|
|
1225
|
+
session_id="$resume_id"
|
|
1226
|
+
else
|
|
1227
|
+
session_id="$(grep -m1 '"type":"thread.started"' "$trace" 2>/dev/null \
|
|
1228
|
+
| grep -o '"thread_id":"[^"]*"' | cut -d'"' -f4 || true)"
|
|
1229
|
+
fi
|
|
1230
|
+
|
|
1231
|
+
# --- The TERMINAL exec receipt, fail-closed (D1 / 3.1.d) -----------------------
|
|
1232
|
+
# Publication ORDER, and it is the whole point: verify the reservation is still OURS (so a tampered
|
|
1233
|
+
# reservation costs no overwritten artifact — nothing at all is published), write the REPORT
|
|
1234
|
+
# atomically, re-verify the owner, then REPLACE the reservation with the terminal receipt. An
|
|
1235
|
+
# artifact that has arrived therefore always has a complete report behind it.
|
|
1236
|
+
#
|
|
1237
|
+
# FAIL-CLOSED, deliberately unlike codex-review.sh's warn-only receipt (a missing review receipt only
|
|
1238
|
+
# fails a checker; a missing exec receipt leaves an EDITED tree with no accounting). Two statuses:
|
|
1239
|
+
# 70 the reservation could not be verified as this run's — NOTHING was published; 71 a publication
|
|
1240
|
+
# could not complete, and the message names WHICH artifact stopped. In both the RESERVATION survives,
|
|
1241
|
+
# so the recovery is the same door — `dispatch return --no-receipt` — while what the kit then reads
|
|
1242
|
+
# differs and stays two separate lanes: a failed REPORT write leaves the reservation alone (the
|
|
1243
|
+
# absorb records reportLength 0 and the metric is ineligible by the name `empty-report`), a failed
|
|
1244
|
+
# TERMINAL write leaves the published report beside it (the absorb reads it, report-if-present).
|
|
1245
|
+
aw_publish_terminal_receipt() {
|
|
1246
|
+
local aw_js=""
|
|
1247
|
+
IFS= read -r -d '' aw_js <<'AW_JS' || true
|
|
1248
|
+
const fs = require("node:fs");
|
|
1249
|
+
const { join, dirname, basename } = require("node:path");
|
|
1250
|
+
const { createHash, randomBytes } = require("node:crypto");
|
|
1251
|
+
const [dir, backend, nonce, owner, exitStatusRaw, sessionIdRaw, reportSrc] = process.argv.slice(1);
|
|
1252
|
+
const name = (prefix, suffix) => join(dir, prefix + backend.length + "-" + backend + "-" + nonce + suffix);
|
|
1253
|
+
const receipt = name("agent-workflow-exec-receipt-", ".json");
|
|
1254
|
+
const report = name("agent-workflow-exec-report-", ".txt");
|
|
1255
|
+
const exitStatus = Number(exitStatusRaw);
|
|
1256
|
+
const sessionId = sessionIdRaw === "" ? null : sessionIdRaw;
|
|
1257
|
+
// D3, the wrapper OUTCOME SUBSET — the only three a run can prove about itself.
|
|
1258
|
+
const outcome = exitStatus !== 0 ? "transport-failure" : (sessionId === null ? "missing-identity" : "success");
|
|
1259
|
+
const die = (code, message) => { process.stderr.write("error: " + message + "\n"); process.exit(code); };
|
|
1260
|
+
// The reservation is re-read and re-verified BEFORE anything at all is published: a foreign owner,
|
|
1261
|
+
// a missing artifact or a terminal one means this run does not hold the nonce, and the honest answer
|
|
1262
|
+
// is to publish NOTHING — neither the report nor the receipt.
|
|
1263
|
+
// `code` differs by WHEN the check runs: before any publication a refusal is total (4), after the
|
|
1264
|
+
// report is on disk it is not (6) — and the message must not claim otherwise.
|
|
1265
|
+
// The publication consequence is identical for every cause, but the DIAGNOSIS is not: "another run
|
|
1266
|
+
// owns this" is a specific claim, and asserting it over a malformed file, a vanished one or an I/O
|
|
1267
|
+
// error sends the operator hunting for a second dispatch that never existed. Each cause is named.
|
|
1268
|
+
const claim = (when, code) => {
|
|
1269
|
+
let raw;
|
|
1270
|
+
try {
|
|
1271
|
+
raw = fs.readFileSync(receipt, "utf8");
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
const why = err && err.code === "ENOENT"
|
|
1274
|
+
? "it is GONE — something removed it after this run published it"
|
|
1275
|
+
: "it could not be read (" + (err && err.code ? err.code : "unknown error") + ")";
|
|
1276
|
+
die(code, "the nonce reservation at " + receipt + " cannot be verified " + when + ": " + why);
|
|
1277
|
+
}
|
|
1278
|
+
let held;
|
|
1279
|
+
try {
|
|
1280
|
+
held = JSON.parse(raw);
|
|
1281
|
+
} catch {
|
|
1282
|
+
die(code, "the nonce reservation at " + receipt + " cannot be verified " + when + ": it is MALFORMED — the bytes there are not valid JSON, so nothing about its ownership is readable");
|
|
1283
|
+
}
|
|
1284
|
+
if (held === null || typeof held !== "object" || Array.isArray(held)) {
|
|
1285
|
+
die(code, "the nonce reservation at " + receipt + " cannot be verified " + when + ": its body is not a JSON object");
|
|
1286
|
+
}
|
|
1287
|
+
if (held.state !== "reserved") {
|
|
1288
|
+
die(code, "the artifact at " + receipt + " is not a RESERVATION " + when + ": its state is " + JSON.stringify(held.state) + " — this run holds no claim on a receipt something else has already finished");
|
|
1289
|
+
}
|
|
1290
|
+
if (held.owner !== owner) {
|
|
1291
|
+
die(code, "the artifact at " + receipt + " is not this run’s reservation " + when + ": its owner token belongs to ANOTHER run");
|
|
1292
|
+
}
|
|
1293
|
+
return held;
|
|
1294
|
+
};
|
|
1295
|
+
const held = claim("before any publication", 4);
|
|
1296
|
+
// THREE cases, decided by lstat BEFORE any read — because `readFileSync` FOLLOWS a symlink, so a
|
|
1297
|
+
// DANGLING one at the capture path reports ENOENT and would be recorded as a clean empty report on a
|
|
1298
|
+
// `success` receipt. ABSENT (no entry at all) is the legitimate "the delegate wrote no final message"
|
|
1299
|
+
// case: the sha256 of no bytes, exactly what the receipt contract expresses. A REGULAR file is read.
|
|
1300
|
+
// Anything else PRESENT — a symlink, a directory, a device — is a FAILED probe, never an absence.
|
|
1301
|
+
// Type only: the capture lives in a directory this run created with `mktemp -d`, so a mode or
|
|
1302
|
+
// ownership check would be theatre against a threat model this wrapper already declares out of scope,
|
|
1303
|
+
// while the type check catches the real accident. Same discipline as the kit's readRegularFileNoFollow.
|
|
1304
|
+
let bytes;
|
|
1305
|
+
if (reportSrc === "") {
|
|
1306
|
+
bytes = Buffer.alloc(0);
|
|
1307
|
+
} else {
|
|
1308
|
+
let stat = null;
|
|
1309
|
+
try {
|
|
1310
|
+
stat = fs.lstatSync(reportSrc);
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
if (!err || err.code !== "ENOENT") {
|
|
1313
|
+
die(5, "the delegate’s final message at " + reportSrc + " could not be probed (" + (err && err.code) + ") — a failed probe is not an absence, and recording it as an empty report would put bytes in the receipt that the run never produced");
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
if (stat === null) {
|
|
1317
|
+
bytes = Buffer.alloc(0);
|
|
1318
|
+
} else if (!stat.isFile()) {
|
|
1319
|
+
const kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "non-regular entry";
|
|
1320
|
+
die(5, "the delegate’s final message at " + reportSrc + " is a " + kind + ", not a regular file — a CORRUPT capture is a FAILED probe, not an absent one (a dangling symlink reads as ENOENT and would otherwise be recorded as a clean empty report)");
|
|
1321
|
+
} else {
|
|
1322
|
+
try {
|
|
1323
|
+
bytes = fs.readFileSync(reportSrc);
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
die(5, "the delegate’s final message at " + reportSrc + " could not be read (" + (err && err.code) + ") — an unreadable capture is a FAILED probe, not an absent one");
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
// Both publications are atomic: an unpredictable sibling temp, then a rename that either replaces
|
|
1330
|
+
// the target whole or leaves it untouched. A reader never sees half a report or half a receipt.
|
|
1331
|
+
const publishAtomically = (path, payload, code, what) => {
|
|
1332
|
+
let tmp = null;
|
|
1333
|
+
try {
|
|
1334
|
+
const candidate = join(dirname(path), "." + basename(path) + "." + process.pid + "." + randomBytes(8).toString("hex") + ".tmp");
|
|
1335
|
+
const fd = fs.openSync(candidate, "wx", 0o600);
|
|
1336
|
+
tmp = candidate;
|
|
1337
|
+
try { fs.writeFileSync(fd, payload); } finally { fs.closeSync(fd); }
|
|
1338
|
+
fs.renameSync(tmp, path);
|
|
1339
|
+
tmp = null;
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
if (tmp !== null) { try { fs.unlinkSync(tmp); } catch { /* an orphan temp never masks the real failure */ } }
|
|
1342
|
+
die(code, what + " could not be published at " + path + " (" + (err && err.code) + ")");
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
publishAtomically(report, bytes, 5, "the delegate’s report");
|
|
1346
|
+
claim("immediately before the terminal replace", 6);
|
|
1347
|
+
publishAtomically(receipt, Buffer.from(JSON.stringify({
|
|
1348
|
+
schema: held.schema,
|
|
1349
|
+
kind: held.kind,
|
|
1350
|
+
state: "terminal",
|
|
1351
|
+
backend: held.backend,
|
|
1352
|
+
nonce: held.nonce,
|
|
1353
|
+
owner: held.owner,
|
|
1354
|
+
contractDigest: held.contractDigest,
|
|
1355
|
+
wrapperVersion: held.wrapperVersion,
|
|
1356
|
+
posture: held.posture,
|
|
1357
|
+
capS: held.capS,
|
|
1358
|
+
killGraceS: held.killGraceS,
|
|
1359
|
+
sessionId,
|
|
1360
|
+
exitStatus,
|
|
1361
|
+
outcome,
|
|
1362
|
+
reportDigest: createHash("sha256").update(bytes).digest("hex"),
|
|
1363
|
+
reportLength: bytes.length,
|
|
1364
|
+
timestamp: new Date().toISOString(),
|
|
1365
|
+
}) + "\n"), 6, "the terminal exec receipt");
|
|
1366
|
+
|
|
1367
|
+
process.stdout.write(outcome);
|
|
1368
|
+
AW_JS
|
|
1369
|
+
aw_require_core "$aw_js" "the terminal publication core" || return 7
|
|
1370
|
+
NODE_OPTIONS= node -e "$aw_js" "$aw_artifact_dir" "$AW_RECEIPT_BACKEND" "$aw_nonce" "$aw_owner" "$1" "$session_id" "$out"
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
# The failure diagnostics print BEFORE the receipt is published, and the exits happen AFTER: a
|
|
1374
|
+
# publication that fails must not swallow the trace tail that explains WHY the run failed. Both are
|
|
1375
|
+
# explicitly NON-FATAL: under `set -e` a diagnostic that cannot run (no `tail` on PATH, an unreadable
|
|
1376
|
+
# trace) would kill the wrapper BEFORE the terminal publication, stranding the reservation with no
|
|
1377
|
+
# recovery status at all — the diagnostics are the least important thing here and must never be the
|
|
1378
|
+
# thing that decides whether the run gets accounted.
|
|
824
1379
|
if [[ $rc -eq 124 || $rc -eq 137 ]]; then
|
|
825
1380
|
echo "error: codex exec exceeded the hard cap CODEX_HARD_TIMEOUT=${CODEX_HARD_TIMEOUT}s and was terminated." >&2
|
|
826
1381
|
echo " Raise CODEX_HARD_TIMEOUT for a known-healthy slow run, or narrow the task, then re-dispatch." >&2
|
|
827
|
-
|
|
1382
|
+
elif [[ $rc -ne 0 ]]; then
|
|
1383
|
+
echo "error: codex exec failed (exit $rc). Last lines of the run trace:" >&2
|
|
1384
|
+
tail -n 40 "$trace" >&2 || true
|
|
1385
|
+
aw_scan_nested_sandbox "$rc" "$trace" || true
|
|
828
1386
|
fi
|
|
1387
|
+
|
|
1388
|
+
if [[ -n "$aw_nonce" ]]; then
|
|
1389
|
+
aw_outcome=""
|
|
1390
|
+
aw_publish_rc=0
|
|
1391
|
+
aw_outcome="$(aw_publish_terminal_receipt "$rc")" || aw_publish_rc=$?
|
|
1392
|
+
if [[ $aw_publish_rc -ne 0 ]]; then
|
|
1393
|
+
echo " The delegate ran and the working tree may be PARTIALLY EDITED — treat it as dirtied, not as" >&2
|
|
1394
|
+
echo " untouched." >&2
|
|
1395
|
+
# What SURVIVES differs per lane, and the message says only what this run can still prove. After
|
|
1396
|
+
# the report is on disk the reservation's fate is no longer known to us (it was already not ours,
|
|
1397
|
+
# or the replace failed for a reason that may also have removed it) — so that lane never claims
|
|
1398
|
+
# "the reservation stands".
|
|
1399
|
+
case "$aw_publish_rc" in
|
|
1400
|
+
7)
|
|
1401
|
+
echo " The publisher never RAN — nothing was published. The reservation is untouched; absorb with:" >&2
|
|
1402
|
+
echo " dispatch return --nonce $aw_nonce --no-receipt --exit-status $rc --outcome <o>" >&2
|
|
1403
|
+
exit 71
|
|
1404
|
+
;;
|
|
1405
|
+
4)
|
|
1406
|
+
echo " NOTHING was published — not the report, not the receipt. The artifact at the receipt path" >&2
|
|
1407
|
+
echo " is not this run's reservation, so establish what replaced it BEFORE absorbing anything:" >&2
|
|
1408
|
+
echo " a --no-receipt absorb would source its posture from another run's artifact." >&2
|
|
1409
|
+
exit 70
|
|
1410
|
+
;;
|
|
1411
|
+
5)
|
|
1412
|
+
echo " Nothing beyond the reservation was published. Absorb the thread with:" >&2
|
|
1413
|
+
echo " dispatch return --nonce $aw_nonce --no-receipt --exit-status $rc --outcome <o>" >&2
|
|
1414
|
+
echo " (with no report on disk it records reportLength 0 — ineligible by the name empty-report)" >&2
|
|
1415
|
+
exit 71
|
|
1416
|
+
;;
|
|
1417
|
+
*)
|
|
1418
|
+
echo " The REPORT is published; the terminal receipt was NOT completed. Absorb the thread with:" >&2
|
|
1419
|
+
echo " dispatch return --nonce $aw_nonce --no-receipt --exit-status $rc --outcome <o>" >&2
|
|
1420
|
+
echo " (it reads the published report, report-if-present), and check the receipt path by hand." >&2
|
|
1421
|
+
exit 71
|
|
1422
|
+
;;
|
|
1423
|
+
esac
|
|
1424
|
+
fi
|
|
1425
|
+
echo "exec receipt: nonce=$aw_nonce outcome=$aw_outcome exit=$rc session=${session_id:-none} → $aw_artifact_dir" >&2
|
|
1426
|
+
fi
|
|
1427
|
+
|
|
829
1428
|
if [[ $rc -ne 0 ]]; then
|
|
830
|
-
echo "error: codex exec failed (exit $rc). Last lines of the run trace:" >&2
|
|
831
|
-
tail -n 40 "$trace" >&2
|
|
832
|
-
aw_scan_nested_sandbox "$rc" "$trace"
|
|
833
1429
|
exit $rc
|
|
834
1430
|
fi
|
|
835
1431
|
|
|
836
|
-
# Success:
|
|
837
|
-
#
|
|
838
|
-
#
|
|
839
|
-
if [[ -z "$resume_mode" ]]; then
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
sidecar="${CODEX_SESSION_FILE:-$PWD/.codex-last-session}"
|
|
844
|
-
if ! printf '%s\n' "$session_id" >"$sidecar" 2>/dev/null; then
|
|
845
|
-
echo "warning: could not write the session sidecar '$sidecar' — 'codex-exec --resume-last' won't find this id." >&2
|
|
846
|
-
fi
|
|
847
|
-
echo "session: $session_id" >&2
|
|
1432
|
+
# Success: record the session id in the sidecar (NORMAL mode only — a resume continues the same
|
|
1433
|
+
# session) BEFORE the trap removes the trace, so an iterative resume (codex-exec --resume-last) can
|
|
1434
|
+
# find it. The id itself was captured above, for every outcome; only the sidecar is success-scoped.
|
|
1435
|
+
if [[ -z "$resume_mode" && -n "$session_id" ]]; then
|
|
1436
|
+
sidecar="${CODEX_SESSION_FILE:-$PWD/.codex-last-session}"
|
|
1437
|
+
if ! printf '%s\n' "$session_id" >"$sidecar" 2>/dev/null; then
|
|
1438
|
+
echo "warning: could not write the session sidecar '$sidecar' — 'codex-exec --resume-last' won't find this id." >&2
|
|
848
1439
|
fi
|
|
1440
|
+
echo "session: $session_id" >&2
|
|
849
1441
|
fi
|
|
850
1442
|
|
|
851
1443
|
if [[ -f "$out" && -s "$out" ]]; then
|