@junghanacs/entwurf 0.22.0 → 0.23.1
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/AGENTS.md +4 -1
- package/BASELINE.md +1 -0
- package/CHANGELOG.md +514 -0
- package/DELIVERY.md +10 -2
- package/README.md +43 -4
- package/VERIFY.md +47 -0
- package/docs/acp-backend-rail.md +96 -17
- package/docs/setup-clean-host.md +11 -1
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +39 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +2 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-declaration.js +609 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +95 -56
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +40 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +6 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peer-observe.js +67 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +9 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +24 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +423 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +98 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +1018 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-placement.js +213 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +46 -273
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +109 -0
- package/mcp/entwurf-bridge/src/index.ts +41 -41
- package/mcp/entwurf-bridge/tsconfig.build.json +1 -0
- package/package.json +9 -8
- package/pi-extensions/entwurf-control.ts +91 -52
- package/pi-extensions/lib/acp/acp-client.ts +3 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +3 -3
- package/pi-extensions/lib/acp/backend.ts +4 -4
- package/pi-extensions/lib/acp/event-mapper.ts +5 -5
- package/pi-extensions/lib/acp/tool-surface.ts +2 -1
- package/pi-extensions/lib/codex-declaration.js +612 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +107 -51
- package/pi-extensions/lib/entwurf-control-rpc.ts +46 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +13 -2
- package/pi-extensions/lib/entwurf-facts.ts +14 -1
- package/pi-extensions/lib/entwurf-peer-observe.ts +72 -2
- package/pi-extensions/lib/entwurf-peers-render.ts +9 -1
- package/pi-extensions/lib/entwurf-self-address.ts +33 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +2 -2
- package/pi-extensions/lib/entwurf-v2-lock.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-runner.ts +1 -1
- package/pi-extensions/lib/fresh-call-composition.ts +493 -0
- package/pi-extensions/lib/fresh-call-dispatch.ts +142 -0
- package/pi-extensions/lib/herdr-fresh-call.ts +1378 -0
- package/pi-extensions/lib/herdr-placement.ts +276 -0
- package/pi-extensions/lib/mux-fresh-call.ts +95 -301
- package/run.sh +588 -204
- package/scripts/check-acp-usage-accounting.ts +9 -9
- package/scripts/check-codex-birth-hook.ts +187 -0
- package/scripts/check-entwurf-control-rpc.ts +138 -0
- package/scripts/check-entwurf-fact-provider.ts +4 -2
- package/scripts/check-entwurf-peers-surface.ts +3 -1
- package/scripts/check-entwurf-self-address.ts +68 -0
- package/scripts/check-fresh-call-dispatch.ts +301 -0
- package/scripts/check-gate-qualification.ts +17 -4
- package/scripts/check-herdr-activation.ts +1293 -0
- package/scripts/check-herdr-fresh-call.ts +1250 -0
- package/scripts/check-herdr-placement.ts +383 -0
- package/scripts/check-herdr-plugin-build.ts +842 -0
- package/scripts/check-herdr-plugin-profile.ts +371 -0
- package/scripts/check-herdr-plugin.ts +523 -0
- package/scripts/check-herdr-runtime-bootstrap.ts +1264 -0
- package/scripts/check-herdr-sandbox.ts +456 -0
- package/scripts/check-herdr-supply.ts +184 -0
- package/scripts/check-mux-launcher-fence.ts +138 -0
- package/scripts/check-peer-facts.ts +331 -0
- package/scripts/check-pi-launch.ts +358 -0
- package/scripts/check-release-gate-outcomes.ts +19 -0
- package/scripts/check-typing-call-fence.ts +296 -0
- package/scripts/codex-birth-doctor.sh +159 -16
- package/scripts/codex-birth-install.sh +178 -72
- package/scripts/codex-birth-uninstall.sh +162 -7
- package/scripts/fixtures/herdr-supply.json +29 -0
- package/scripts/herdr-activation.mjs +536 -0
- package/scripts/herdr-plugin-activate.mjs +270 -0
- package/scripts/herdr-plugin-deactivate.mjs +193 -0
- package/scripts/herdr-runtime.mjs +1203 -0
- package/scripts/install-herdr-ci.sh +96 -0
- package/scripts/inventory-verification-surface.ts +86 -10
- package/scripts/lib/codex-fresh-live-protocol.ts +2 -2
- package/scripts/lib/tree-digest.ts +97 -0
- package/scripts/meta-bridge-install.sh +19 -2
- package/scripts/meta-bridge-state.py +167 -11
- package/scripts/mutants/codex-caller-seat.json +6 -8
- package/scripts/mutants/codex-native.json +195 -29
- package/scripts/mutants/control-socket-disconnect.json +57 -0
- package/scripts/mutants/fresh-call-dispatch.json +154 -0
- package/scripts/mutants/herdr-activation.json +305 -0
- package/scripts/mutants/herdr-fresh-call.json +448 -0
- package/scripts/mutants/herdr-placement.json +161 -0
- package/scripts/mutants/herdr-plugin-build.json +160 -0
- package/scripts/mutants/herdr-plugin-profile.json +202 -0
- package/scripts/mutants/herdr-plugin.json +133 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +411 -0
- package/scripts/mutants/herdr-supply.json +133 -0
- package/scripts/mutants/meta-hook-session-switch.json +2 -2
- package/scripts/mutants/mux-fresh-call.json +104 -17
- package/scripts/mutants/mux-launcher-fence.json +13 -0
- package/scripts/mutants/omp-fresh.json +4 -4
- package/scripts/mutants/peer-facts.json +98 -0
- package/scripts/mutants/pi-launch.json +113 -0
- package/scripts/mutants/resume-args.json +6 -6
- package/scripts/mutants/self-address.json +33 -0
- package/scripts/mutants/typing-call-fence.json +17 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +17 -17
- package/scripts/peer-facts.ts +120 -0
- package/scripts/register-pi-package.py +8 -0
- package/scripts/register-pi-provider.py +88 -12
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-codex-birth.sh +281 -12
- package/scripts/smoke-entwurf-v2-matrix-live.ts +1 -1
- package/scripts/smoke-herdr-fresh-call-live.ts +1027 -0
- package/scripts/smoke-herdr-plugin-build-live.ts +543 -0
- package/scripts/smoke-herdr-raw-install-live.sh +849 -0
- package/scripts/tsconfig.json +5 -0
- package/scripts/check-entwurf-facts.ts +0 -385
- package/scripts/check-entwurf-resume-args.ts +0 -122
- package/scripts/check-entwurf-v2-decider.ts +0 -657
- package/scripts/check-entwurf-v2-lock.ts +0 -371
- package/scripts/check-entwurf-v2-mailbox.ts +0 -261
- package/scripts/check-entwurf-v2-matrix.ts +0 -401
- package/scripts/check-entwurf-v2-native-push.ts +0 -221
- package/scripts/check-entwurf-v2-release.ts +0 -151
- package/scripts/check-entwurf-v2-runner.ts +0 -448
- package/scripts/check-entwurf-v2-send-fallback.ts +0 -350
- package/scripts/check-entwurf-v2-send.ts +0 -458
- package/scripts/check-entwurf-v2-visible-resume.ts +0 -445
- package/scripts/check-socket-discovery.ts +0 -401
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# Three paths, all the operator's own:
|
|
5
5
|
#
|
|
6
|
-
# $CODEX_HOME/hooks.json ONE
|
|
6
|
+
# $CODEX_HOME/hooks.json ONE SessionStart group inside a SHARED file
|
|
7
7
|
# ${XDG_DATA_HOME:-~/.local/share}/entwurf/codex-birth/helper/
|
|
8
8
|
# launcher + the TS payload closure
|
|
9
9
|
# .../entwurf/codex-birth/install-state.json the digest inventory that licenses removal
|
|
@@ -23,6 +23,16 @@
|
|
|
23
23
|
# give; forging it in a file we already own would be the one shortcut that turns a security
|
|
24
24
|
# prompt into a silent install.
|
|
25
25
|
#
|
|
26
|
+
# WHAT THIS UNIT OWNS IN hooks.json, AND WHAT IT DOES NOT (#117). Until 0.22.0 this unit owned
|
|
27
|
+
# that file WHOLE: it refused any file it had not written and recorded a whole-file sha256. That
|
|
28
|
+
# was true only while entwurf was the only thing declaring a Codex hook. Herdr's official Codex
|
|
29
|
+
# integration appends its own `SessionStart` group and the vendor keeps running both, because
|
|
30
|
+
# `[source]` discovery.rs:664-665 keys trust by `<path>:<event>:<group_idx>:<handler_idx>` — trust
|
|
31
|
+
# is DECLARATION-scoped and always was. So this unit now owns exactly one group holding exactly
|
|
32
|
+
# one handler whose command is our quoted launcher path. Every other group is FOREIGN: reported,
|
|
33
|
+
# never certified, never rewritten. Adding and removing our group is a TEXT SPLICE
|
|
34
|
+
# (pi-extensions/lib/codex-declaration.js), so a neighbour's bytes come through untouched.
|
|
35
|
+
#
|
|
26
36
|
# THE TRUST IDENTITY IS THE LAUNCHER PATH. `[source]` discovery.rs:775-792 hashes a
|
|
27
37
|
# normalized identity of (event name + matcher group + the single handler) — so `type`, the
|
|
28
38
|
# quoted absolute `command`, `timeout` and the absent `matcher` ARE the operator's approval,
|
|
@@ -79,6 +89,9 @@ case "$NODE_BIN" in /*) : ;; *) die "resolved node path is not absolute: $NODE_B
|
|
|
79
89
|
NODE_MAJOR="$("$NODE_BIN" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
|
|
80
90
|
[ "$NODE_MAJOR" -ge 24 ] 2>/dev/null || die "node >= 24 is required (the payload is stripped, not compiled); $NODE_BIN reports major '$NODE_MAJOR'."
|
|
81
91
|
|
|
92
|
+
DECLARATION_LIB="$REPO/pi-extensions/lib/codex-declaration.js"
|
|
93
|
+
[ -f "$DECLARATION_LIB" ] || die "the declaration leaf is missing: $DECLARATION_LIB (reinstall the package, or check out the repo completely)."
|
|
94
|
+
|
|
82
95
|
PAYLOAD_SRC="$REPO/pi-extensions/$PAYLOAD_NAME"
|
|
83
96
|
LIB_SRC="$REPO/pi-extensions/lib/meta-session.ts"
|
|
84
97
|
CODEX_CLIENT_SRC="$REPO/pi-extensions/lib/native-push/codex-ws-client.ts"
|
|
@@ -108,6 +121,38 @@ safe_own_dir() { # $1 = dir, $2 = label
|
|
|
108
121
|
[ $((8#$mode & 0022)) -eq 0 ] || die "$2 ($1) is group/world-writable (mode $mode), so its contents are not provably ours. Nothing written."
|
|
109
122
|
}
|
|
110
123
|
|
|
124
|
+
# The SHARED file's own ownership, judged by the SHARED predicate (sol B3, 2026-09-18).
|
|
125
|
+
# `classifyOwnedPath` lives in codex-declaration.js because four surfaces decide about this one
|
|
126
|
+
# file — this installer, the inverse, the doctor and the fresh-call preflight — and they used to
|
|
127
|
+
# decide differently: the two shells asked only symlink-and-regular, preflight asked owner and mode
|
|
128
|
+
# too, and the doctor asked neither. An install could therefore succeed and a doctor go green on
|
|
129
|
+
# bytes every Codex launch then refused as `codex-birth-unit-missing`.
|
|
130
|
+
#
|
|
131
|
+
# We still do NOT chmod what we share: a neighbour's mode is theirs, and the APPEND below carries
|
|
132
|
+
# it over untouched. Writing into a file anyone else may rewrite is the different question, and the
|
|
133
|
+
# answer to that one is a refusal, because a receipt cannot bind bytes somebody else controls.
|
|
134
|
+
owned_path_verdict() { # $1 = path, $2 = file|directory
|
|
135
|
+
"$NODE_BIN" -e '
|
|
136
|
+
import(process.argv[1]).then((m) => {
|
|
137
|
+
const fs = require("node:fs");
|
|
138
|
+
process.stdout.write(m.classifyOwnedPath(m.statOwnedPath(fs, process.argv[2]), process.getuid(), { kind: process.argv[3] }));
|
|
139
|
+
}).catch((err) => { process.stderr.write(String(err && err.message ? err.message : err)); process.exit(1); });
|
|
140
|
+
' "$DECLARATION_LIB" "$1" "${2:-file}"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
require_writable_share() { # $1 = path, $2 = label — `missing` is fine; we are about to create it
|
|
144
|
+
local verdict
|
|
145
|
+
verdict="$(owned_path_verdict "$1" file)" || die "$2 ($1) could not be judged: $verdict. Nothing written."
|
|
146
|
+
case "$verdict" in
|
|
147
|
+
ok|missing) : ;;
|
|
148
|
+
symlink) die "$2 ($1) is a SYMLINK. This unit owns that path as ONE regular file and never publishes through a link. Refusing; nothing written." ;;
|
|
149
|
+
not-regular) die "$2 ($1) exists and is not a regular file. Refusing; nothing written." ;;
|
|
150
|
+
foreign-uid) die "$2 ($1) is owned by another user, so nothing this unit certifies in it can bind what it will say next. Refusing; nothing written." ;;
|
|
151
|
+
writable-by-others) die "$2 ($1) is group/world-writable, so anything certified in it can be rewritten by someone else afterwards. Its mode is not ours to change either. Refusing; nothing written." ;;
|
|
152
|
+
*) die "$2 ($1) returned an unknown ownership verdict '$verdict'. Refusing; nothing written." ;;
|
|
153
|
+
esac
|
|
154
|
+
}
|
|
155
|
+
|
|
111
156
|
# ── ownership preflight — READ-ONLY, and every refusal is zero-write ─────────
|
|
112
157
|
# Three different facts, three different refusals, none of them silent:
|
|
113
158
|
# a foreign hooks.json (no state behind it), a hand-edited one (state disagrees), and a
|
|
@@ -120,7 +165,7 @@ safe_own_dir() { # $1 = dir, $2 = label
|
|
|
120
165
|
[ -e "$UNIT_ROOT" ] && safe_own_dir "$UNIT_ROOT" "the unit root (it holds the ownership state)"
|
|
121
166
|
|
|
122
167
|
STATE_PRESENT=0
|
|
123
|
-
|
|
168
|
+
RECORDED_SCHEMA=""
|
|
124
169
|
if [ -e "$STATE_FILE" ] || [ -L "$STATE_FILE" ]; then
|
|
125
170
|
[ -L "$STATE_FILE" ] && die "$STATE_FILE is a symlink — refusing to take replacement authority from a link. Nothing written."
|
|
126
171
|
[ -f "$STATE_FILE" ] || die "$STATE_FILE is not a regular file. Nothing written."
|
|
@@ -131,14 +176,19 @@ if [ -e "$STATE_FILE" ] || [ -L "$STATE_FILE" ]; then
|
|
|
131
176
|
STATE_READ="$("$NODE_BIN" -e '
|
|
132
177
|
const s = JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"));
|
|
133
178
|
const path = require("node:path");
|
|
134
|
-
|
|
179
|
+
// v1 is READ HERE AND NOWHERE ELSE, and only to be SUPERSEDED. It recorded a whole-file
|
|
180
|
+
// hooksSha256 this installer no longer consults: whether hooks.json may be edited is now
|
|
181
|
+
// decided by reading the live document and selecting our own declaration, not by a receipt
|
|
182
|
+
// from a generation that claimed the whole file. What v1 still holds that this run needs is
|
|
183
|
+
// the CLOSURE inventory, whose shape is identical in both schemas. Every other reader —
|
|
184
|
+
// uninstall, doctor, preflight — refuses v1 by name and sends the operator here.
|
|
185
|
+
if (s.schema !== "codex-birth-install-state/v2" && s.schema !== "codex-birth-install-state/v1") throw new Error("foreign state schema " + JSON.stringify(s.schema));
|
|
135
186
|
// A state this unit never wrote — or one left in a status it does not define — is not a
|
|
136
187
|
// receipt, however well its digests happen to match. `publishing` is the one interrupted
|
|
137
188
|
// generation this installer knows how to finish.
|
|
138
189
|
if (s.status !== "installed" && s.status !== "publishing") throw new Error("unknown state status " + JSON.stringify(s.status));
|
|
139
190
|
if (s.hooksFile !== process.argv[2]) throw new Error("state is bound to " + s.hooksFile + ", not " + process.argv[2]);
|
|
140
191
|
if (s.helperDir !== process.argv[3]) throw new Error("state is bound to helper dir " + s.helperDir);
|
|
141
|
-
if (!/^[0-9a-f]{64}$/.test(s.hooksSha256)) throw new Error("hooksSha256 is not a digest");
|
|
142
192
|
// The inventory is the removal/replacement authority, so it must be EXACTLY the closure
|
|
143
193
|
// this unit publishes: a state naming fewer members would leave an unlisted file in the
|
|
144
194
|
// helper directory that nothing can reclaim, and one naming more would license deleting
|
|
@@ -164,7 +214,7 @@ if [ -e "$STATE_FILE" ] || [ -L "$STATE_FILE" ]; then
|
|
|
164
214
|
(duplicate.length ? "; duplicated " + duplicate.join(", ") : ""),
|
|
165
215
|
);
|
|
166
216
|
}
|
|
167
|
-
const lines = ["
|
|
217
|
+
const lines = ["SCHEMA\t" + s.schema];
|
|
168
218
|
for (const f of s.helperFiles) {
|
|
169
219
|
if (typeof f?.path !== "string" || f.path.length === 0 || f.path.startsWith("/") || f.path.split("/").includes("..")) {
|
|
170
220
|
throw new Error("unsafe helper path " + JSON.stringify(f?.path));
|
|
@@ -175,7 +225,7 @@ if [ -e "$STATE_FILE" ] || [ -L "$STATE_FILE" ]; then
|
|
|
175
225
|
process.stdout.write(lines.join("\n"));
|
|
176
226
|
' "$STATE_FILE" "$HOOKS_FILE" "$HELPER_DIR" 2>&1)" || die "the ownership state is malformed or bound elsewhere, so replacement authority is UNKNOWN: $STATE_READ
|
|
177
227
|
Move $STATE_FILE aside deliberately and re-run. Nothing written."
|
|
178
|
-
|
|
228
|
+
RECORDED_SCHEMA="$(printf '%s\n' "$STATE_READ" | sed -n 's/^SCHEMA\t//p')"
|
|
179
229
|
STATE_PRESENT=1
|
|
180
230
|
|
|
181
231
|
# The closure is licensed by the SAME rule as the declaration, and for the same reason.
|
|
@@ -214,25 +264,9 @@ $STATE_READ
|
|
|
214
264
|
EOF
|
|
215
265
|
fi
|
|
216
266
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if [ -e "$HOOKS_FILE" ]; then
|
|
221
|
-
[ -f "$HOOKS_FILE" ] || die "$HOOKS_FILE exists and is not a regular file. Refusing; nothing written."
|
|
222
|
-
LIVE_HOOKS_SHA="$(sha_of "$HOOKS_FILE")"
|
|
223
|
-
if [ "$STATE_PRESENT" = "0" ]; then
|
|
224
|
-
die "$HOOKS_FILE already exists and this host has NO entwurf ownership state ($STATE_FILE).
|
|
225
|
-
This unit owns hooks.json as ONE COMPLETE FILE, so adopting it would silently delete hook
|
|
226
|
-
declarations somebody else wrote. Refusing; nothing written.
|
|
227
|
-
Move it aside deliberately and re-run — or declare your own hooks in config.toml, which the
|
|
228
|
-
vendor merges with this file (both sources load per layer)."
|
|
229
|
-
fi
|
|
230
|
-
if [ "$LIVE_HOOKS_SHA" != "$RECORDED_HOOKS_SHA" ]; then
|
|
231
|
-
die "$HOOKS_FILE was edited after install (live $LIVE_HOOKS_SHA, recorded $RECORDED_HOOKS_SHA).
|
|
232
|
-
Republishing would throw away an edit this unit cannot read back. Refusing; nothing written.
|
|
233
|
-
If the edit was yours, keep it and stop using this unit; otherwise run the inverse first."
|
|
234
|
-
fi
|
|
235
|
-
note "adopting the hooks.json this unit published (sha256 $LIVE_HOOKS_SHA) — it will be republished."
|
|
267
|
+
require_writable_share "$HOOKS_FILE" "the declaration file"
|
|
268
|
+
if [ "$STATE_PRESENT" = "1" ] && [ "$RECORDED_SCHEMA" = "codex-birth-install-state/v1" ]; then
|
|
269
|
+
note "superseding a v1 ownership receipt (it claimed the WHOLE hooks.json) with v2 (it certifies entwurf's own declaration) — no foreign byte is read as ours, and none is rewritten."
|
|
236
270
|
fi
|
|
237
271
|
|
|
238
272
|
if [ -e "$HELPER_DIR" ] || [ -L "$HELPER_DIR" ]; then
|
|
@@ -301,59 +335,113 @@ REGISTRY_SHA="$(sha_of "$STAGE/entwurf-capabilities.json")"
|
|
|
301
335
|
# identity from the record this hook writes.
|
|
302
336
|
# state not a hooks.json key at all; `[hooks.state]` is the vendor's, in config.toml, and
|
|
303
337
|
# this unit neither reads nor writes that file.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
338
|
+
#
|
|
339
|
+
# FOUR OUTCOMES, AND ONLY TWO OF THEM WRITE (#117). The declaration leaf decides, because it is
|
|
340
|
+
# the same leaf the doctor and the fresh preflight judge with:
|
|
341
|
+
#
|
|
342
|
+
# PRESENT+CERTIFIED our group is there, shaped right, and its normalized digest is the one
|
|
343
|
+
# this unit publishes -> hooks.json is NOT TOUCHED AT ALL. Not rewritten,
|
|
344
|
+
# not re-serialized, mtime unchanged. This is the ordinary re-run on a host
|
|
345
|
+
# where a neighbour has since re-serialized the document.
|
|
346
|
+
# ABSENT our group is not there -> it is APPENDED by text splice. Every foreign
|
|
347
|
+
# group comes through byte-for-byte, and appending (rather than inserting)
|
|
348
|
+
# leaves a neighbour's index — and therefore their trust receipt — alone.
|
|
349
|
+
# CREATE no file at all -> this unit writes the whole document, description and all.
|
|
350
|
+
# A description is OURS only on a file we created; into a shared file this
|
|
351
|
+
# unit adds no prose, because that member belongs to whoever made it.
|
|
352
|
+
# DRIFTED/DUPLICATED our group is there and edited, or there twice -> zero-write REFUSAL by
|
|
353
|
+
# name. Republishing would throw away an edit nobody can read back, and
|
|
354
|
+
# duplication is a birth hook the vendor would run twice.
|
|
355
|
+
#
|
|
356
|
+
# Every splice is re-parsed and deep-compared to the value it intended before it reaches a file
|
|
357
|
+
# (`certifySplice`), so a span edit that lands anywhere else is a refusal, not a written file.
|
|
358
|
+
HOOKS_DESCRIPTION="entwurf codex-birth $UNIT_VERSION — mints one garden meta-record per top-level Codex thread on its first turn. Managed by scripts/codex-birth-install.sh; inverse: scripts/codex-birth-uninstall.sh. This unit owns ONLY this SessionStart declaration; any other group in this file is foreign and is never read as ours. The vendor's trust decision for this declaration lives in config.toml and is the operator's alone."
|
|
359
|
+
DECISION="$("$NODE_BIN" -e '
|
|
360
|
+
const fs = require("node:fs");
|
|
361
|
+
const [, lib, hooksFile, launcher, description, outTmp] = process.argv;
|
|
362
|
+
import(lib).then((m) => {
|
|
363
|
+
const group = m.entwurfDeclarationGroup(launcher);
|
|
364
|
+
const digest = m.declarationDigest(group);
|
|
365
|
+
const emit = (verdict, detail) => process.stdout.write([verdict, digest, detail ?? ""].join("\t"));
|
|
366
|
+
if (!fs.existsSync(hooksFile)) {
|
|
367
|
+
fs.writeFileSync(outTmp, JSON.stringify({ description, hooks: { SessionStart: [group] } }, null, 2) + "\n");
|
|
368
|
+
return emit("CREATE", "");
|
|
369
|
+
}
|
|
370
|
+
const text = fs.readFileSync(hooksFile, "utf8");
|
|
371
|
+
let parsed;
|
|
372
|
+
try {
|
|
373
|
+
parsed = JSON.parse(text);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
throw new Error(hooksFile + " is not readable JSON (" + err.message + "). This unit edits only its own declaration inside that file and will not rewrite a document it cannot read.");
|
|
376
|
+
}
|
|
377
|
+
const sel = m.selectEntwurfDeclaration(parsed, launcher);
|
|
378
|
+
if (sel.ok) {
|
|
379
|
+
if (sel.digest !== digest) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
"entwurf\u2019s own declaration in " + hooksFile + " was EDITED after install (group " + sel.groupIndex +
|
|
382
|
+
": live " + sel.digest + ", this unit publishes " + digest + "). Republishing would throw away an edit this unit cannot read back. If the edit was yours, keep it and stop using this unit; otherwise run the inverse first.",
|
|
383
|
+
);
|
|
317
384
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
385
|
+
return emit("UNCHANGED", String(sel.foreign.length));
|
|
386
|
+
}
|
|
387
|
+
if (sel.code !== "declaration-absent") throw new Error(sel.code + ": " + sel.detail);
|
|
388
|
+
const want = JSON.parse(text);
|
|
389
|
+
if (want.hooks === null || typeof want.hooks !== "object" || Array.isArray(want.hooks)) {
|
|
390
|
+
throw new Error(hooksFile + " carries no `hooks` object, so there is nowhere to declare a SessionStart group without rewriting somebody else\u2019s grammar.");
|
|
391
|
+
}
|
|
392
|
+
want.hooks.SessionStart = (Array.isArray(want.hooks.SessionStart) ? want.hooks.SessionStart : []).concat([group]);
|
|
393
|
+
fs.writeFileSync(outTmp, m.certifySplice(m.appendSessionStartGroup(m.ensureSessionStartArray(text), group), want));
|
|
394
|
+
return emit("APPEND", String(sel.foreign.length));
|
|
395
|
+
}).catch((err) => {
|
|
396
|
+
process.stderr.write(err && err.message ? err.message : String(err));
|
|
397
|
+
process.exit(1);
|
|
398
|
+
});
|
|
399
|
+
' "$DECLARATION_LIB" "$HOOKS_FILE" "$LAUNCHER" "$HOOKS_DESCRIPTION" "$HOOKS_TMP" 2>&1)" || die "$DECISION
|
|
400
|
+
Refusing; nothing written."
|
|
401
|
+
HOOKS_ACTION="$(printf '%s' "$DECISION" | cut -f1)"
|
|
402
|
+
DECL_SHA="$(printf '%s' "$DECISION" | cut -f2)"
|
|
403
|
+
FOREIGN_COUNT="$(printf '%s' "$DECISION" | cut -f3)"
|
|
404
|
+
# Exactly 64 hex, not "starts hex": this value is written into the ownership receipt and every
|
|
405
|
+
# later reader compares the live declaration to it, so a truncated or warning-polluted capture
|
|
406
|
+
# would record a receipt nothing can ever match.
|
|
407
|
+
case "$DECL_SHA" in
|
|
408
|
+
""|*[!0-9a-f]*) die "the declaration leaf returned no digest (got '$DECISION'). Nothing written." ;;
|
|
409
|
+
esac
|
|
410
|
+
[ "${#DECL_SHA}" -eq 64 ] || die "the declaration leaf returned a ${#DECL_SHA}-character digest, not 64 (got '$DECISION'). Nothing written."
|
|
411
|
+
# `mktemp` made this file, so it always exists; chmod it unconditionally rather than behind a
|
|
412
|
+
# test whose false branch would be the last command of a `set -e` line.
|
|
322
413
|
chmod 0644 -- "$HOOKS_TMP"
|
|
323
414
|
|
|
324
|
-
# Prove the bytes
|
|
325
|
-
# hooks.json is a hook nobody declared and nobody can see failing.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
' "$HOOKS_TMP" "$LAUNCHER" || die "the staged hooks.json is not the exact declaration this unit publishes. Nothing written."
|
|
342
|
-
HOOKS_SHA="$(sha_of "$HOOKS_TMP")"
|
|
343
|
-
|
|
415
|
+
# Prove the staged bytes carry EXACTLY our declaration before they are published: a broken or
|
|
416
|
+
# absorbed hooks.json is a hook nobody declared and nobody can see failing. The same leaf reads
|
|
417
|
+
# it back, so what is asserted here is what the doctor and the preflight will assert later.
|
|
418
|
+
if [ "$HOOKS_ACTION" != "UNCHANGED" ]; then
|
|
419
|
+
"$NODE_BIN" -e '
|
|
420
|
+
const fs = require("node:fs");
|
|
421
|
+
const [, lib, staged, launcher, wantDigest] = process.argv;
|
|
422
|
+
import(lib).then((m) => {
|
|
423
|
+
const sel = m.selectEntwurfDeclaration(JSON.parse(fs.readFileSync(staged, "utf8")), launcher);
|
|
424
|
+
if (!sel.ok) throw new Error("the staged document does not carry our declaration: " + sel.code + " " + sel.detail);
|
|
425
|
+
if (sel.digest !== wantDigest) throw new Error("the staged declaration digest is " + sel.digest + ", not " + wantDigest);
|
|
426
|
+
}).catch((err) => {
|
|
427
|
+
process.stderr.write(err && err.message ? err.message : String(err));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
});
|
|
430
|
+
' "$DECLARATION_LIB" "$HOOKS_TMP" "$LAUNCHER" "$DECL_SHA" || die "the staged hooks.json is not the exact declaration this unit publishes. Nothing written."
|
|
431
|
+
fi
|
|
344
432
|
write_state() { # $1 = status
|
|
345
433
|
# Created here only when absent — the safe umask floors it and the explicit mode removes any
|
|
346
434
|
# doubt. An EXISTING root was judged above and is left exactly as it was found.
|
|
347
435
|
if [ ! -e "$UNIT_ROOT" ]; then mkdir -p -- "$UNIT_ROOT" && chmod 0755 -- "$UNIT_ROOT"; fi
|
|
348
436
|
cat > "$STATE_TMP" <<STATE
|
|
349
437
|
{
|
|
350
|
-
"schema": "codex-birth-install-state/
|
|
438
|
+
"schema": "codex-birth-install-state/v2",
|
|
351
439
|
"status": "$1",
|
|
352
440
|
"unitVersion": "$UNIT_VERSION",
|
|
353
441
|
"writtenAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
354
442
|
"nodeBin": "$NODE_BIN",
|
|
355
443
|
"hooksFile": "$HOOKS_FILE",
|
|
356
|
-
"
|
|
444
|
+
"declaration": { "event": "SessionStart", "command": "'$LAUNCHER'", "sha256": "$DECL_SHA" },
|
|
357
445
|
"hooksExistedBefore": $HOOKS_EXISTED,
|
|
358
446
|
"helperDir": "$HELPER_DIR",
|
|
359
447
|
"helperFiles": [
|
|
@@ -396,18 +484,36 @@ chmod 0755 -- "$LAUNCHER"
|
|
|
396
484
|
chmod 0644 -- "$HELPER_DIR/$PAYLOAD_NAME" "$HELPER_DIR/lib/meta-session.ts" "$HELPER_DIR/lib/native-push/codex-ws-client.ts" "$HELPER_DIR/lib/session-id.js" "$HELPER_DIR/entwurf-capabilities.json"
|
|
397
485
|
|
|
398
486
|
if [ ! -e "$CODEX_HOME" ]; then mkdir -p -- "$CODEX_HOME" && chmod 0755 -- "$CODEX_HOME"; fi
|
|
399
|
-
# Strongest idempotence is an
|
|
400
|
-
# re-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
487
|
+
# Strongest idempotence is an UNTOUCHED file, and on a shared hooks.json that is also the only
|
|
488
|
+
# honest one: re-serializing a document to reproduce bytes we already agree with would rewrite
|
|
489
|
+
# every neighbouring declaration for no reason at all.
|
|
490
|
+
case "$HOOKS_ACTION" in
|
|
491
|
+
UNCHANGED)
|
|
492
|
+
note "hooks.json already carries entwurf's declaration (normalized sha256 $DECL_SHA) — NOT REWRITTEN, not one byte"
|
|
493
|
+
;;
|
|
494
|
+
APPEND)
|
|
495
|
+
# The file was somebody else's before this line and is shared after it, so its MODE is
|
|
496
|
+
# carried over from what we found rather than reset to the one a fresh publish would use.
|
|
497
|
+
chmod "$(stat -c %a -- "$HOOKS_FILE")" -- "$HOOKS_TMP"
|
|
498
|
+
cp -- "$HOOKS_TMP" "$HOOKS_FILE.tmp" && mv -f -- "$HOOKS_FILE.tmp" "$HOOKS_FILE"
|
|
499
|
+
note "appended entwurf's declaration to the existing hooks.json by text splice — every foreign group came through byte-for-byte"
|
|
500
|
+
;;
|
|
501
|
+
CREATE)
|
|
502
|
+
cp -- "$HOOKS_TMP" "$HOOKS_FILE.tmp" && mv -f -- "$HOOKS_FILE.tmp" "$HOOKS_FILE"
|
|
503
|
+
;;
|
|
504
|
+
*)
|
|
505
|
+
die "unknown declaration decision '$HOOKS_ACTION'. Nothing more written."
|
|
506
|
+
;;
|
|
507
|
+
esac
|
|
406
508
|
|
|
407
509
|
write_state installed
|
|
408
510
|
|
|
409
511
|
note "installed"
|
|
410
|
-
note " declaration : $HOOKS_FILE (sha256 $
|
|
512
|
+
note " declaration : $HOOKS_FILE (normalized sha256 $DECL_SHA)"
|
|
513
|
+
case "$FOREIGN_COUNT" in
|
|
514
|
+
""|0) note " neighbours : none — entwurf is the only SessionStart declaration in that file" ;;
|
|
515
|
+
*) note " neighbours : $FOREIGN_COUNT foreign SessionStart group(s) in that file, neither certified nor touched" ;;
|
|
516
|
+
esac
|
|
411
517
|
note " launcher : $LAUNCHER"
|
|
412
518
|
note " state : $STATE_FILE"
|
|
413
519
|
note "The vendor will ask the operator to trust this declaration ONCE, in the Codex TUI."
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# codex-birth-uninstall — the inverse of codex-birth-install.sh.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# WHAT IT REMOVES FROM hooks.json, AND WHAT IT LEAVES (#117). Not the file: ONE `SessionStart`
|
|
5
|
+
# group, the one whose handler commands our launcher, taken out by TEXT SPLICE so every
|
|
6
|
+
# neighbouring declaration comes out byte-for-byte identical. The file itself is removed only
|
|
7
|
+
# when our group was the only group in it and no other event is declared — the case where
|
|
8
|
+
# removing our declaration leaves a document with nothing in it but our own prose. A top-level
|
|
9
|
+
# `description` is removed only when it is entwurf's own; anybody else's is a foreign byte.
|
|
10
|
+
#
|
|
11
|
+
# The ownership state is the ONLY input for the CLOSURE. Every removal there is licensed
|
|
12
|
+
# per-byte by a digest it recorded, so this inverse can say three different things about every
|
|
13
|
+
# path it published and never guesses at a fourth:
|
|
7
14
|
#
|
|
8
15
|
# * already absent -> done, not drift.
|
|
9
16
|
# * bytes match the state -> removed.
|
|
@@ -11,6 +18,10 @@
|
|
|
11
18
|
# edited it, and deleting an edit nobody can read back is worse
|
|
12
19
|
# than leaving a file behind.
|
|
13
20
|
#
|
|
21
|
+
# The DECLARATION is licensed the same way but by its NORMALIZED digest rather than by file
|
|
22
|
+
# bytes, because a neighbour re-serializing the document (measured: Herdr does) changes every
|
|
23
|
+
# byte of that file and nothing about what entwurf declared.
|
|
24
|
+
#
|
|
14
25
|
# It never reads or writes config.toml, so the vendor's `[hooks.state]` trust record survives
|
|
15
26
|
# this inverse untouched. Removing the declaration does not un-trust it; re-publishing the
|
|
16
27
|
# SAME declaration later reuses the operator's original approval, because the trust identity
|
|
@@ -42,6 +53,21 @@ command -v sha256sum >/dev/null 2>&1 || die "sha256sum is required (removal is l
|
|
|
42
53
|
NODE_BIN="$(command -v node)" || die "node is not on PATH — it parses the ownership state."
|
|
43
54
|
sha_of() { sha256sum -- "$1" | cut -d' ' -f1; }
|
|
44
55
|
|
|
56
|
+
REPO="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
57
|
+
DECLARATION_LIB="$REPO/pi-extensions/lib/codex-declaration.js"
|
|
58
|
+
|
|
59
|
+
# One shared ownership predicate for the file this unit SHARES — see the installer's copy of this
|
|
60
|
+
# comment for why four surfaces had to stop deciding it three different ways.
|
|
61
|
+
owned_path_verdict() { # $1 = path, $2 = file|directory
|
|
62
|
+
"$NODE_BIN" -e '
|
|
63
|
+
import(process.argv[1]).then((m) => {
|
|
64
|
+
const fs = require("node:fs");
|
|
65
|
+
process.stdout.write(m.classifyOwnedPath(m.statOwnedPath(fs, process.argv[2]), process.getuid(), { kind: process.argv[3] }));
|
|
66
|
+
}).catch((err) => { process.stderr.write(String(err && err.message ? err.message : err)); process.exit(1); });
|
|
67
|
+
' "$DECLARATION_LIB" "$1" "${2:-file}"
|
|
68
|
+
}
|
|
69
|
+
[ -f "$DECLARATION_LIB" ] || die "the declaration leaf is missing: $DECLARATION_LIB — without it this inverse cannot tell entwurf's declaration from a neighbour's. Nothing removed."
|
|
70
|
+
|
|
45
71
|
# Judged BEFORE anything is removed: a state sitting in a directory somebody else can write
|
|
46
72
|
# is a removal licence somebody else can forge.
|
|
47
73
|
safe_parent() { # $1 = dir, $2 = label
|
|
@@ -72,11 +98,21 @@ PLAN="$("$NODE_BIN" -e '
|
|
|
72
98
|
const path = require("node:path");
|
|
73
99
|
const s = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
74
100
|
const hex = (v) => typeof v === "string" && /^[0-9a-f]{64}$/.test(v);
|
|
75
|
-
if (s.schema !== "codex-birth-install-state/
|
|
101
|
+
if (s.schema !== "codex-birth-install-state/v2") {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"the ownership state is " + JSON.stringify(s.schema) + ". This inverse reads codex-birth-install-state/v2, " +
|
|
104
|
+
"which certifies entwurf\u2019s own declaration; v1 claimed the WHOLE hooks.json and removing on its word " +
|
|
105
|
+
"would delete a neighbour\u2019s. Run `entwurf install-codex-birth` once to supersede the receipt, then this inverse.",
|
|
106
|
+
);
|
|
107
|
+
}
|
|
76
108
|
if (s.status !== "installed" && s.status !== "publishing") throw new Error("unknown status " + JSON.stringify(s.status));
|
|
77
109
|
if (typeof s.hooksFile !== "string" || !s.hooksFile.startsWith("/")) throw new Error("hooksFile is not an absolute path");
|
|
78
110
|
if (typeof s.helperDir !== "string" || !s.helperDir.startsWith("/")) throw new Error("helperDir is not an absolute path");
|
|
79
|
-
|
|
111
|
+
const decl = s.declaration;
|
|
112
|
+
if (decl === null || typeof decl !== "object" || Array.isArray(decl)) throw new Error("the state records no declaration receipt");
|
|
113
|
+
if (decl.event !== "SessionStart") throw new Error("the recorded declaration event is " + JSON.stringify(decl.event));
|
|
114
|
+
if (typeof decl.command !== "string" || decl.command.length === 0) throw new Error("the recorded declaration command is not a string");
|
|
115
|
+
if (!hex(decl.sha256)) throw new Error("the recorded declaration digest is not a sha256 hex digest");
|
|
80
116
|
if (!Array.isArray(s.helperFiles) || s.helperFiles.length === 0) throw new Error("helperFiles is empty");
|
|
81
117
|
// Exactly the closure this unit publishes. Removing on a SHORT inventory would leave a file
|
|
82
118
|
// behind and then delete the directory that held it; a LONG one would license deleting
|
|
@@ -101,7 +137,17 @@ PLAN="$("$NODE_BIN" -e '
|
|
|
101
137
|
(duplicate.length ? "; duplicated " + duplicate.join(", ") : ""),
|
|
102
138
|
);
|
|
103
139
|
}
|
|
104
|
-
|
|
140
|
+
// WHO CREATED THE FILE is a removal authority, and the receipt has always recorded it — the
|
|
141
|
+
// inverse simply never read it (sol B2, 2026-09-18). Anything other than an explicit `false`
|
|
142
|
+
// reads as "it was already there", which is the safe direction: an old receipt that predates
|
|
143
|
+
// this field licenses a splice, never a delete.
|
|
144
|
+
const createdByUs = s.hooksExistedBefore === false;
|
|
145
|
+
const lines = [
|
|
146
|
+
["STATUS", s.status, s.unitVersion ?? ""].join("\t"),
|
|
147
|
+
["DECLCMD", "-", decl.command].join("\t"),
|
|
148
|
+
["HOOKSCREATED", "-", createdByUs ? "yes" : "no"].join("\t"),
|
|
149
|
+
["DECLARATION", decl.sha256, s.hooksFile].join("\t"),
|
|
150
|
+
];
|
|
105
151
|
const seen = new Set();
|
|
106
152
|
for (const f of s.helperFiles) {
|
|
107
153
|
if (typeof f?.path !== "string" || f.path.length === 0 || f.path.startsWith("/") || f.path.split("/").includes("..")) {
|
|
@@ -118,6 +164,111 @@ PLAN="$("$NODE_BIN" -e '
|
|
|
118
164
|
Inspect it and remove the unit deliberately. Nothing removed."
|
|
119
165
|
|
|
120
166
|
DRIFTED=0
|
|
167
|
+
|
|
168
|
+
# The declaration is not a file this inverse deletes — it is ONE GROUP inside a file it shares.
|
|
169
|
+
# Four verdicts, and only two of them touch a byte: already absent (done, not drift), drifted
|
|
170
|
+
# (named, left exactly as found), sole-owner (the file was ours whole, so the file goes), and
|
|
171
|
+
# shared (our group is spliced out and every neighbouring group comes through byte-for-byte).
|
|
172
|
+
REMOVE_DECLARATION() { # $1 = recorded normalized digest, $2 = hooks.json path
|
|
173
|
+
local want="$1" target="$2" verdict kind detail tmp owned
|
|
174
|
+
# THE SAME PREDICATE THE INSTALLER AND THE DOCTOR USE (sol B3, 2026-09-18). This inverse used to
|
|
175
|
+
# ask only symlink-and-regular, so it would happily rewrite a hooks.json owned by another user or
|
|
176
|
+
# writable by a group — a file whose next state nothing here can bind. Every non-`ok` verdict is
|
|
177
|
+
# DRIFT: named, counted, and zero-write.
|
|
178
|
+
owned="$(owned_path_verdict "$target" file)" || {
|
|
179
|
+
printf '[codex-birth-uninstall] DRIFT: %s could not be judged for ownership (%s) — leaving it exactly as found.\n' "$target" "$owned" >&2
|
|
180
|
+
DRIFTED=$((DRIFTED + 1)); return
|
|
181
|
+
}
|
|
182
|
+
if [ "$owned" = "missing" ]; then
|
|
183
|
+
note "already absent: $target"; return
|
|
184
|
+
fi
|
|
185
|
+
if [ "$owned" != "ok" ]; then
|
|
186
|
+
printf '[codex-birth-uninstall] DRIFT: the declaration file %s (%s) — refusing to edit it.\n' "$owned" "$target" >&2
|
|
187
|
+
DRIFTED=$((DRIFTED + 1)); return
|
|
188
|
+
fi
|
|
189
|
+
tmp="$(mktemp)"
|
|
190
|
+
verdict="$("$NODE_BIN" -e '
|
|
191
|
+
const fs = require("node:fs");
|
|
192
|
+
const [, lib, hooksFile, launcherCommand, wantDigest, outTmp, createdByUs] = process.argv;
|
|
193
|
+
import(lib).then((m) => {
|
|
194
|
+
const text = fs.readFileSync(hooksFile, "utf8");
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(text);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return process.stdout.write("DRIFT\t" + hooksFile + " is not readable JSON (" + err.message + ") — this inverse edits only its own group and will not rewrite a document it cannot read");
|
|
200
|
+
}
|
|
201
|
+
// The launcher the RECEIPT names, not one rebuilt from paths: the receipt is the authority.
|
|
202
|
+
const launcher = launcherCommand.replace(/^\x27|\x27$/g, "");
|
|
203
|
+
const sel = m.selectEntwurfDeclaration(parsed, launcher);
|
|
204
|
+
if (!sel.ok) {
|
|
205
|
+
if (sel.code === "declaration-absent") return process.stdout.write("ABSENT\t");
|
|
206
|
+
return process.stdout.write("DRIFT\t" + sel.code + ": " + sel.detail);
|
|
207
|
+
}
|
|
208
|
+
if (sel.digest !== wantDigest) {
|
|
209
|
+
return process.stdout.write(
|
|
210
|
+
"DRIFT\tentwurf\u2019s declaration was edited after install (group " + sel.groupIndex + ": live " + sel.digest + ", recorded " + wantDigest + ")",
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
// FILE — deleting the whole file — is the ONE verdict that can destroy a byte nobody here
|
|
214
|
+
// wrote, so it takes three proofs and not one (sol B2, 2026-09-18). "Our group is the last
|
|
215
|
+
// declaration" was the only one it used to take, and that is true of a file we merely
|
|
216
|
+
// APPENDED to: `{"description":"foreign","hooks":{"SessionStart":[]}}` has no foreign GROUP,
|
|
217
|
+
// so after our install the old shape classified it FILE and deleted top-level bytes
|
|
218
|
+
// belonging to a neighbour.
|
|
219
|
+
// 1. the receipt says this unit created the file (hooksExistedBefore === false),
|
|
220
|
+
// 2. nothing but our own two top-level keys is in it, and the description is OURS,
|
|
221
|
+
// 3. our group is the only declaration, under the only event.
|
|
222
|
+
// Anything else SPLICES: our group comes out, every other byte stays.
|
|
223
|
+
const events = Object.keys(parsed.hooks);
|
|
224
|
+
const topLevel = Object.keys(parsed).sort().join(",");
|
|
225
|
+
const ourDescription =
|
|
226
|
+
typeof parsed.description === "string" && parsed.description.startsWith(m.CODEX_BIRTH_DESCRIPTION_PREFIX);
|
|
227
|
+
const soleDeclaration = sel.foreign.length === 0 && events.length === 1 && events[0] === m.CODEX_BIRTH_EVENT;
|
|
228
|
+
if (createdByUs === "yes" && soleDeclaration && topLevel === "description,hooks" && ourDescription) {
|
|
229
|
+
return process.stdout.write("FILE\t");
|
|
230
|
+
}
|
|
231
|
+
const want = JSON.parse(text);
|
|
232
|
+
want.hooks[m.CODEX_BIRTH_EVENT].splice(sel.groupIndex, 1);
|
|
233
|
+
const description = want.description;
|
|
234
|
+
let spliced = m.certifySplice(m.removeSessionStartGroup(text, sel.groupIndex), want);
|
|
235
|
+
if (typeof description === "string" && description.startsWith(m.CODEX_BIRTH_DESCRIPTION_PREFIX)) {
|
|
236
|
+
delete want.description;
|
|
237
|
+
spliced = m.certifySplice(m.removeEntwurfDescription(spliced), want);
|
|
238
|
+
}
|
|
239
|
+
fs.writeFileSync(outTmp, spliced);
|
|
240
|
+
process.stdout.write("SPLICE\t" + String(sel.foreign.length));
|
|
241
|
+
}).catch((err) => {
|
|
242
|
+
process.stderr.write(err && err.message ? err.message : String(err));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
|
245
|
+
' "$DECLARATION_LIB" "$target" "$DECLARATION_COMMAND" "$want" "$tmp" "$HOOKS_CREATED_BY_US" 2>&1)" || {
|
|
246
|
+
rm -f -- "$tmp"
|
|
247
|
+
printf '[codex-birth-uninstall] DRIFT: the declaration could not be judged (%s) — leaving %s exactly as found.\n' "$verdict" "$target" >&2
|
|
248
|
+
DRIFTED=$((DRIFTED + 1)); return
|
|
249
|
+
}
|
|
250
|
+
kind="$(printf '%s' "$verdict" | cut -f1)"
|
|
251
|
+
detail="$(printf '%s' "$verdict" | cut -f2-)"
|
|
252
|
+
case "$kind" in
|
|
253
|
+
ABSENT) rm -f -- "$tmp"; note "already absent: entwurf declares nothing in $target" ;;
|
|
254
|
+
DRIFT)
|
|
255
|
+
rm -f -- "$tmp"
|
|
256
|
+
printf '[codex-birth-uninstall] DRIFT: %s — LEFT IN PLACE (%s).\n' "$detail" "$target" >&2
|
|
257
|
+
DRIFTED=$((DRIFTED + 1))
|
|
258
|
+
;;
|
|
259
|
+
FILE) rm -f -- "$tmp" "$target"; note "removed $target (entwurf was its only declaration)" ;;
|
|
260
|
+
SPLICE)
|
|
261
|
+
# The file survives this inverse, so its MODE is one more thing that is not ours to
|
|
262
|
+
# change: it is carried over from what we found rather than reset to what we publish.
|
|
263
|
+
chmod "$(stat -c %a -- "$target")" -- "$tmp"
|
|
264
|
+
cp -- "$tmp" "$target.tmp" && mv -f -- "$target.tmp" "$target"
|
|
265
|
+
rm -f -- "$tmp"
|
|
266
|
+
note "removed entwurf's declaration from $target by text splice — $detail foreign group(s) preserved byte-for-byte"
|
|
267
|
+
;;
|
|
268
|
+
*) rm -f -- "$tmp"; die "the declaration reader returned '$verdict'. Nothing else removed." ;;
|
|
269
|
+
esac
|
|
270
|
+
}
|
|
271
|
+
|
|
121
272
|
REMOVE_ONE() { # $1 = recorded digest, $2 = absolute path, $3 = label
|
|
122
273
|
local want="$1" target="$2" label="$3" live
|
|
123
274
|
if [ -L "$target" ]; then
|
|
@@ -141,10 +292,14 @@ REMOVE_ONE() { # $1 = recorded digest, $2 = absolute path, $3 = label
|
|
|
141
292
|
}
|
|
142
293
|
|
|
143
294
|
HELPER_DIR=""
|
|
295
|
+
DECLARATION_COMMAND=""
|
|
296
|
+
HOOKS_CREATED_BY_US="no"
|
|
144
297
|
while IFS="$(printf '\t')" read -r kind digest target; do
|
|
145
298
|
case "$kind" in
|
|
146
299
|
STATUS) note "state: status=$digest unitVersion=$target" ;;
|
|
147
|
-
|
|
300
|
+
DECLCMD) DECLARATION_COMMAND="$target" ;;
|
|
301
|
+
HOOKSCREATED) HOOKS_CREATED_BY_US="$target" ;;
|
|
302
|
+
DECLARATION) REMOVE_DECLARATION "$digest" "$target" ;;
|
|
148
303
|
HELPER) REMOVE_ONE "$digest" "$target" "helper member" ;;
|
|
149
304
|
HELPERDIR) HELPER_DIR="$target" ;;
|
|
150
305
|
esac
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "SSOT for the herdr binary this repository has MEASURED against. Nothing else may carry a version or a digest: the CI installer, the sandbox gate's boundary check and the launch-rail document all read or cite this file. Re-measuring a new herdr means running check-herdr-sandbox against it FIRST, then editing this file, then repairing the measured sentences in docs/herdr-launch-rail.md in the same commit — miss one and the gate fails on version mismatch, which is the point.",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"tag": "v0.9.1",
|
|
5
|
+
"tagCommit": "065ef9d6a531c49fb8bee7e818ef837065b21ee9",
|
|
6
|
+
"socketProtocol": 22,
|
|
7
|
+
"measuredOn": "2026-09-17",
|
|
8
|
+
"measuredArch": "aarch64",
|
|
9
|
+
"releaseUrlPrefix": "https://github.com/herdrdev/herdr/releases/download",
|
|
10
|
+
"integrity": {
|
|
11
|
+
"kind": "github-immutable-release-attestation",
|
|
12
|
+
"predicateType": "https://in-toto.io/attestation/release/v0.2",
|
|
13
|
+
"note": "GitHub signs that these digests are this tag's assets. It is NOT a SLSA build provenance authored by herdr and NOT a reproducible-build claim. Corroborate with `gh api repos/herdrdev/herdr/attestations/sha256:<digest>`; `gh attestation verify` 2.97.0 cannot close this predicate and must not be depended on."
|
|
14
|
+
},
|
|
15
|
+
"assets": {
|
|
16
|
+
"x86_64": {
|
|
17
|
+
"name": "herdr-linux-x86_64",
|
|
18
|
+
"sha256": "2a02fed16beb651ef006e1d43f048f652ca4dc58ad053cd2d44450563d5c54b7",
|
|
19
|
+
"executed": false,
|
|
20
|
+
"executedNote": "pending the first CI run on an x86_64 runner — the digest is pinned, but no cell of this rail has ever run on this binary"
|
|
21
|
+
},
|
|
22
|
+
"aarch64": {
|
|
23
|
+
"name": "herdr-linux-aarch64",
|
|
24
|
+
"sha256": "f4ccf4de745f2cb9a39a983e9ba3703dad50ec2a58dea83026ceab721bbd8d9e",
|
|
25
|
+
"executed": true,
|
|
26
|
+
"executedNote": "the #116 rail was re-measured on this exact binary on 2026-09-17: `check-herdr-sandbox` green, `herdr status client` reports protocol 22 (unchanged from 0.9.0), and `integration status` adds exactly one row — `letta (experimental)` — with every other row byte-identical to 0.9.0. The operator host still runs the 0.9.0 asset at that date; the binary measured here was fetched by digest into a temp dir"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|