@mnemom/mnemom 0.17.33 → 0.17.34-next.2
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/dist/base-url-proxy.mjs +18 -4
- package/dist/commands/agent-config.js +5 -1
- package/dist/commands/agent-doctor.d.ts +9 -0
- package/dist/commands/agent-doctor.js +4 -0
- package/dist/commands/agent-session.d.ts +1 -2
- package/dist/commands/agent-session.js +1 -2
- package/dist/commands/agent-setup.d.ts +6 -0
- package/dist/commands/agent-setup.js +12 -1
- package/dist/commands/agent-share-render.d.ts +1 -1
- package/dist/commands/agent-share-render.js +1 -1
- package/dist/commands/agent.d.ts +26 -0
- package/dist/commands/agent.js +228 -53
- package/dist/commands/durable-doctor.d.ts +50 -0
- package/dist/commands/durable-doctor.js +186 -0
- package/dist/commands/durable-lock.d.ts +254 -0
- package/dist/commands/durable-lock.js +595 -0
- package/dist/commands/durable-sessions.d.ts +87 -0
- package/dist/commands/durable-sessions.js +303 -0
- package/dist/commands/durable-verbs.d.ts +116 -0
- package/dist/commands/durable-verbs.js +393 -0
- package/dist/commands/protection.js +1 -1
- package/dist/commands/scan.d.ts +97 -0
- package/dist/commands/scan.js +238 -0
- package/dist/durable/flock.d.ts +26 -0
- package/dist/durable/flock.js +80 -0
- package/dist/durable/launch-spec.d.ts +7 -1
- package/dist/durable/launch-spec.js +10 -3
- package/dist/durable/seat-proxy-entry.d.ts +11 -0
- package/dist/durable/seat-proxy-entry.js +25 -0
- package/dist/durable/seat-proxy.d.ts +107 -0
- package/dist/durable/seat-proxy.js +599 -0
- package/dist/durable/seat-socket.d.ts +46 -0
- package/dist/durable/seat-socket.js +179 -0
- package/dist/durable/seat-spec-validator.d.ts +28 -0
- package/dist/durable/seat-spec-validator.js +59 -0
- package/dist/durable/test-mode.mjs +81 -0
- package/dist/durable/turn-chain.d.ts +55 -0
- package/dist/durable/turn-chain.js +178 -0
- package/dist/host/auto-continue.d.ts +67 -0
- package/dist/host/auto-continue.js +118 -0
- package/dist/host/bootid.d.ts +13 -0
- package/dist/host/bootid.js +41 -0
- package/dist/host/client.d.ts +123 -0
- package/dist/host/client.js +171 -0
- package/dist/host/commands.d.ts +2 -0
- package/dist/host/commands.js +122 -0
- package/dist/host/control.d.ts +33 -0
- package/dist/host/control.js +144 -0
- package/dist/host/daemon.d.ts +217 -0
- package/dist/host/daemon.js +606 -0
- package/dist/host/gate.d.ts +46 -0
- package/dist/host/gate.js +82 -0
- package/dist/host/grant.d.ts +24 -0
- package/dist/host/grant.js +24 -0
- package/dist/host/instance.d.ts +35 -0
- package/dist/host/instance.js +62 -0
- package/dist/host/integrity.d.ts +35 -0
- package/dist/host/integrity.js +115 -0
- package/dist/host/launchd.d.ts +16 -0
- package/dist/host/launchd.js +45 -0
- package/dist/host/lock.d.ts +20 -0
- package/dist/host/lock.js +63 -0
- package/dist/host/owner.d.ts +55 -0
- package/dist/host/owner.js +121 -0
- package/dist/host/plist.d.ts +16 -0
- package/dist/host/plist.js +85 -0
- package/dist/host/procs.d.ts +27 -0
- package/dist/host/procs.js +105 -0
- package/dist/host/run.d.ts +66 -0
- package/dist/host/run.js +314 -0
- package/dist/host/runtime.d.ts +8 -0
- package/dist/host/runtime.js +22 -0
- package/dist/host/s1-names.d.ts +22 -0
- package/dist/host/s1-names.js +21 -0
- package/dist/host/seat-credential.d.ts +27 -0
- package/dist/host/seat-credential.js +41 -0
- package/dist/host/seat-launch-main.d.ts +1 -0
- package/dist/host/seat-launch-main.js +59 -0
- package/dist/host/seat-launch.d.ts +116 -0
- package/dist/host/seat-launch.js +302 -0
- package/dist/host/seat.d.ts +48 -0
- package/dist/host/seat.js +149 -0
- package/dist/host/state.d.ts +33 -0
- package/dist/host/state.js +117 -0
- package/dist/host/status.d.ts +101 -0
- package/dist/host/status.js +307 -0
- package/dist/host/tmux.d.ts +56 -0
- package/dist/host/tmux.js +182 -0
- package/dist/host/transcript.d.ts +30 -0
- package/dist/host/transcript.js +130 -0
- package/dist/host/verbs.d.ts +50 -0
- package/dist/host/verbs.js +271 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +232 -35
- package/dist/lib/agent.d.ts +8 -26
- package/dist/lib/agent.js +11 -26
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +8 -0
- package/dist/lib/claude-plugin.d.ts +11 -2
- package/dist/lib/claude-plugin.js +10 -1
- package/dist/lib/durable/api.d.ts +136 -0
- package/dist/lib/durable/api.js +220 -0
- package/dist/lib/durable/codes.d.ts +8 -0
- package/dist/lib/durable/codes.js +2 -0
- package/dist/lib/durable/first-turn.d.ts +50 -0
- package/dist/lib/durable/first-turn.js +102 -0
- package/dist/lib/durable/gate.d.ts +48 -0
- package/dist/lib/durable/gate.js +122 -0
- package/dist/lib/durable/handover.d.ts +101 -0
- package/dist/lib/durable/handover.js +175 -0
- package/dist/lib/durable/launch.d.ts +182 -0
- package/dist/lib/durable/launch.js +443 -0
- package/dist/lib/durable/launching.d.ts +15 -0
- package/dist/lib/durable/launching.js +70 -0
- package/dist/lib/durable/live.d.ts +194 -0
- package/dist/lib/durable/live.js +635 -0
- package/dist/lib/durable/refusal.d.ts +57 -0
- package/dist/lib/durable/refusal.js +78 -0
- package/dist/lib/durable/view.d.ts +83 -0
- package/dist/lib/durable/view.js +244 -0
- package/dist/lib/experimental.d.ts +0 -9
- package/dist/lib/experimental.js +18 -17
- package/dist/lib/launcher-config.d.ts +7 -0
- package/dist/lib/launcher-config.js +21 -0
- package/dist/lib/scan/caps.d.ts +35 -0
- package/dist/lib/scan/caps.js +67 -0
- package/dist/lib/scan/credential.d.ts +37 -0
- package/dist/lib/scan/credential.js +104 -0
- package/dist/lib/scan/failure.d.ts +10 -0
- package/dist/lib/scan/failure.js +43 -0
- package/dist/lib/scan/pr-source.d.ts +57 -0
- package/dist/lib/scan/pr-source.js +130 -0
- package/dist/lib/scan/record.d.ts +49 -0
- package/dist/lib/scan/record.js +85 -0
- package/dist/lib/scan/report-email.d.ts +92 -0
- package/dist/lib/scan/report-email.js +144 -0
- package/dist/lib/scan/report.d.ts +13 -0
- package/dist/lib/scan/report.js +30 -0
- package/dist/lib/scan/scan.d.ts +64 -0
- package/dist/lib/scan/scan.js +182 -0
- package/dist/lib/scan/share.d.ts +38 -0
- package/dist/lib/scan/share.js +47 -0
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
- package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
- package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
- package/dist/lib/scan/spend/anthropic-usage.js +244 -0
- package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
- package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
- package/dist/lib/scan/spend/prices.d.ts +33 -0
- package/dist/lib/scan/spend/prices.js +57 -0
- package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
- package/dist/lib/scan/spend/spend-estimate.js +70 -0
- package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
- package/dist/lib/scan/spend/spend-tiers.js +42 -0
- package/dist/lib/scan/spend/token-rate.d.ts +30 -0
- package/dist/lib/scan/spend/token-rate.js +54 -0
- package/dist/lib/session-key.d.ts +41 -0
- package/dist/lib/session-key.js +124 -0
- package/dist/lib/subscription-login.d.ts +15 -0
- package/dist/lib/subscription-login.js +28 -1
- package/dist/rc-key-401.mjs +75 -0
- package/dist/rc-proxy.mjs +221 -25
- package/dist/vendor/durable-contract/index.js +240 -18
- package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
- package/dist/vendor/shared/durable-contract/admission.js +533 -0
- package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
- package/dist/vendor/shared/durable-contract/arm.js +133 -0
- package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/canonical.js +98 -0
- package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
- package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
- package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
- package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
- package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
- package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
- package/dist/vendor/shared/durable-contract/codes.js +475 -0
- package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
- package/dist/vendor/shared/durable-contract/constants.js +179 -0
- package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/doors.js +249 -0
- package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
- package/dist/vendor/shared/durable-contract/envelope.js +317 -0
- package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
- package/dist/vendor/shared/durable-contract/events.js +630 -0
- package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
- package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
- package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
- package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
- package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
- package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
- package/dist/vendor/shared/durable-contract/index.js +37 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
- package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
- package/dist/vendor/shared/durable-contract/keys.js +66 -0
- package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
- package/dist/vendor/shared/durable-contract/lock.js +151 -0
- package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
- package/dist/vendor/shared/durable-contract/names.js +78 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
- package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
- package/dist/vendor/shared/durable-contract/paths.js +35 -0
- package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
- package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
- package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
- package/dist/vendor/shared/durable-contract/routes.js +315 -0
- package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
- package/dist/vendor/shared/durable-contract/schema.js +441 -0
- package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
- package/dist/vendor/shared/durable-contract/seal.js +99 -0
- package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
- package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
- package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
- package/dist/vendor/shared/durable-contract/session-id.js +58 -0
- package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
- package/dist/vendor/shared/durable-contract/spec.js +286 -0
- package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
- package/dist/vendor/shared/durable-contract/states.js +218 -0
- package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
- package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
- package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
- package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
- package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
- package/dist/vendor/shared/durable-contract/turn.js +81 -0
- package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
- package/dist/vendor/shared/durable-contract/vectors.js +208 -0
- package/package.json +4 -3
- package/plugins/integrity-manifest.json +35 -0
- package/plugins/mnemom/hooks/hooks.json +15 -1
- package/plugins/mnemom/hooks/register.tsx +60 -2
- package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
- package/dist/commands/agent-implicit-notice.d.ts +0 -31
- package/dist/commands/agent-implicit-notice.js +0 -73
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seat launcher: what a durable seat's tmux pane runs (BACKBONE C2 "tmux seats with pinned
|
|
3
|
+
* claude", C3 "lease key on stdin never env/disk"; C12-R16, R21, R27).
|
|
4
|
+
*
|
|
5
|
+
* node <cli>/dist/host/seat-launch-main.js --control <daemon.sock> --session <uuid> --epoch <n>
|
|
6
|
+
* --spawn-id <uuid> --conversation <uuid> --agent-id <uuid> --door <url> --cwd <dir>
|
|
7
|
+
* --cli-entry <cli>/dist/index.js -- <pinned claude> --resume <cc session> …
|
|
8
|
+
*
|
|
9
|
+
* 0. Resolves the owner's provider credential exactly as `mnemom agent` does (seat-credential.ts,
|
|
10
|
+
* dogfood B6). None resolves: the seat refuses with `mnemom agent`'s own message, before it takes
|
|
11
|
+
* the lease key.
|
|
12
|
+
* 1. Takes the lease key ONCE from the daemon's control socket (op seat_key; the daemon clears it).
|
|
13
|
+
* 2. Starts C3's hidden `mnemom agent host seat-proxy` with the seat ids and the control socket as
|
|
14
|
+
* its hello relay, and writes the key as one line on the proxy's stdin. The key is never in argv,
|
|
15
|
+
* env, a file or a log line.
|
|
16
|
+
* 3. Waits for the proxy's one stdout line `READY <port> <socket path>`.
|
|
17
|
+
* 4. Starts claude (stdio inherited, so it owns the pane) against `http://127.0.0.1:<port>` with
|
|
18
|
+
* `mnemom agent`'s own env arms (lib/subscription-login.ts launchChildEnv): the Anthropic key in
|
|
19
|
+
* ANTHROPIC_CUSTOM_HEADERS behind a placeholder token, or its claude.ai login untouched. The
|
|
20
|
+
* proxy passes that credential through and adds the lease key. The control socket path is never
|
|
21
|
+
* in claude's env.
|
|
22
|
+
* 5. Mirrors exits: claude's exit is the seat's (the daemon reads it as the pane's dead status);
|
|
23
|
+
* if the proxy exits first (dead key, C13-R7), claude is stopped and the proxy's code is the
|
|
24
|
+
* seat's.
|
|
25
|
+
*
|
|
26
|
+
* Pure over injected deps; seat-launch-main.ts is the thin driver.
|
|
27
|
+
*/
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
import { buildCustomHeaders } from "../lib/agent.js";
|
|
30
|
+
import { launchChildEnv } from "../lib/subscription-login.js";
|
|
31
|
+
/** How long the proxy has to print READY (it reads the key, takes the conversation lock, listens). */
|
|
32
|
+
export const READY_TIMEOUT_MS = 15_000;
|
|
33
|
+
const FLAGS = {
|
|
34
|
+
"--control": "control",
|
|
35
|
+
"--session": "session",
|
|
36
|
+
"--epoch": "epoch",
|
|
37
|
+
"--spawn-id": "spawnId",
|
|
38
|
+
"--conversation": "conversation",
|
|
39
|
+
"--agent-id": "agentId",
|
|
40
|
+
"--door": "door",
|
|
41
|
+
"--cwd": "cwd",
|
|
42
|
+
"--cli-entry": "cliEntry",
|
|
43
|
+
};
|
|
44
|
+
export function parseSeatLaunchArgs(argv) {
|
|
45
|
+
const raw = {};
|
|
46
|
+
let i = 0;
|
|
47
|
+
for (; i < argv.length && argv[i] !== "--"; i += 2) {
|
|
48
|
+
const flag = argv[i];
|
|
49
|
+
const key = FLAGS[flag];
|
|
50
|
+
if (!key)
|
|
51
|
+
return { ok: false, error: `unknown argument ${flag}` };
|
|
52
|
+
const v = argv[i + 1];
|
|
53
|
+
if (v === undefined)
|
|
54
|
+
return { ok: false, error: `${flag} needs a value` };
|
|
55
|
+
raw[key] = v;
|
|
56
|
+
}
|
|
57
|
+
for (const [flag, key] of Object.entries(FLAGS)) {
|
|
58
|
+
if (raw[key] === undefined)
|
|
59
|
+
return { ok: false, error: `${flag} is required` };
|
|
60
|
+
}
|
|
61
|
+
if (!/^\d{1,9}$/.test(raw.epoch))
|
|
62
|
+
return { ok: false, error: "--epoch must be a non-negative integer" };
|
|
63
|
+
const claude = argv.slice(i + 1);
|
|
64
|
+
if (argv[i] !== "--" || claude.length === 0)
|
|
65
|
+
return { ok: false, error: "no claude command after --" };
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
args: {
|
|
69
|
+
control: raw.control,
|
|
70
|
+
session: raw.session,
|
|
71
|
+
epoch: Number(raw.epoch),
|
|
72
|
+
spawnId: raw.spawnId,
|
|
73
|
+
conversation: raw.conversation,
|
|
74
|
+
agentId: raw.agentId,
|
|
75
|
+
door: raw.door,
|
|
76
|
+
cwd: raw.cwd,
|
|
77
|
+
cliEntry: raw.cliEntry,
|
|
78
|
+
claude,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** C3's seat-proxy argv (after the node binary): the seat ids, and the daemon socket as the hello relay. */
|
|
83
|
+
export function proxyArgv(a) {
|
|
84
|
+
return [
|
|
85
|
+
a.cliEntry,
|
|
86
|
+
"agent",
|
|
87
|
+
"host",
|
|
88
|
+
"seat-proxy",
|
|
89
|
+
"--conversation",
|
|
90
|
+
a.conversation,
|
|
91
|
+
"--agent-id",
|
|
92
|
+
a.agentId,
|
|
93
|
+
"--door",
|
|
94
|
+
a.door,
|
|
95
|
+
"--cwd",
|
|
96
|
+
a.cwd,
|
|
97
|
+
"--session",
|
|
98
|
+
a.session,
|
|
99
|
+
"--epoch",
|
|
100
|
+
String(a.epoch),
|
|
101
|
+
"--spawn-id",
|
|
102
|
+
a.spawnId,
|
|
103
|
+
"--hello-relay",
|
|
104
|
+
a.control,
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
const isAnthropicCred = (k) => k === "ANTHROPIC_API_KEY" ||
|
|
108
|
+
k === "ANTHROPIC_AUTH_TOKEN" ||
|
|
109
|
+
k === "ANTHROPIC_BASE_URL" ||
|
|
110
|
+
k === "ANTHROPIC_CUSTOM_HEADERS" ||
|
|
111
|
+
k === "MNEMOM_AGENT_ANTHROPIC_KEY";
|
|
112
|
+
function copyEnv(env, drop) {
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const [k, v] of Object.entries(env))
|
|
115
|
+
if (v !== undefined && !drop(k))
|
|
116
|
+
out[k] = v;
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The proxy reads its seat ids from argv; it gets no seat fact and no Anthropic credential from env
|
|
121
|
+
* (the provider credential reaches it only per request, from claude, and passes through).
|
|
122
|
+
*/
|
|
123
|
+
export const proxyEnv = (env) => copyEnv(env, (k) => k.startsWith("MNEMOM_SEAT_") || isAnthropicCred(k));
|
|
124
|
+
/**
|
|
125
|
+
* claude's env: `mnemom agent`'s own env for the resolved credential (launchChildEnv over
|
|
126
|
+
* buildCustomHeaders), pointed at the loopback proxy, plus the seat socket for the plugin's hello.
|
|
127
|
+
* The headers' x-mnemom-* lines are replaced by the proxy from the lease; their x-api-key line (the
|
|
128
|
+
* api-key arm) is what reaches the door. The plugin's hello hook runs `node`, so the directory of
|
|
129
|
+
* the node running this launcher goes LAST on PATH: a node already on PATH still wins, and a PATH
|
|
130
|
+
* without one (a version-manager shim that has since gone) still finds this one. Claude Code's
|
|
131
|
+
* gateway hint headers come with launchChildEnv, as for `mnemom agent`: without x-claude-code-request-class and
|
|
132
|
+
* x-claude-code-prompt-id the proxy can only chain a turn to the newest prompt already in the
|
|
133
|
+
* transcript, which for a seat's first typed turn is the launch prompt (dogfood B8).
|
|
134
|
+
*/
|
|
135
|
+
export function claudeEnv(env, port, socketPath, seat, nodeDir) {
|
|
136
|
+
const headers = buildCustomHeaders({
|
|
137
|
+
anthropicKey: seat.anthropicKey,
|
|
138
|
+
agent: seat.agentId,
|
|
139
|
+
agentId: seat.agentId,
|
|
140
|
+
conversationId: seat.conversation,
|
|
141
|
+
});
|
|
142
|
+
const base = copyEnv(env, (k) => k === "MNEMOM_SEAT_SOCKET");
|
|
143
|
+
const out = copyEnv(launchChildEnv(base, `http://127.0.0.1:${port}`, headers, seat.anthropicKey), () => false);
|
|
144
|
+
if (nodeDir) {
|
|
145
|
+
const dirs = (out.PATH ?? "").split(":").filter(Boolean);
|
|
146
|
+
if (!dirs.includes(nodeDir))
|
|
147
|
+
out.PATH = [...dirs, nodeDir].join(":");
|
|
148
|
+
}
|
|
149
|
+
return { ...out, MNEMOM_SEAT_SOCKET: socketPath };
|
|
150
|
+
}
|
|
151
|
+
export function parseReady(line) {
|
|
152
|
+
const m = /^READY (\d{1,5}) (\/.*)$/.exec(line);
|
|
153
|
+
if (!m)
|
|
154
|
+
return null;
|
|
155
|
+
const port = Number(m[1]);
|
|
156
|
+
if (port < 1 || port > 65535)
|
|
157
|
+
return null;
|
|
158
|
+
return { port, socketPath: m[2] };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The seat proxy's log: its per-request lines (`seat-proxy /v1/messages -> 200 …`) and its errors.
|
|
162
|
+
* The pane belongs to claude; a proxy line written to the tty is drawn over Claude Code's input box
|
|
163
|
+
* and reads like a half-typed prompt (dogfood cosmetic finding).
|
|
164
|
+
*/
|
|
165
|
+
export function seatProxyLogPath(logDir, session, spawnId) {
|
|
166
|
+
return path.join(logDir, `seat-proxy-${session}-${spawnId}.log`);
|
|
167
|
+
}
|
|
168
|
+
/** The proxy's stdio: stdin and stdout piped (key in, READY out), stderr to the log fd or dropped. */
|
|
169
|
+
export function seatProxyStdio(logFd) {
|
|
170
|
+
return ["pipe", "pipe", logFd ?? "ignore"];
|
|
171
|
+
}
|
|
172
|
+
/** Open the seat proxy's log (append, 0600, in the 0700 durable log dir); null when it cannot. */
|
|
173
|
+
export function openProxyLog(file, f) {
|
|
174
|
+
try {
|
|
175
|
+
f.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
176
|
+
return f.openSync(file, "a", 0o600);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* runSeatLaunch's deps over the process's own: the proxy's stderr goes to its log fd
|
|
184
|
+
* (seatProxyStdio), claude inherits the pane, and the log is named only when it opened.
|
|
185
|
+
*/
|
|
186
|
+
export function seatLaunchDeps(io) {
|
|
187
|
+
return {
|
|
188
|
+
env: io.env,
|
|
189
|
+
execPath: io.execPath,
|
|
190
|
+
credential: io.credential,
|
|
191
|
+
request: io.request,
|
|
192
|
+
spawn: (cmd, args, opts) => io.spawn(cmd, [...args], {
|
|
193
|
+
env: opts.env,
|
|
194
|
+
stdio: opts.role === "proxy" ? seatProxyStdio(io.proxyLogFd) : "inherit",
|
|
195
|
+
}),
|
|
196
|
+
stderr: io.stderr,
|
|
197
|
+
...(io.proxyLogFd !== null ? { proxyLog: io.proxyLog } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const SIGNALS = {
|
|
201
|
+
SIGHUP: 1,
|
|
202
|
+
SIGINT: 2,
|
|
203
|
+
SIGKILL: 9,
|
|
204
|
+
SIGTERM: 15,
|
|
205
|
+
};
|
|
206
|
+
const exitStatus = (code, signal) => code !== null ? code : 128 + (signal ? (SIGNALS[signal] ?? 0) : 0);
|
|
207
|
+
function waitReady(proxy, timeoutMs) {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
let buf = "";
|
|
210
|
+
let done = false;
|
|
211
|
+
const finish = (r) => {
|
|
212
|
+
if (done)
|
|
213
|
+
return;
|
|
214
|
+
done = true;
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
resolve(r);
|
|
217
|
+
};
|
|
218
|
+
const timer = setTimeout(() => finish({ kind: "timeout" }), timeoutMs);
|
|
219
|
+
proxy.stdout?.on("data", (d) => {
|
|
220
|
+
buf += d.toString();
|
|
221
|
+
for (let i = buf.indexOf("\n"); i >= 0; i = buf.indexOf("\n")) {
|
|
222
|
+
const r = parseReady(buf.slice(0, i));
|
|
223
|
+
buf = buf.slice(i + 1);
|
|
224
|
+
if (r)
|
|
225
|
+
return finish({ kind: "ready", ...r });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
proxy.on("exit", (code) => finish({ kind: "exit", code }));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
export async function runSeatLaunch(a, d) {
|
|
232
|
+
const say = (s) => d.stderr(`mnemom seat: ${s}\n`);
|
|
233
|
+
const cred = await d
|
|
234
|
+
.credential()
|
|
235
|
+
.catch((e) => ({ ok: false, message: e.message }));
|
|
236
|
+
if (!cred.ok) {
|
|
237
|
+
d.stderr(`${cred.message}\n`);
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
const reply = await d
|
|
241
|
+
.request(a.control, { op: "seat_key", session_id: a.session, spawn_id: a.spawnId })
|
|
242
|
+
.catch(() => null);
|
|
243
|
+
const key = reply && reply.ok === true && typeof reply.lease_key === "string" ? reply.lease_key : null;
|
|
244
|
+
if (!key) {
|
|
245
|
+
say("no lease key from the host daemon");
|
|
246
|
+
return 1;
|
|
247
|
+
}
|
|
248
|
+
let proxy;
|
|
249
|
+
try {
|
|
250
|
+
proxy = d.spawn(d.execPath, proxyArgv(a), { env: proxyEnv(d.env), role: "proxy" });
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
say(`could not start the seat proxy: ${e.message}`);
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
let proxyExit = null;
|
|
257
|
+
const proxyExited = new Promise((resolve) => proxy.on("exit", (code, signal) => {
|
|
258
|
+
proxyExit = exitStatus(code, signal);
|
|
259
|
+
resolve(proxyExit);
|
|
260
|
+
}));
|
|
261
|
+
const ready = waitReady(proxy, d.readyTimeoutMs ?? READY_TIMEOUT_MS);
|
|
262
|
+
proxy.stdin?.end(`${key}\n`);
|
|
263
|
+
const r = await ready;
|
|
264
|
+
if (r.kind === "exit") {
|
|
265
|
+
say(`the seat proxy exited before it was ready (exit ${r.code ?? "signal"})${d.proxyLog ? `; see ${d.proxyLog}` : ""}`);
|
|
266
|
+
return r.code && r.code > 0 ? r.code : 1;
|
|
267
|
+
}
|
|
268
|
+
if (r.kind === "timeout") {
|
|
269
|
+
say("the seat proxy was not ready in time");
|
|
270
|
+
proxy.kill("SIGKILL");
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
const [cmd, ...rest] = a.claude;
|
|
274
|
+
let claude;
|
|
275
|
+
try {
|
|
276
|
+
claude = d.spawn(cmd, rest, {
|
|
277
|
+
env: claudeEnv(d.env, r.port, r.socketPath, { agentId: a.agentId, conversation: a.conversation, anthropicKey: cred.anthropicKey }, path.dirname(d.execPath)),
|
|
278
|
+
role: "claude",
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
say(`could not start claude: ${e.message}`);
|
|
283
|
+
proxy.kill("SIGTERM");
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
let claudeDone = false;
|
|
287
|
+
const claudeExited = new Promise((resolve) => claude.on("exit", (code, signal) => {
|
|
288
|
+
claudeDone = true;
|
|
289
|
+
resolve(exitStatus(code, signal));
|
|
290
|
+
}));
|
|
291
|
+
void proxyExited.then((code) => {
|
|
292
|
+
if (claudeDone)
|
|
293
|
+
return;
|
|
294
|
+
say(`the seat proxy exited (exit ${code}); stopping claude`);
|
|
295
|
+
claude.kill("SIGTERM");
|
|
296
|
+
});
|
|
297
|
+
const claudeCode = await claudeExited;
|
|
298
|
+
if (proxyExit !== null)
|
|
299
|
+
return proxyExit;
|
|
300
|
+
proxy.kill("SIGTERM");
|
|
301
|
+
return claudeCode;
|
|
302
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SessionItem } from "./client.js";
|
|
2
|
+
/** The seat launcher's driver, relative to the CLI entry's directory (dist/index.js -> dist/host/…). */
|
|
3
|
+
export declare const SEAT_LAUNCH_MAIN = "host/seat-launch-main.js";
|
|
4
|
+
/** Env keys a seat inherits from the daemon, when set; everything else is dropped (secret scope). */
|
|
5
|
+
export declare const SEAT_ENV_PASS: string[];
|
|
6
|
+
export declare function seatEnv(env: Readonly<Record<string, string | undefined>>, extra: Readonly<Record<string, string>>): Record<string, string>;
|
|
7
|
+
/** POSIX single-quote one token. */
|
|
8
|
+
/**
|
|
9
|
+
* The door a seat's proxy forwards to: the same Anthropic door an attended launch uses
|
|
10
|
+
* (lib/agent resolveAnthropicDoor), with `cellGateway` as the gateway host when
|
|
11
|
+
* MNEMOM_AGENT_GATEWAY is unset. C3's proxy joins door path + request path, so the door carries
|
|
12
|
+
* the `/anthropic` path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function seatDoor(env: Readonly<Record<string, string | undefined>>, cellGateway: string): string;
|
|
15
|
+
export declare const shq: (s: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* The owner's own default permission mode (`permissions.defaultMode` in the Claude Code user
|
|
18
|
+
* settings the seat reads), or null when it is unset or unreadable.
|
|
19
|
+
*/
|
|
20
|
+
export declare function seatUserDefaultMode(env: Readonly<Record<string, string | undefined>>): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The pinned claude argv for a resume (C4 owns the full renderer; M1 seat flags only).
|
|
23
|
+
*
|
|
24
|
+
* When the owner's user settings set a default mode other than auto, the seat restates that same
|
|
25
|
+
* mode as a --settings source: Claude Code offers its one-time "make auto mode your default?"
|
|
26
|
+
* dialog only when no settings source but the user's sets a mode, and nobody is at a seat to
|
|
27
|
+
* answer it. The mode the seat runs in is unchanged.
|
|
28
|
+
*/
|
|
29
|
+
export declare function claudeArgv(claudeBin: string, item: SessionItem, pluginDir: string | null, newSession?: boolean, userDefaultMode?: string | null): string[];
|
|
30
|
+
export interface SeatLaunch {
|
|
31
|
+
readonly command: string;
|
|
32
|
+
readonly env: Record<string, string>;
|
|
33
|
+
readonly specPath: string;
|
|
34
|
+
readonly specSha256: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function buildSeatLaunch(opts: {
|
|
37
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
38
|
+
cliExec: readonly string[];
|
|
39
|
+
claudeBin: string;
|
|
40
|
+
pluginDir: string | null;
|
|
41
|
+
item: SessionItem;
|
|
42
|
+
epoch: number;
|
|
43
|
+
spawnId: string;
|
|
44
|
+
newSession?: boolean;
|
|
45
|
+
specDir: string;
|
|
46
|
+
controlSocket: string;
|
|
47
|
+
door: string;
|
|
48
|
+
}): SeatLaunch;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the daemon launches one seat (BACKBONE C2 "tmux seats with pinned claude"; C3 seat proxy;
|
|
3
|
+
* C12-R10, R16, R21, R23, R27, R35).
|
|
4
|
+
*
|
|
5
|
+
* The pane execs the seat launcher (seat-launch.ts): it takes the lease key once over the control
|
|
6
|
+
* socket, starts C3's hidden `mnemom agent host seat-proxy` with the key on stdin, then the pinned
|
|
7
|
+
* claude with `--resume <cc_session_id>` against the proxy's loopback port. The launch-spec file
|
|
8
|
+
* (`spec_path`, host status) is the session's committed launch spec, one file per session, so its
|
|
9
|
+
* sha256 is unchanged across respawns. Non-secret seat facts ride as plain env; the control socket
|
|
10
|
+
* path is in the launcher's argv only. No secret is in argv, env or any file here.
|
|
11
|
+
*/
|
|
12
|
+
import { SEAT_TEST_ENV_PASS } from "../durable/test-mode.mjs";
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import { canonicalJson } from "../vendor/shared/durable-contract/canonical.js";
|
|
17
|
+
import { resolveAnthropicDoor } from "../lib/agent.js";
|
|
18
|
+
/** The seat launcher's driver, relative to the CLI entry's directory (dist/index.js -> dist/host/…). */
|
|
19
|
+
export const SEAT_LAUNCH_MAIN = "host/seat-launch-main.js";
|
|
20
|
+
/** Env keys a seat inherits from the daemon, when set; everything else is dropped (secret scope). */
|
|
21
|
+
export const SEAT_ENV_PASS = [
|
|
22
|
+
"PATH",
|
|
23
|
+
"HOME",
|
|
24
|
+
"USER",
|
|
25
|
+
"LOGNAME",
|
|
26
|
+
"LANG",
|
|
27
|
+
"TMPDIR",
|
|
28
|
+
"CLAUDE_CONFIG_DIR",
|
|
29
|
+
"MNEMOM_API_URL",
|
|
30
|
+
"MNEMOM_GATEWAY_URL",
|
|
31
|
+
"MNEMOM_AGENT_GATEWAY",
|
|
32
|
+
"DISABLE_AUTOUPDATER",
|
|
33
|
+
"MNEMOM_DISABLE_AUTOUPDATE",
|
|
34
|
+
"MNEMOM_UPDATE_MODE",
|
|
35
|
+
...SEAT_TEST_ENV_PASS,
|
|
36
|
+
];
|
|
37
|
+
export function seatEnv(env, extra) {
|
|
38
|
+
const out = { TERM: "xterm-256color" };
|
|
39
|
+
for (const k of SEAT_ENV_PASS) {
|
|
40
|
+
const v = env[k];
|
|
41
|
+
if (v)
|
|
42
|
+
out[k] = v;
|
|
43
|
+
}
|
|
44
|
+
return { ...out, ...extra };
|
|
45
|
+
}
|
|
46
|
+
/** POSIX single-quote one token. */
|
|
47
|
+
/**
|
|
48
|
+
* The door a seat's proxy forwards to: the same Anthropic door an attended launch uses
|
|
49
|
+
* (lib/agent resolveAnthropicDoor), with `cellGateway` as the gateway host when
|
|
50
|
+
* MNEMOM_AGENT_GATEWAY is unset. C3's proxy joins door path + request path, so the door carries
|
|
51
|
+
* the `/anthropic` path.
|
|
52
|
+
*/
|
|
53
|
+
export function seatDoor(env, cellGateway) {
|
|
54
|
+
const agentGateway = (env.MNEMOM_AGENT_GATEWAY ?? "").trim() || cellGateway;
|
|
55
|
+
return resolveAnthropicDoor({ ...env, MNEMOM_AGENT_GATEWAY: agentGateway });
|
|
56
|
+
}
|
|
57
|
+
export const shq = (s) => /^[A-Za-z0-9_./:=@%+-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
|
|
58
|
+
/**
|
|
59
|
+
* The owner's own default permission mode (`permissions.defaultMode` in the Claude Code user
|
|
60
|
+
* settings the seat reads), or null when it is unset or unreadable.
|
|
61
|
+
*/
|
|
62
|
+
export function seatUserDefaultMode(env) {
|
|
63
|
+
const dir = env.CLAUDE_CONFIG_DIR || (env.HOME ? path.join(env.HOME, ".claude") : "");
|
|
64
|
+
if (!dir)
|
|
65
|
+
return null;
|
|
66
|
+
try {
|
|
67
|
+
const mode = JSON.parse(fs.readFileSync(path.join(dir, "settings.json"), "utf8"))?.permissions
|
|
68
|
+
?.defaultMode;
|
|
69
|
+
return typeof mode === "string" && mode ? mode : null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The pinned claude argv for a resume (C4 owns the full renderer; M1 seat flags only).
|
|
77
|
+
*
|
|
78
|
+
* When the owner's user settings set a default mode other than auto, the seat restates that same
|
|
79
|
+
* mode as a --settings source: Claude Code offers its one-time "make auto mode your default?"
|
|
80
|
+
* dialog only when no settings source but the user's sets a mode, and nobody is at a seat to
|
|
81
|
+
* answer it. The mode the seat runs in is unchanged.
|
|
82
|
+
*/
|
|
83
|
+
export function claudeArgv(claudeBin, item, pluginDir, newSession = false, userDefaultMode = null) {
|
|
84
|
+
// B5a: a born-locked session's first spawn starts its CC session under the registered id.
|
|
85
|
+
const argv = [claudeBin, newSession ? "--session-id" : "--resume", item.cc_session_id ?? ""];
|
|
86
|
+
if (pluginDir)
|
|
87
|
+
argv.push("--plugin-dir", pluginDir);
|
|
88
|
+
const spec = item.spec ?? {};
|
|
89
|
+
if (typeof spec.model === "string")
|
|
90
|
+
argv.push("--model", spec.model);
|
|
91
|
+
if (typeof spec.effort === "string")
|
|
92
|
+
argv.push("--effort", spec.effort);
|
|
93
|
+
if (userDefaultMode && userDefaultMode !== "auto")
|
|
94
|
+
argv.push("--settings", JSON.stringify({ permissions: { defaultMode: userDefaultMode } }));
|
|
95
|
+
return argv;
|
|
96
|
+
}
|
|
97
|
+
export function buildSeatLaunch(opts) {
|
|
98
|
+
const { item } = opts;
|
|
99
|
+
const spec = item.spec ?? { v: 1, cwd: item.cwd, plugin: "bundled-mnemom", trim_mcp: false };
|
|
100
|
+
fs.mkdirSync(opts.specDir, { recursive: true, mode: 0o700 });
|
|
101
|
+
const specPath = path.join(opts.specDir, `${item.id}.json`);
|
|
102
|
+
const text = `${canonicalJson(spec)}\n`;
|
|
103
|
+
let cur;
|
|
104
|
+
try {
|
|
105
|
+
cur = fs.readFileSync(specPath, "utf8");
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
cur = null;
|
|
109
|
+
}
|
|
110
|
+
if (cur !== text)
|
|
111
|
+
fs.writeFileSync(specPath, text, { mode: 0o600 });
|
|
112
|
+
const [node, entry] = [opts.cliExec[0] ?? process.execPath, opts.cliExec[1] ?? ""];
|
|
113
|
+
const launcher = [
|
|
114
|
+
node,
|
|
115
|
+
path.join(path.dirname(entry), SEAT_LAUNCH_MAIN),
|
|
116
|
+
"--control",
|
|
117
|
+
opts.controlSocket,
|
|
118
|
+
"--session",
|
|
119
|
+
item.id,
|
|
120
|
+
"--epoch",
|
|
121
|
+
String(opts.epoch),
|
|
122
|
+
"--spawn-id",
|
|
123
|
+
opts.spawnId,
|
|
124
|
+
"--conversation",
|
|
125
|
+
item.conversation_id ?? item.id,
|
|
126
|
+
"--agent-id",
|
|
127
|
+
item.agent_id ?? "",
|
|
128
|
+
"--door",
|
|
129
|
+
opts.door,
|
|
130
|
+
"--cwd",
|
|
131
|
+
item.cwd ?? "",
|
|
132
|
+
"--cli-entry",
|
|
133
|
+
entry,
|
|
134
|
+
"--",
|
|
135
|
+
...claudeArgv(opts.claudeBin, item, opts.pluginDir, opts.newSession === true, seatUserDefaultMode(opts.env)),
|
|
136
|
+
];
|
|
137
|
+
const env = seatEnv(opts.env, {
|
|
138
|
+
MNEMOM_SEAT_SESSION_ID: item.id,
|
|
139
|
+
MNEMOM_SEAT_SPAWN_ID: opts.spawnId,
|
|
140
|
+
MNEMOM_SEAT_EPOCH: String(opts.epoch),
|
|
141
|
+
MNEMOM_SEAT_SPEC: specPath,
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
command: `exec ${launcher.map(shq).join(" ")}`,
|
|
145
|
+
env,
|
|
146
|
+
specPath,
|
|
147
|
+
specSha256: createHash("sha256").update(text).digest("hex"),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ExecutorState } from "../vendor/shared/durable-contract/executor-state.js";
|
|
2
|
+
/** What a readable state file yields; boot_id null when missing or unusable. */
|
|
3
|
+
export interface LoadedState {
|
|
4
|
+
readonly executor_id: string;
|
|
5
|
+
readonly executor_key: string;
|
|
6
|
+
readonly nonce: string;
|
|
7
|
+
readonly boot_id: string | null;
|
|
8
|
+
}
|
|
9
|
+
export type ReadResult = {
|
|
10
|
+
kind: "absent";
|
|
11
|
+
} | {
|
|
12
|
+
kind: "corrupt";
|
|
13
|
+
reason: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "ok";
|
|
16
|
+
state: LoadedState;
|
|
17
|
+
};
|
|
18
|
+
export declare function readExecutorState(file: string): ReadResult;
|
|
19
|
+
export declare function parseExecutorState(raw: unknown): ReadResult;
|
|
20
|
+
/** A fresh random nonce (A1 accepts 1..128 chars). */
|
|
21
|
+
export declare function newNonce(): string;
|
|
22
|
+
/**
|
|
23
|
+
* C12-R06: keep the nonce only when the recorded boot id equals the OS boot id; otherwise (a
|
|
24
|
+
* reboot, a restore, a copied file, or a missing/corrupt boot id) mint a new one.
|
|
25
|
+
*/
|
|
26
|
+
export declare function bindNonce(loaded: LoadedState, osBootId: string, mint?: () => string): {
|
|
27
|
+
state: ExecutorState;
|
|
28
|
+
rotated: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** True when the file on disk already holds exactly `state` in the frozen shape (no rewrite needed). */
|
|
31
|
+
export declare function stateFileCurrent(file: string, state: ExecutorState): boolean;
|
|
32
|
+
/** Write the frozen shape: dir 0700, file 0600, tmp+rename (the lock is elsewhere, C12-R03). */
|
|
33
|
+
export declare function writeExecutorState(file: string, state: ExecutorState): void;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The executor state file (S1 EXECUTOR_STATE_FILE; C12-R05, R06; ruling 24.14).
|
|
3
|
+
*
|
|
4
|
+
* The file is the only home of the executor key. It is written 0600 in a 0700 directory. The
|
|
5
|
+
* single-instance lock lives on a separate file (lock.ts), so writing this file by tmp+rename
|
|
6
|
+
* never drops the lock (C12-R03).
|
|
7
|
+
*
|
|
8
|
+
* Reading is lenient about extra keys (an enrollment written by an older build, or a test
|
|
9
|
+
* fixture) but strict about the identity triple (executor_id, executor_key, nonce). A missing or
|
|
10
|
+
* corrupt boot_id is not an error: bindNonce mints a new nonce for it (C12-R06 fail safe), and
|
|
11
|
+
* the file is rewritten in the frozen S1 shape.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
16
|
+
import { EXECUTOR_STATE_VERSION, executorStateOk, } from "../vendor/shared/durable-contract/executor-state.js";
|
|
17
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
|
+
export function readExecutorState(file) {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = fs.readFileSync(file, "utf8");
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e.code === "ENOENT")
|
|
25
|
+
return { kind: "absent" };
|
|
26
|
+
return { kind: "corrupt", reason: `unreadable: ${e.message}` };
|
|
27
|
+
}
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { kind: "corrupt", reason: "not JSON" };
|
|
34
|
+
}
|
|
35
|
+
return parseExecutorState(raw);
|
|
36
|
+
}
|
|
37
|
+
export function parseExecutorState(raw) {
|
|
38
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
39
|
+
return { kind: "corrupt", reason: "not an object" };
|
|
40
|
+
const o = raw;
|
|
41
|
+
if (typeof o.executor_id !== "string" || !UUID_RE.test(o.executor_id))
|
|
42
|
+
return { kind: "corrupt", reason: "executor_id" };
|
|
43
|
+
if (typeof o.executor_key !== "string" || o.executor_key === "")
|
|
44
|
+
return { kind: "corrupt", reason: "executor_key" };
|
|
45
|
+
const nonce = typeof o.nonce === "string" && o.nonce.length >= 1 && o.nonce.length <= 128 ? o.nonce : "";
|
|
46
|
+
const boot = typeof o.boot_id === "string" && o.boot_id.trim() !== "" ? o.boot_id : null;
|
|
47
|
+
return {
|
|
48
|
+
kind: "ok",
|
|
49
|
+
state: {
|
|
50
|
+
executor_id: o.executor_id.toLowerCase(),
|
|
51
|
+
executor_key: o.executor_key,
|
|
52
|
+
nonce,
|
|
53
|
+
boot_id: nonce ? boot : null,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** A fresh random nonce (A1 accepts 1..128 chars). */
|
|
58
|
+
export function newNonce() {
|
|
59
|
+
return `n-${randomBytes(24).toString("hex")}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* C12-R06: keep the nonce only when the recorded boot id equals the OS boot id; otherwise (a
|
|
63
|
+
* reboot, a restore, a copied file, or a missing/corrupt boot id) mint a new one.
|
|
64
|
+
*/
|
|
65
|
+
export function bindNonce(loaded, osBootId, mint = newNonce) {
|
|
66
|
+
const keep = loaded.boot_id !== null && loaded.nonce !== "" && loaded.boot_id === osBootId;
|
|
67
|
+
return {
|
|
68
|
+
state: {
|
|
69
|
+
version: EXECUTOR_STATE_VERSION,
|
|
70
|
+
executor_id: loaded.executor_id,
|
|
71
|
+
executor_key: loaded.executor_key,
|
|
72
|
+
nonce: keep ? loaded.nonce : mint(),
|
|
73
|
+
boot_id: osBootId,
|
|
74
|
+
},
|
|
75
|
+
rotated: !keep,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** True when the file on disk already holds exactly `state` in the frozen shape (no rewrite needed). */
|
|
79
|
+
export function stateFileCurrent(file, state) {
|
|
80
|
+
try {
|
|
81
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
82
|
+
return (executorStateOk(raw) &&
|
|
83
|
+
raw.nonce === state.nonce &&
|
|
84
|
+
raw.boot_id === state.boot_id &&
|
|
85
|
+
raw.executor_key === state.executor_key &&
|
|
86
|
+
raw.executor_id === state.executor_id);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Write the frozen shape: dir 0700, file 0600, tmp+rename (the lock is elsewhere, C12-R03). */
|
|
93
|
+
export function writeExecutorState(file, state) {
|
|
94
|
+
if (!executorStateOk(state))
|
|
95
|
+
throw new Error("refusing to write an executor state outside the S1 shape");
|
|
96
|
+
const dir = path.dirname(file);
|
|
97
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
98
|
+
fs.chmodSync(dir, 0o700);
|
|
99
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
100
|
+
const body = `${JSON.stringify({
|
|
101
|
+
version: state.version,
|
|
102
|
+
executor_id: state.executor_id,
|
|
103
|
+
executor_key: state.executor_key,
|
|
104
|
+
nonce: state.nonce,
|
|
105
|
+
boot_id: state.boot_id,
|
|
106
|
+
}, null, 2)}\n`;
|
|
107
|
+
const fd = fs.openSync(tmp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, 0o600);
|
|
108
|
+
try {
|
|
109
|
+
fs.fchmodSync(fd, 0o600);
|
|
110
|
+
fs.writeSync(fd, body);
|
|
111
|
+
fs.fsyncSync(fd);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
fs.closeSync(fd);
|
|
115
|
+
}
|
|
116
|
+
fs.renameSync(tmp, file);
|
|
117
|
+
}
|