@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,76 @@
|
|
|
1
|
+
// GENERATED from shared/durable-contract/src/secret-scope.ts by cli/scripts/sync-durable-contract.mjs; do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* The secret scope (BACKBONE line 69; C04-P01; wave-0 fix ruling 2), frozen as data so every lane
|
|
4
|
+
* greps the same list: which secrets, in which encodings, may appear on which surfaces. None may
|
|
5
|
+
* appear on any surface, with one allowance: the executor key in EXECUTOR_STATE_FILE (mode 0600).
|
|
6
|
+
* The owner's own files in `excluded_paths` are outside the scan. Paths are HOME-relative.
|
|
7
|
+
*/
|
|
8
|
+
import { EXECUTOR_STATE_FILE } from "./paths.js";
|
|
9
|
+
export const SECRET_KINDS = [
|
|
10
|
+
"owner_jwt",
|
|
11
|
+
"owner_refresh_token",
|
|
12
|
+
/** The owner's mnm_ API key (canaries). */
|
|
13
|
+
"owner_api_key",
|
|
14
|
+
/** Every mnm_ds_ lease key. */
|
|
15
|
+
"lease_key",
|
|
16
|
+
"executor_key",
|
|
17
|
+
"enroll_poll_secret",
|
|
18
|
+
];
|
|
19
|
+
export const SECRET_ENCODINGS = [
|
|
20
|
+
"raw",
|
|
21
|
+
"base64",
|
|
22
|
+
"base64url",
|
|
23
|
+
"hex",
|
|
24
|
+
"percent",
|
|
25
|
+
"json_escaped",
|
|
26
|
+
];
|
|
27
|
+
/** Where no secret may appear. */
|
|
28
|
+
export const SECRET_SURFACES = [
|
|
29
|
+
/** Files written by the daemon, proxy, host install and enroll: state, lock, plist, logs, any ~/.mnemom file. */
|
|
30
|
+
"host_files",
|
|
31
|
+
/** env of the daemon, proxy and seat process tree. */
|
|
32
|
+
"process_env",
|
|
33
|
+
/** argv of the daemon, proxy and seat process tree. */
|
|
34
|
+
"process_argv",
|
|
35
|
+
"tmux_global_env",
|
|
36
|
+
"tmux_session_env",
|
|
37
|
+
"tmux_scrollback",
|
|
38
|
+
"tmpdir_files",
|
|
39
|
+
"seat_transcripts",
|
|
40
|
+
"durable_spans",
|
|
41
|
+
"durable_logs",
|
|
42
|
+
];
|
|
43
|
+
export const SECRET_SCOPE = Object.freeze({
|
|
44
|
+
secrets: SECRET_KINDS,
|
|
45
|
+
encodings: SECRET_ENCODINGS,
|
|
46
|
+
surfaces: SECRET_SURFACES,
|
|
47
|
+
allow: Object.freeze([
|
|
48
|
+
Object.freeze({ secret: "executor_key", path: EXECUTOR_STATE_FILE }),
|
|
49
|
+
]),
|
|
50
|
+
excluded_paths: Object.freeze([".mnemom/auth.json", ".mnemom/launcher.toml"]),
|
|
51
|
+
});
|
|
52
|
+
const toHex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
53
|
+
function toBase64(b) {
|
|
54
|
+
let bin = "";
|
|
55
|
+
for (const x of b)
|
|
56
|
+
bin += String.fromCharCode(x);
|
|
57
|
+
return btoa(bin);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Every encoded form of one secret value the scan greps for, keyed by encoding (UTF-8 bytes;
|
|
61
|
+
* base64 and base64url without padding variants, so a padded copy still matches as a substring).
|
|
62
|
+
*/
|
|
63
|
+
export function secretEncodings(value) {
|
|
64
|
+
if (value.length === 0)
|
|
65
|
+
throw new RangeError("an empty secret matches everything");
|
|
66
|
+
const bytes = new TextEncoder().encode(value);
|
|
67
|
+
const b64 = toBase64(bytes).replace(/=+$/, "");
|
|
68
|
+
return {
|
|
69
|
+
raw: value,
|
|
70
|
+
base64: b64,
|
|
71
|
+
base64url: b64.replace(/\+/g, "-").replace(/\//g, "_"),
|
|
72
|
+
hex: toHex(bytes),
|
|
73
|
+
percent: encodeURIComponent(value),
|
|
74
|
+
json_escaped: JSON.stringify(value).slice(1, -1),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deriveSessionId: the conversation-scoped arm of gateway/src/index.ts deriveSessionId, ported for
|
|
3
|
+
* agent_sessions.aip_session_id (stored once at create, C01-R23; returned verbatim by the resolver,
|
|
4
|
+
* C06-R26): `${agentHash}-c<first 32 hex of sha256(agentHash|cid)>`.
|
|
5
|
+
*
|
|
6
|
+
* Caller bytes are hashed, never used raw: session ids are interpolated into PostgREST filters.
|
|
7
|
+
* The durable arm is always conversation-scoped, so a blank conversation id (which would fall back
|
|
8
|
+
* to the gateway's hour bucket) is refused here instead of silently bucketing.
|
|
9
|
+
* conversation_id is trimmed exactly as the gateway trims the header (String.prototype.trim).
|
|
10
|
+
*/
|
|
11
|
+
export declare const SESSION_ID_HASH_HEX_CHARS = 32;
|
|
12
|
+
export declare function deriveSessionId(agentHash: string, conversationId: string): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Golden vectors, computed independently with node:crypto (see __tests__/session-id.test.ts) and
|
|
15
|
+
* pinned here. Keyed on (agents.agent_hash at create, conversation_id).
|
|
16
|
+
*/
|
|
17
|
+
export declare const SESSION_ID_VECTORS: readonly {
|
|
18
|
+
agent_hash: string;
|
|
19
|
+
conversation_id: string;
|
|
20
|
+
aip_session_id: string;
|
|
21
|
+
}[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// GENERATED from shared/durable-contract/src/session-id.ts by cli/scripts/sync-durable-contract.mjs; do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* deriveSessionId: the conversation-scoped arm of gateway/src/index.ts deriveSessionId, ported for
|
|
4
|
+
* agent_sessions.aip_session_id (stored once at create, C01-R23; returned verbatim by the resolver,
|
|
5
|
+
* C06-R26): `${agentHash}-c<first 32 hex of sha256(agentHash|cid)>`.
|
|
6
|
+
*
|
|
7
|
+
* Caller bytes are hashed, never used raw: session ids are interpolated into PostgREST filters.
|
|
8
|
+
* The durable arm is always conversation-scoped, so a blank conversation id (which would fall back
|
|
9
|
+
* to the gateway's hour bucket) is refused here instead of silently bucketing.
|
|
10
|
+
* conversation_id is trimmed exactly as the gateway trims the header (String.prototype.trim).
|
|
11
|
+
*/
|
|
12
|
+
import { sha256Hex } from "./canonical.js";
|
|
13
|
+
export const SESSION_ID_HASH_HEX_CHARS = 32;
|
|
14
|
+
export async function deriveSessionId(agentHash, conversationId) {
|
|
15
|
+
if (typeof agentHash !== "string" || agentHash.length === 0)
|
|
16
|
+
throw new RangeError("agentHash is required");
|
|
17
|
+
const cid = typeof conversationId === "string" ? conversationId.trim() : "";
|
|
18
|
+
if (cid.length === 0)
|
|
19
|
+
throw new RangeError("conversation id is blank: the durable arm never hour-buckets");
|
|
20
|
+
const digest = await sha256Hex(`${agentHash}|${cid}`);
|
|
21
|
+
return `${agentHash}-c${digest.slice(0, SESSION_ID_HASH_HEX_CHARS)}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Golden vectors, computed independently with node:crypto (see __tests__/session-id.test.ts) and
|
|
25
|
+
* pinned here. Keyed on (agents.agent_hash at create, conversation_id).
|
|
26
|
+
*/
|
|
27
|
+
export const SESSION_ID_VECTORS = [
|
|
28
|
+
{
|
|
29
|
+
agent_hash: "smolt-0a1b2c3d",
|
|
30
|
+
conversation_id: "6f1c9a2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b",
|
|
31
|
+
aip_session_id: "smolt-0a1b2c3d-c491fc4d06c067b7baa0083400cdcb97d",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
agent_hash: "smolt-0a1b2c3d",
|
|
35
|
+
conversation_id: "6f1c9a2e-3b4d-4e5f-8a9b-0c1d2e3f4a5c",
|
|
36
|
+
aip_session_id: "smolt-0a1b2c3d-cb2b4ab1e1da376e82b4059635d4f74d6",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
agent_hash: "smolt-ffffffff",
|
|
40
|
+
conversation_id: "00000000-0000-4000-8000-000000000000",
|
|
41
|
+
aip_session_id: "smolt-ffffffff-c645a4fdd38c8d94d2fbcace132e5aa3d",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
agent_hash: "smolt-12345678",
|
|
45
|
+
conversation_id: " 6f1c9a2e-3b4d-4e5f-8a9b-0c1d2e3f4a5b\n",
|
|
46
|
+
aip_session_id: "smolt-12345678-c9e54f7096bc5b7d8f4b7c02ee5d8cc7e",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
agent_hash: "smolt-12345678",
|
|
50
|
+
conversation_id: "\u00a0conv-\u00e9-\u2713\ufeff",
|
|
51
|
+
aip_session_id: "smolt-12345678-c5401c9673d20a9788cb8750950cb5d2d",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
agent_hash: "smolt-deadbeef",
|
|
55
|
+
conversation_id: "a|b",
|
|
56
|
+
aip_session_id: "smolt-deadbeef-ccec66b82e597a4ac96a596b3c67e0696",
|
|
57
|
+
},
|
|
58
|
+
];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The closed launch-spec schema, the canonical spec hash, the seat argv allowlist and the C4
|
|
3
|
+
* builder input table (C14-R01..R05, R13; C02-R26; BACKBONE C4, amendments 31, 43; final F01, F09).
|
|
4
|
+
*
|
|
5
|
+
* Frozen choices (C14-R01 asks S1 before the oracle):
|
|
6
|
+
* - argv level compared: the claude-level execve argv;
|
|
7
|
+
* - token order: canonical multiset of (flag, value) pairs (canonicalArgvPairs); exact bytes only
|
|
8
|
+
* for verified spec vs actual execve;
|
|
9
|
+
* - `--name` is NOT allowlisted (dropped and named);
|
|
10
|
+
* - the yolo-default bypass flag counts as dropped and is named (C14-R12).
|
|
11
|
+
*/
|
|
12
|
+
import { PLUGIN_HASH_RECIPE_VERSION } from "./constants.js";
|
|
13
|
+
export declare const SPEC_VERSION = 1;
|
|
14
|
+
export declare const PLUGIN_TOKEN = "bundled-mnemom";
|
|
15
|
+
export declare const EFFORT_VALUES: readonly ["low", "medium", "high", "xhigh", "max"];
|
|
16
|
+
export declare const MODEL_RE: RegExp;
|
|
17
|
+
/** The committed, goal-elided launch spec. No remote-control field, no path or hash for the plugin. */
|
|
18
|
+
export interface LaunchSpec {
|
|
19
|
+
v: 1;
|
|
20
|
+
/** The registered worktree root (realpath equal to `git rev-parse --show-toplevel`). */
|
|
21
|
+
cwd: string;
|
|
22
|
+
plugin: typeof PLUGIN_TOKEN;
|
|
23
|
+
trim_mcp: boolean;
|
|
24
|
+
model?: string;
|
|
25
|
+
effort?: (typeof EFFORT_VALUES)[number];
|
|
26
|
+
}
|
|
27
|
+
export declare const LAUNCH_SPEC_KEYS: readonly ["v", "cwd", "plugin", "trim_mcp", "model", "effort"];
|
|
28
|
+
/** Validate against the closed schema (additionalProperties false). Returns the spec or null. */
|
|
29
|
+
export declare function parseLaunchSpec(raw: unknown): LaunchSpec | null;
|
|
30
|
+
/** Whether a raw spec (or lock body) carries a remote-control field or flag: checked BEFORE schema validation (422 remote_control). */
|
|
31
|
+
export declare function hasRemoteControl(raw: unknown): boolean;
|
|
32
|
+
/** The canonical spec hash: sha256 hex of canonicalJson(spec) over the full spec (F09 re-lock compare). */
|
|
33
|
+
export declare function specHash(spec: LaunchSpec): Promise<string>;
|
|
34
|
+
/** User flags a seat may carry (from the spec), each with its value arity. */
|
|
35
|
+
export declare const SEAT_USER_FLAGS: {
|
|
36
|
+
readonly "--model": 1;
|
|
37
|
+
readonly "--effort": 1;
|
|
38
|
+
readonly "--trim-mcp": 0;
|
|
39
|
+
};
|
|
40
|
+
/** Flags only the daemon adds. */
|
|
41
|
+
export declare const SEAT_DAEMON_FLAGS: readonly ["--plugin-dir", "--mcp-config", "--strict-mcp-config", "--settings", "--setting-sources", "--resume"];
|
|
42
|
+
/** Canonical multiset form of an argv (after argv[0]): sorted "flag\u0000value" pairs, `--f=v` split. */
|
|
43
|
+
export declare function canonicalArgvPairs(argv: readonly string[]): string[];
|
|
44
|
+
/** The one entry every dropped positional of a passthrough adds together; positional text is never echoed. */
|
|
45
|
+
export declare const DROPPED_POSITIONALS_ENTRY: (n: number) => string;
|
|
46
|
+
/**
|
|
47
|
+
* The 'flags dropped:' list for a claude passthrough (ruling 15; C14-R02, R03, R12), classified left
|
|
48
|
+
* to right the way claude would. Before claude's own `--`, a '-'-leading token other than '-' is a
|
|
49
|
+
* flag: an allowlisted one is kept (an arity-1 flag with its value, inline or the next non-flag
|
|
50
|
+
* token; with no such value it is dropped and named and does not consume the next token), any other
|
|
51
|
+
* is dropped and named verbatim (single-dash multi-letter tokens whole, `--f=v` as `--f`). Every
|
|
52
|
+
* other token, and every token after claude's `--`, is a positional. Names come first-seen,
|
|
53
|
+
* deduplicated, then one DROPPED_POSITIONALS_ENTRY when any positional was dropped. The `--`
|
|
54
|
+
* separator itself is never named. renderAdoptNote and renderNonTtyLocked take the result unchanged.
|
|
55
|
+
*/
|
|
56
|
+
export declare function droppedArgvNames(passthrough: readonly string[], allow?: Readonly<Record<string, number>>): string[];
|
|
57
|
+
/** The C4 builder input table (final F01): which argv the one builder reads, per path. */
|
|
58
|
+
export declare const BUILDER_INPUT_TABLE: {
|
|
59
|
+
readonly running: {
|
|
60
|
+
readonly when: "a live tracked claude for the conversation on this machine (C02-R26 step-2 discovery)";
|
|
61
|
+
readonly argv: "the original's argv, read exactly (KERN_PROCARGS2 or the shim log, never ps)";
|
|
62
|
+
readonly paths: readonly ["sessions adopt", "lock verb", "/mnemom lock"];
|
|
63
|
+
readonly shape: "as launched (a dropped --remote-control is named)";
|
|
64
|
+
readonly steps: readonly [1, 2, 3, 4, 5, 6, 7];
|
|
65
|
+
};
|
|
66
|
+
readonly idle: {
|
|
67
|
+
readonly when: "no live claude for the conversation";
|
|
68
|
+
readonly argv: "the launcher's resolved continue argv at lock time (launcher config, agent defaults, --resume <cc_session_id>), with the registered cwd";
|
|
69
|
+
readonly paths: readonly ["lock verb", "API lock-route validation", "durable=always continue (C02-R23(e))"];
|
|
70
|
+
readonly shape: "terminal (resolveRemoteControlShape never called)";
|
|
71
|
+
readonly steps: readonly [1, 3, 4, 6];
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
export type BuilderSource = keyof typeof BUILDER_INPUT_TABLE;
|
|
75
|
+
/** Which source a path uses given whether a live tracked claude exists. Adopt requires one. */
|
|
76
|
+
export declare function builderSourceFor(path: "adopt" | "lock_verb" | "slash_lock" | "api_lock" | "always_continue", liveClaude: boolean): BuilderSource | null;
|
|
77
|
+
/** Plugin integrity (S1 dir-hash recipe, version 1): sha256 of canonicalJson of the sorted [relpath, sha256] list. */
|
|
78
|
+
export declare function pluginDirHash(files: readonly {
|
|
79
|
+
path: string;
|
|
80
|
+
sha256: string;
|
|
81
|
+
}[]): Promise<string>;
|
|
82
|
+
/** The plugin dir inside the published @mnemom/mnemom package. */
|
|
83
|
+
export declare const PLUGIN_DIR = "plugins/mnemom";
|
|
84
|
+
/** The pack-time manifest, beside PLUGIN_DIR (never inside it, so it never hashes itself). */
|
|
85
|
+
export declare const PLUGIN_MANIFEST_PATH = "plugins/integrity-manifest.json";
|
|
86
|
+
/**
|
|
87
|
+
* The manifest's closed shape (JSON Schema). Two rules a JSON Schema cannot say, enforced by
|
|
88
|
+
* pluginManifestOk: files is sorted by path in UTF-16 code-unit order (pluginDirHash's comparison)
|
|
89
|
+
* with no repeated path, and dir_hash = pluginDirHash(files) (checked by verifyPluginManifest).
|
|
90
|
+
*/
|
|
91
|
+
export declare const PLUGIN_MANIFEST_SCHEMA: Readonly<{
|
|
92
|
+
readonly type: "object";
|
|
93
|
+
readonly additionalProperties: false;
|
|
94
|
+
readonly required: readonly ["recipe_version", "plugin_dir", "files", "dir_hash"];
|
|
95
|
+
readonly properties: {
|
|
96
|
+
readonly recipe_version: {
|
|
97
|
+
readonly const: 1;
|
|
98
|
+
};
|
|
99
|
+
readonly plugin_dir: {
|
|
100
|
+
readonly const: "plugins/mnemom";
|
|
101
|
+
};
|
|
102
|
+
readonly files: {
|
|
103
|
+
readonly type: "array";
|
|
104
|
+
readonly items: {
|
|
105
|
+
readonly type: "object";
|
|
106
|
+
readonly additionalProperties: false;
|
|
107
|
+
readonly required: readonly ["path", "sha256"];
|
|
108
|
+
readonly properties: {
|
|
109
|
+
readonly path: {
|
|
110
|
+
readonly type: "string";
|
|
111
|
+
readonly pattern: "^(?!.*(?:^|/)\\.{1,2}(?:/|$))[^/\\\\\\u0000]+(?:/[^/\\\\\\u0000]+)*$";
|
|
112
|
+
};
|
|
113
|
+
readonly sha256: {
|
|
114
|
+
readonly type: "string";
|
|
115
|
+
readonly pattern: "^[0-9a-f]{64}$";
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
readonly dir_hash: {
|
|
121
|
+
readonly type: "string";
|
|
122
|
+
readonly pattern: "^[0-9a-f]{64}$";
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
}>;
|
|
126
|
+
export interface PluginManifest {
|
|
127
|
+
readonly recipe_version: typeof PLUGIN_HASH_RECIPE_VERSION;
|
|
128
|
+
readonly plugin_dir: typeof PLUGIN_DIR;
|
|
129
|
+
readonly files: readonly {
|
|
130
|
+
readonly path: string;
|
|
131
|
+
readonly sha256: string;
|
|
132
|
+
}[];
|
|
133
|
+
readonly dir_hash: string;
|
|
134
|
+
}
|
|
135
|
+
/** One entry of the installed plugin dir as the verifier walks it (directories are not entries). */
|
|
136
|
+
export interface PluginDirEntry {
|
|
137
|
+
/** POSIX, relative to PLUGIN_DIR. */
|
|
138
|
+
readonly path: string;
|
|
139
|
+
/** Lowercase hex sha256 of the bytes; null for a non-regular entry. */
|
|
140
|
+
readonly sha256: string | null;
|
|
141
|
+
/** false for a symlink or any other non-regular file. */
|
|
142
|
+
readonly regular: boolean;
|
|
143
|
+
}
|
|
144
|
+
/** Failure causes, in the order verifyPluginManifest checks them (first match wins). */
|
|
145
|
+
export declare const PLUGIN_MANIFEST_FAILURES: readonly ["missing", "unparseable", "recipe", "dir_hash", "not_regular", "added", "removed", "edited"];
|
|
146
|
+
export type PluginManifestFailure = (typeof PLUGIN_MANIFEST_FAILURES)[number];
|
|
147
|
+
/** True for exactly PLUGIN_MANIFEST_SCHEMA plus sorted, unique paths (dir_hash is not recomputed). */
|
|
148
|
+
export declare function pluginManifestOk(v: unknown): v is PluginManifest;
|
|
149
|
+
/**
|
|
150
|
+
* Verify the installed plugin dir against its shipped manifest (the daemon's pre-claim check; the
|
|
151
|
+
* C12/C14 harness and oracle use the same function). `manifestText` is PLUGIN_MANIFEST_PATH's
|
|
152
|
+
* content (null when the file is absent); `actual` lists every non-directory entry under
|
|
153
|
+
* PLUGIN_DIR. One cause per failure, first match in PLUGIN_MANIFEST_FAILURES order (ruling
|
|
154
|
+
* 25.1). A numeric recipe_version other than the frozen one is `recipe`, before the shape check, so
|
|
155
|
+
* a newer recipe is named as such; a missing or wrong-typed one is `unparseable`.
|
|
156
|
+
*/
|
|
157
|
+
export declare function verifyPluginManifest(manifestText: string | null, actual: readonly PluginDirEntry[]): Promise<{
|
|
158
|
+
ok: true;
|
|
159
|
+
} | {
|
|
160
|
+
ok: false;
|
|
161
|
+
cause: PluginManifestFailure;
|
|
162
|
+
}>;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// GENERATED from shared/durable-contract/src/spec.ts by cli/scripts/sync-durable-contract.mjs; do not edit.
|
|
2
|
+
/**
|
|
3
|
+
* The closed launch-spec schema, the canonical spec hash, the seat argv allowlist and the C4
|
|
4
|
+
* builder input table (C14-R01..R05, R13; C02-R26; BACKBONE C4, amendments 31, 43; final F01, F09).
|
|
5
|
+
*
|
|
6
|
+
* Frozen choices (C14-R01 asks S1 before the oracle):
|
|
7
|
+
* - argv level compared: the claude-level execve argv;
|
|
8
|
+
* - token order: canonical multiset of (flag, value) pairs (canonicalArgvPairs); exact bytes only
|
|
9
|
+
* for verified spec vs actual execve;
|
|
10
|
+
* - `--name` is NOT allowlisted (dropped and named);
|
|
11
|
+
* - the yolo-default bypass flag counts as dropped and is named (C14-R12).
|
|
12
|
+
*/
|
|
13
|
+
import { canonicalJson, HEX64_RE, sha256Hex } from "./canonical.js";
|
|
14
|
+
import { PLUGIN_HASH_RECIPE_VERSION } from "./constants.js";
|
|
15
|
+
export const SPEC_VERSION = 1;
|
|
16
|
+
export const PLUGIN_TOKEN = "bundled-mnemom";
|
|
17
|
+
export const EFFORT_VALUES = ["low", "medium", "high", "xhigh", "max"];
|
|
18
|
+
export const MODEL_RE = /^[A-Za-z0-9._[\]-]{1,64}$/;
|
|
19
|
+
export const LAUNCH_SPEC_KEYS = ["v", "cwd", "plugin", "trim_mcp", "model", "effort"];
|
|
20
|
+
const REQUIRED_SPEC_KEYS = ["v", "cwd", "plugin", "trim_mcp"];
|
|
21
|
+
/** Validate against the closed schema (additionalProperties false). Returns the spec or null. */
|
|
22
|
+
export function parseLaunchSpec(raw) {
|
|
23
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
24
|
+
return null;
|
|
25
|
+
const r = raw;
|
|
26
|
+
if (Object.keys(r).some((k) => !LAUNCH_SPEC_KEYS.includes(k)))
|
|
27
|
+
return null;
|
|
28
|
+
if (!REQUIRED_SPEC_KEYS.every((k) => k in r))
|
|
29
|
+
return null;
|
|
30
|
+
if (r.v !== SPEC_VERSION || r.plugin !== PLUGIN_TOKEN || typeof r.trim_mcp !== "boolean")
|
|
31
|
+
return null;
|
|
32
|
+
if (typeof r.cwd !== "string" ||
|
|
33
|
+
!r.cwd.startsWith("/") ||
|
|
34
|
+
r.cwd.includes("\n") ||
|
|
35
|
+
r.cwd.length > 4096)
|
|
36
|
+
return null;
|
|
37
|
+
if ("model" in r && (typeof r.model !== "string" || !MODEL_RE.test(r.model)))
|
|
38
|
+
return null;
|
|
39
|
+
if ("effort" in r && !EFFORT_VALUES.includes(r.effort))
|
|
40
|
+
return null;
|
|
41
|
+
return r;
|
|
42
|
+
}
|
|
43
|
+
/** Whether a raw spec (or lock body) carries a remote-control field or flag: checked BEFORE schema validation (422 remote_control). */
|
|
44
|
+
export function hasRemoteControl(raw) {
|
|
45
|
+
if (raw === null || typeof raw !== "object")
|
|
46
|
+
return false;
|
|
47
|
+
const text = JSON.stringify(raw).toLowerCase();
|
|
48
|
+
return (text.includes("remote_control") ||
|
|
49
|
+
text.includes("remote-control") ||
|
|
50
|
+
text.includes("remotecontrol"));
|
|
51
|
+
}
|
|
52
|
+
/** The canonical spec hash: sha256 hex of canonicalJson(spec) over the full spec (F09 re-lock compare). */
|
|
53
|
+
export async function specHash(spec) {
|
|
54
|
+
return sha256Hex(canonicalJson(spec));
|
|
55
|
+
}
|
|
56
|
+
/** User flags a seat may carry (from the spec), each with its value arity. */
|
|
57
|
+
export const SEAT_USER_FLAGS = { "--model": 1, "--effort": 1, "--trim-mcp": 0 };
|
|
58
|
+
/** Flags only the daemon adds. */
|
|
59
|
+
export const SEAT_DAEMON_FLAGS = [
|
|
60
|
+
"--plugin-dir",
|
|
61
|
+
"--mcp-config",
|
|
62
|
+
"--strict-mcp-config",
|
|
63
|
+
"--settings",
|
|
64
|
+
"--setting-sources",
|
|
65
|
+
"--resume",
|
|
66
|
+
];
|
|
67
|
+
/** One argv token as both parsers read it: a '-'-leading token is a flag; `--f=v` carries its value inline. */
|
|
68
|
+
function readArgvToken(tok) {
|
|
69
|
+
if (!tok.startsWith("-"))
|
|
70
|
+
return { flag: null };
|
|
71
|
+
const eq = tok.startsWith("--") ? tok.indexOf("=") : -1;
|
|
72
|
+
return eq > 0 ? { flag: tok.slice(0, eq), inline: tok.slice(eq + 1) } : { flag: tok };
|
|
73
|
+
}
|
|
74
|
+
/** Canonical multiset form of an argv (after argv[0]): sorted "flag\u0000value" pairs, `--f=v` split. */
|
|
75
|
+
export function canonicalArgvPairs(argv) {
|
|
76
|
+
const out = [];
|
|
77
|
+
for (let i = 0; i < argv.length; i++) {
|
|
78
|
+
const tok = argv[i];
|
|
79
|
+
const { flag, inline } = readArgvToken(tok);
|
|
80
|
+
if (flag !== null && inline !== undefined) {
|
|
81
|
+
out.push(`${flag}\u0000${inline}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const next = argv[i + 1];
|
|
85
|
+
if (flag !== null && next !== undefined && readArgvToken(next).flag === null) {
|
|
86
|
+
out.push(`${tok}\u0000${next}`);
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
out.push(`${tok}\u0000`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out.sort();
|
|
94
|
+
}
|
|
95
|
+
/** The one entry every dropped positional of a passthrough adds together; positional text is never echoed. */
|
|
96
|
+
export const DROPPED_POSITIONALS_ENTRY = (n) => `positional arguments (${n})`;
|
|
97
|
+
/**
|
|
98
|
+
* The 'flags dropped:' list for a claude passthrough (ruling 15; C14-R02, R03, R12), classified left
|
|
99
|
+
* to right the way claude would. Before claude's own `--`, a '-'-leading token other than '-' is a
|
|
100
|
+
* flag: an allowlisted one is kept (an arity-1 flag with its value, inline or the next non-flag
|
|
101
|
+
* token; with no such value it is dropped and named and does not consume the next token), any other
|
|
102
|
+
* is dropped and named verbatim (single-dash multi-letter tokens whole, `--f=v` as `--f`). Every
|
|
103
|
+
* other token, and every token after claude's `--`, is a positional. Names come first-seen,
|
|
104
|
+
* deduplicated, then one DROPPED_POSITIONALS_ENTRY when any positional was dropped. The `--`
|
|
105
|
+
* separator itself is never named. renderAdoptNote and renderNonTtyLocked take the result unchanged.
|
|
106
|
+
*/
|
|
107
|
+
export function droppedArgvNames(passthrough, allow = SEAT_USER_FLAGS) {
|
|
108
|
+
const names = [];
|
|
109
|
+
let positionals = 0;
|
|
110
|
+
const name = (f) => {
|
|
111
|
+
if (!names.includes(f))
|
|
112
|
+
names.push(f);
|
|
113
|
+
};
|
|
114
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
115
|
+
const tok = passthrough[i];
|
|
116
|
+
if (tok === "--") {
|
|
117
|
+
positionals += passthrough.length - i - 1;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
const { flag, inline } = tok === "-" ? { flag: null } : readArgvToken(tok);
|
|
121
|
+
if (flag === null) {
|
|
122
|
+
positionals++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const arity = Object.prototype.hasOwnProperty.call(allow, flag) ? allow[flag] : undefined;
|
|
126
|
+
if (arity === undefined) {
|
|
127
|
+
name(flag);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (arity === 0 || inline !== undefined)
|
|
131
|
+
continue;
|
|
132
|
+
const next = passthrough[i + 1];
|
|
133
|
+
if (next !== undefined && readArgvToken(next).flag === null)
|
|
134
|
+
i++;
|
|
135
|
+
else
|
|
136
|
+
name(flag);
|
|
137
|
+
}
|
|
138
|
+
if (positionals > 0)
|
|
139
|
+
names.push(DROPPED_POSITIONALS_ENTRY(positionals));
|
|
140
|
+
return names;
|
|
141
|
+
}
|
|
142
|
+
/** The C4 builder input table (final F01): which argv the one builder reads, per path. */
|
|
143
|
+
export const BUILDER_INPUT_TABLE = {
|
|
144
|
+
running: {
|
|
145
|
+
when: "a live tracked claude for the conversation on this machine (C02-R26 step-2 discovery)",
|
|
146
|
+
argv: "the original's argv, read exactly (KERN_PROCARGS2 or the shim log, never ps)",
|
|
147
|
+
paths: ["sessions adopt", "lock verb", "/mnemom lock"],
|
|
148
|
+
shape: "as launched (a dropped --remote-control is named)",
|
|
149
|
+
steps: [1, 2, 3, 4, 5, 6, 7],
|
|
150
|
+
},
|
|
151
|
+
idle: {
|
|
152
|
+
when: "no live claude for the conversation",
|
|
153
|
+
argv: "the launcher's resolved continue argv at lock time (launcher config, agent defaults, --resume <cc_session_id>), with the registered cwd",
|
|
154
|
+
paths: ["lock verb", "API lock-route validation", "durable=always continue (C02-R23(e))"],
|
|
155
|
+
shape: "terminal (resolveRemoteControlShape never called)",
|
|
156
|
+
steps: [1, 3, 4, 6],
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
/** Which source a path uses given whether a live tracked claude exists. Adopt requires one. */
|
|
160
|
+
export function builderSourceFor(path, liveClaude) {
|
|
161
|
+
if (path === "api_lock" || path === "always_continue")
|
|
162
|
+
return "idle";
|
|
163
|
+
if (path === "adopt" || path === "slash_lock")
|
|
164
|
+
return liveClaude ? "running" : null;
|
|
165
|
+
return liveClaude ? "running" : "idle";
|
|
166
|
+
}
|
|
167
|
+
/** Plugin integrity (S1 dir-hash recipe, version 1): sha256 of canonicalJson of the sorted [relpath, sha256] list. */
|
|
168
|
+
export async function pluginDirHash(files) {
|
|
169
|
+
const rows = files
|
|
170
|
+
.map((f) => [f.path, f.sha256])
|
|
171
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
172
|
+
return sha256Hex(canonicalJson(rows));
|
|
173
|
+
}
|
|
174
|
+
// ---- plugin integrity manifest (wave-0 fix ruling 25; RULINGS-evals U13; C12-R36, C14-R26..R28) --
|
|
175
|
+
/** The plugin dir inside the published @mnemom/mnemom package. */
|
|
176
|
+
export const PLUGIN_DIR = "plugins/mnemom";
|
|
177
|
+
/** The pack-time manifest, beside PLUGIN_DIR (never inside it, so it never hashes itself). */
|
|
178
|
+
export const PLUGIN_MANIFEST_PATH = "plugins/integrity-manifest.json";
|
|
179
|
+
/** A manifest path: POSIX, relative to PLUGIN_DIR, no empty, "." or ".." segment, no backslash. */
|
|
180
|
+
const MANIFEST_FILE_PATH_PATTERN = "^(?!.*(?:^|/)\\.{1,2}(?:/|$))[^/\\\\\\u0000]+(?:/[^/\\\\\\u0000]+)*$";
|
|
181
|
+
const MANIFEST_FILE_PATH_RE = new RegExp(MANIFEST_FILE_PATH_PATTERN);
|
|
182
|
+
/**
|
|
183
|
+
* The manifest's closed shape (JSON Schema). Two rules a JSON Schema cannot say, enforced by
|
|
184
|
+
* pluginManifestOk: files is sorted by path in UTF-16 code-unit order (pluginDirHash's comparison)
|
|
185
|
+
* with no repeated path, and dir_hash = pluginDirHash(files) (checked by verifyPluginManifest).
|
|
186
|
+
*/
|
|
187
|
+
export const PLUGIN_MANIFEST_SCHEMA = Object.freeze({
|
|
188
|
+
type: "object",
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
required: ["recipe_version", "plugin_dir", "files", "dir_hash"],
|
|
191
|
+
properties: {
|
|
192
|
+
recipe_version: { const: PLUGIN_HASH_RECIPE_VERSION },
|
|
193
|
+
plugin_dir: { const: PLUGIN_DIR },
|
|
194
|
+
files: {
|
|
195
|
+
type: "array",
|
|
196
|
+
items: {
|
|
197
|
+
type: "object",
|
|
198
|
+
additionalProperties: false,
|
|
199
|
+
required: ["path", "sha256"],
|
|
200
|
+
properties: {
|
|
201
|
+
path: { type: "string", pattern: MANIFEST_FILE_PATH_PATTERN },
|
|
202
|
+
sha256: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
dir_hash: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
/** Failure causes, in the order verifyPluginManifest checks them (first match wins). */
|
|
210
|
+
export const PLUGIN_MANIFEST_FAILURES = [
|
|
211
|
+
"missing",
|
|
212
|
+
"unparseable",
|
|
213
|
+
"recipe",
|
|
214
|
+
"dir_hash",
|
|
215
|
+
"not_regular",
|
|
216
|
+
"added",
|
|
217
|
+
"removed",
|
|
218
|
+
"edited",
|
|
219
|
+
];
|
|
220
|
+
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
221
|
+
/** True for exactly PLUGIN_MANIFEST_SCHEMA plus sorted, unique paths (dir_hash is not recomputed). */
|
|
222
|
+
export function pluginManifestOk(v) {
|
|
223
|
+
if (!isPlainObject(v))
|
|
224
|
+
return false;
|
|
225
|
+
const keys = PLUGIN_MANIFEST_SCHEMA.required;
|
|
226
|
+
if (Object.keys(v).length !== keys.length || keys.some((k) => !(k in v)))
|
|
227
|
+
return false;
|
|
228
|
+
if (v.recipe_version !== PLUGIN_HASH_RECIPE_VERSION || v.plugin_dir !== PLUGIN_DIR)
|
|
229
|
+
return false;
|
|
230
|
+
if (typeof v.dir_hash !== "string" || !HEX64_RE.test(v.dir_hash))
|
|
231
|
+
return false;
|
|
232
|
+
if (!Array.isArray(v.files))
|
|
233
|
+
return false;
|
|
234
|
+
let prev = null;
|
|
235
|
+
for (const f of v.files) {
|
|
236
|
+
if (!isPlainObject(f) || Object.keys(f).length !== 2)
|
|
237
|
+
return false;
|
|
238
|
+
if (typeof f.path !== "string" || !MANIFEST_FILE_PATH_RE.test(f.path))
|
|
239
|
+
return false;
|
|
240
|
+
if (typeof f.sha256 !== "string" || !HEX64_RE.test(f.sha256))
|
|
241
|
+
return false;
|
|
242
|
+
if (prev !== null && !(prev < f.path))
|
|
243
|
+
return false;
|
|
244
|
+
prev = f.path;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Verify the installed plugin dir against its shipped manifest (the daemon's pre-claim check; the
|
|
250
|
+
* C12/C14 harness and oracle use the same function). `manifestText` is PLUGIN_MANIFEST_PATH's
|
|
251
|
+
* content (null when the file is absent); `actual` lists every non-directory entry under
|
|
252
|
+
* PLUGIN_DIR. One cause per failure, first match in PLUGIN_MANIFEST_FAILURES order (ruling
|
|
253
|
+
* 25.1). A numeric recipe_version other than the frozen one is `recipe`, before the shape check, so
|
|
254
|
+
* a newer recipe is named as such; a missing or wrong-typed one is `unparseable`.
|
|
255
|
+
*/
|
|
256
|
+
export async function verifyPluginManifest(manifestText, actual) {
|
|
257
|
+
const fail = (cause) => ({ ok: false, cause });
|
|
258
|
+
if (manifestText === null)
|
|
259
|
+
return fail("missing");
|
|
260
|
+
let parsed;
|
|
261
|
+
try {
|
|
262
|
+
parsed = JSON.parse(manifestText);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return fail("unparseable");
|
|
266
|
+
}
|
|
267
|
+
if (isPlainObject(parsed) &&
|
|
268
|
+
typeof parsed.recipe_version === "number" &&
|
|
269
|
+
parsed.recipe_version !== PLUGIN_HASH_RECIPE_VERSION)
|
|
270
|
+
return fail("recipe");
|
|
271
|
+
if (!pluginManifestOk(parsed))
|
|
272
|
+
return fail("unparseable");
|
|
273
|
+
if ((await pluginDirHash(parsed.files)) !== parsed.dir_hash)
|
|
274
|
+
return fail("dir_hash");
|
|
275
|
+
if (actual.some((e) => !e.regular))
|
|
276
|
+
return fail("not_regular");
|
|
277
|
+
const listed = new Map(parsed.files.map((f) => [f.path, f.sha256]));
|
|
278
|
+
const present = new Map(actual.map((e) => [e.path, e.sha256]));
|
|
279
|
+
if ([...present.keys()].some((p) => !listed.has(p)))
|
|
280
|
+
return fail("added");
|
|
281
|
+
if ([...listed.keys()].some((p) => !present.has(p)))
|
|
282
|
+
return fail("removed");
|
|
283
|
+
if ([...listed].some(([p, h]) => present.get(p) !== h))
|
|
284
|
+
return fail("edited");
|
|
285
|
+
return { ok: true };
|
|
286
|
+
}
|