@agfpd/iapeer-memory 0.1.13 → 0.2.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/package.json +2 -2
- package/src/binary.ts +17 -10
- package/src/cli.ts +38 -8
- package/src/commands/hook.ts +7 -8
- package/src/commands/init.ts +136 -66
- package/src/commands/install-binary.ts +3 -2
- package/src/commands/memoryd.ts +3 -2
- package/src/commands/provision-peer.ts +172 -0
- package/src/commands/status.ts +15 -8
- package/src/commands/uninstall.ts +78 -48
- package/src/commands/update.ts +125 -53
- package/src/commands/verify.ts +125 -13
- package/src/egress.ts +181 -0
- package/src/fleet.ts +94 -31
- package/src/paths.ts +5 -0
- package/src/provision.ts +3 -2
- package/src/roles.ts +2 -1
- package/src/signing.ts +19 -23
- package/src/slot.ts +97 -52
- package/src/surfaces/claude.ts +497 -0
- package/src/surfaces/codex.ts +156 -0
- package/src/surfaces/lock.ts +72 -0
- package/src/surfaces/sweep.ts +170 -0
- package/src/sync-versions.ts +3 -2
- package/src/templates/index.ts +2 -1
- package/src/templates/skills.ts +196 -0
- package/src/watcher.ts +72 -68
package/src/fleet.ts
CHANGED
|
@@ -7,13 +7,21 @@
|
|
|
7
7
|
* new peer wakes → SessionStart kick → `verify --repair` re-writes the
|
|
8
8
|
* map → memoryd renders the newcomer's fragment within a tick.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* READ-AS-EGRESS (incident 11.06, the FOURTH of its class — first FILE-path
|
|
11
|
+
* one): `iapeer list` is read-only, but its RESULT is the target list of the
|
|
12
|
+
* surfaces sweep — a sandboxed `verify --repair` with no fleet map repaired
|
|
13
|
+
* the map from the LIVE registry and then swept the LIVE peers' cwds with
|
|
14
|
+
* direct surfaces (the send-fuse never saw it: no IAP send involved).
|
|
15
|
+
* Querying the live registry from a test IS the leak — the query now goes
|
|
16
|
+
* through the egress handle (deny-by-default §4 П5): a refusing handle
|
|
17
|
+
* blocks the default binary; tests pass a fake `iapeerBin` (explicit-bin
|
|
18
|
+
* allowance) or write the map file directly.
|
|
13
19
|
*/
|
|
14
20
|
|
|
15
21
|
import fs from "node:fs";
|
|
16
22
|
import path from "node:path";
|
|
23
|
+
import { IAPEER_BIN, type Egress } from "./egress.js";
|
|
24
|
+
import { guardedWriteFileSync } from "@agfpd/iapeer-memory-core";
|
|
17
25
|
|
|
18
26
|
export type FleetMapResult = {
|
|
19
27
|
action: "written" | "failed";
|
|
@@ -21,33 +29,78 @@ export type FleetMapResult = {
|
|
|
21
29
|
detail: string;
|
|
22
30
|
};
|
|
23
31
|
|
|
24
|
-
type ListedPeer = {
|
|
32
|
+
type ListedPeer = {
|
|
33
|
+
personality?: unknown;
|
|
34
|
+
cwd?: unknown;
|
|
35
|
+
/** iapeer registry: `[{runtime: "claude"|"codex"|…, status}]`. */
|
|
36
|
+
runtimes?: Array<{ runtime?: unknown }>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Fleet-map entry. `runtimes` (ADR-009 v1.2) names the peer's session
|
|
40
|
+
* runtimes from the registry — the surfaces sweep keys its per-runtime
|
|
41
|
+
* forms on it (claude: hooks+mcp+skills; codex: project-local MCP).
|
|
42
|
+
* Core's memoryd reader takes personality/cwd only — additive, fail-open. */
|
|
43
|
+
export type FleetPeer = { personality: string; cwd: string; runtimes: string[] };
|
|
25
44
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const bin = opts.iapeerBin ?? "iapeer";
|
|
33
|
-
let stdout: string;
|
|
45
|
+
/** Fail-open fleet-map reader (the package side: the surfaces sweep and
|
|
46
|
+
* verify's per-peer checks). Missing/unreadable map → null — callers report
|
|
47
|
+
* honestly instead of guessing the fleet. Entries without a runtimes array
|
|
48
|
+
* (pre-v1.2 maps) read as `runtimes: []` — the sweep skips them until the
|
|
49
|
+
* next map re-write (init/update/verify --repair). */
|
|
50
|
+
export function readFleetMap(fleetMapPath: string): FleetPeer[] | null {
|
|
34
51
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const raw = JSON.parse(fs.readFileSync(fleetMapPath, "utf-8")) as {
|
|
53
|
+
peers?: Array<{ personality?: unknown; cwd?: unknown; runtimes?: unknown }>;
|
|
54
|
+
};
|
|
55
|
+
if (!Array.isArray(raw?.peers)) return null;
|
|
56
|
+
return raw.peers
|
|
57
|
+
.filter(
|
|
58
|
+
(p): p is { personality: string; cwd: string; runtimes?: unknown } =>
|
|
59
|
+
typeof p?.personality === "string" && typeof p?.cwd === "string",
|
|
60
|
+
)
|
|
61
|
+
.map((p) => ({
|
|
62
|
+
personality: p.personality,
|
|
63
|
+
cwd: p.cwd,
|
|
64
|
+
runtimes: Array.isArray(p.runtimes)
|
|
65
|
+
? p.runtimes.filter((r): r is string => typeof r === "string")
|
|
66
|
+
: [],
|
|
67
|
+
}));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function writeFleetMap(
|
|
74
|
+
egress: Egress,
|
|
75
|
+
opts: {
|
|
76
|
+
fleetMapPath: string;
|
|
77
|
+
iapeerBin?: string;
|
|
78
|
+
/** Injectable for tests. */
|
|
79
|
+
nowIso?: string;
|
|
80
|
+
},
|
|
81
|
+
): FleetMapResult {
|
|
82
|
+
const bin = opts.iapeerBin ?? IAPEER_BIN;
|
|
83
|
+
const proc = egress.spawnSync([bin, "list", "--json"], {
|
|
84
|
+
explicitBin: opts.iapeerBin !== undefined,
|
|
85
|
+
});
|
|
86
|
+
if (proc.refused) {
|
|
87
|
+
return {
|
|
88
|
+
action: "failed",
|
|
89
|
+
count: 0,
|
|
90
|
+
detail: "live-registry query suppressed (test sandbox) — pass a fake iapeerBin",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (proc.spawnError) {
|
|
94
|
+
return { action: "failed", count: 0, detail: `${bin} unavailable: ${proc.spawnError}` };
|
|
95
|
+
}
|
|
96
|
+
if (proc.exitCode !== 0) {
|
|
97
|
+
return {
|
|
98
|
+
action: "failed",
|
|
99
|
+
count: 0,
|
|
100
|
+
detail: (proc.stderr.trim() || `iapeer list exited ${proc.exitCode}`).slice(0, 160),
|
|
101
|
+
};
|
|
50
102
|
}
|
|
103
|
+
const stdout = proc.stdout;
|
|
51
104
|
|
|
52
105
|
let listed: ListedPeer[];
|
|
53
106
|
try {
|
|
@@ -57,15 +110,25 @@ export function writeFleetMap(opts: {
|
|
|
57
110
|
return { action: "failed", count: 0, detail: "iapeer list --json: unparsable output" };
|
|
58
111
|
}
|
|
59
112
|
|
|
60
|
-
const peers = listed
|
|
113
|
+
const peers: FleetPeer[] = listed
|
|
61
114
|
.filter(
|
|
62
|
-
(p): p is { personality: string; cwd: string } =>
|
|
115
|
+
(p): p is ListedPeer & { personality: string; cwd: string } =>
|
|
63
116
|
typeof p.personality === "string" &&
|
|
64
117
|
p.personality.trim() !== "" &&
|
|
65
118
|
typeof p.cwd === "string" &&
|
|
66
119
|
p.cwd.trim() !== "",
|
|
67
120
|
)
|
|
68
|
-
.map((p) => ({
|
|
121
|
+
.map((p) => ({
|
|
122
|
+
personality: p.personality.trim(),
|
|
123
|
+
cwd: p.cwd.trim(),
|
|
124
|
+
runtimes: [
|
|
125
|
+
...new Set(
|
|
126
|
+
(Array.isArray(p.runtimes) ? p.runtimes : [])
|
|
127
|
+
.map((r) => (typeof r?.runtime === "string" ? r.runtime.trim() : ""))
|
|
128
|
+
.filter(Boolean),
|
|
129
|
+
),
|
|
130
|
+
],
|
|
131
|
+
}));
|
|
69
132
|
|
|
70
133
|
const body =
|
|
71
134
|
JSON.stringify(
|
|
@@ -75,7 +138,7 @@ export function writeFleetMap(opts: {
|
|
|
75
138
|
) + "\n";
|
|
76
139
|
fs.mkdirSync(path.dirname(opts.fleetMapPath), { recursive: true });
|
|
77
140
|
const tmp = `${opts.fleetMapPath}.tmp`;
|
|
78
|
-
|
|
141
|
+
guardedWriteFileSync(tmp, body, "utf-8");
|
|
79
142
|
fs.renameSync(tmp, opts.fleetMapPath); // atomic — memoryd may race a read
|
|
80
143
|
return {
|
|
81
144
|
action: "written",
|
package/src/paths.ts
CHANGED
|
@@ -48,6 +48,10 @@ export type MemoryPaths = {
|
|
|
48
48
|
binaryPath: string;
|
|
49
49
|
/** Materialised package-owned templates (roles, guide) — see templates/index.ts. */
|
|
50
50
|
templatesDir: string;
|
|
51
|
+
/** Materialised hook shims (fail-open bash, 3 lines) — the ABSOLUTE command
|
|
52
|
+
* paths merged into peers' `.claude/settings.json` (ownership lives IN THE
|
|
53
|
+
* DATA: the command path is the identity of our entries — ADR-009 v1.2). */
|
|
54
|
+
hooksDir: string;
|
|
51
55
|
/** memoryd launcher — the notifier watcher's script (wraps the stable binary). */
|
|
52
56
|
launcherPath: string;
|
|
53
57
|
/** Sweep check-script — gates the fail-open inbox sweep (ADR-015). */
|
|
@@ -92,6 +96,7 @@ export function memoryPaths(
|
|
|
92
96
|
binaryPath:
|
|
93
97
|
env.IAPEER_MEMORY_BINARY_PATH || path.join(home, ".local", "bin", "iapeer-memory"),
|
|
94
98
|
templatesDir: path.join(path.dirname(configFile), "templates"),
|
|
99
|
+
hooksDir: path.join(path.dirname(configFile), "hooks"),
|
|
95
100
|
launcherPath: path.join(path.dirname(configFile), "memoryd-launcher.sh"),
|
|
96
101
|
checkScriptPath: path.join(path.dirname(configFile), "inbox-stale-check.sh"),
|
|
97
102
|
fleetMapPath: path.join(stateDir, "fleet.json"),
|
package/src/provision.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import type { LocaleId, TaxonomyPreset } from "@agfpd/iapeer-memory-core";
|
|
16
|
+
import { guardedWriteFileSync } from "@agfpd/iapeer-memory-core";
|
|
16
17
|
|
|
17
18
|
export type ProvisionResult = {
|
|
18
19
|
createdDirs: string[];
|
|
@@ -129,7 +130,7 @@ export function provisionVault(opts: {
|
|
|
129
130
|
continue;
|
|
130
131
|
}
|
|
131
132
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
132
|
-
|
|
133
|
+
guardedWriteFileSync(file, content, "utf-8");
|
|
133
134
|
createdFiles.push(rel);
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -183,6 +184,6 @@ export function writeDefaultConfig(
|
|
|
183
184
|
): "written" | "exists" {
|
|
184
185
|
if (fs.existsSync(opts.configFile)) return "exists";
|
|
185
186
|
fs.mkdirSync(path.dirname(opts.configFile), { recursive: true });
|
|
186
|
-
|
|
187
|
+
guardedWriteFileSync(opts.configFile, defaultConfigContent(opts), "utf-8");
|
|
187
188
|
return "written";
|
|
188
189
|
}
|
package/src/roles.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
|
+
import { guardedWriteFileSync } from "@agfpd/iapeer-memory-core";
|
|
15
16
|
|
|
16
17
|
export type RoleEntry = { role: string; peerCwd: string; template: string };
|
|
17
18
|
|
|
@@ -23,7 +24,7 @@ export function writeRolesManifest(opts: {
|
|
|
23
24
|
}): void {
|
|
24
25
|
fs.mkdirSync(path.dirname(opts.rolesManifestPath), { recursive: true });
|
|
25
26
|
const tmp = `${opts.rolesManifestPath}.tmp`;
|
|
26
|
-
|
|
27
|
+
guardedWriteFileSync(
|
|
27
28
|
tmp,
|
|
28
29
|
JSON.stringify({ roles: opts.roles } satisfies RolesManifest, null, 2) + "\n",
|
|
29
30
|
"utf-8",
|
package/src/signing.ts
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
import fs from "node:fs";
|
|
39
39
|
import os from "node:os";
|
|
40
40
|
import path from "node:path";
|
|
41
|
+
import type { Egress } from "./egress.js";
|
|
42
|
+
import { guardedRmSync } from "@agfpd/iapeer-memory-core";
|
|
41
43
|
|
|
42
44
|
export const SIGNING_IDENTITY_CN = "iapeer Local Codesign";
|
|
43
45
|
export const SIGNING_IDENTIFIER = "com.agfpd.iapeer-memory";
|
|
@@ -53,22 +55,16 @@ export type SigningRunner = (
|
|
|
53
55
|
args: string[],
|
|
54
56
|
) => { status: number | null; stdout: string; stderr: string };
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
stderr: r.stderr.toString(),
|
|
67
|
-
};
|
|
68
|
-
} catch (err) {
|
|
69
|
-
return { status: null, stdout: "", stderr: String(err) };
|
|
70
|
-
}
|
|
71
|
-
};
|
|
58
|
+
/** The keychain trio (security/openssl/codesign) goes out through the
|
|
59
|
+
* egress handle — 90 s ceiling so an unanswered keychain prompt can't
|
|
60
|
+
* wedge an unattended update. */
|
|
61
|
+
function egressRunner(egress: Egress): SigningRunner {
|
|
62
|
+
return (cmd, args) => {
|
|
63
|
+
const r = egress.spawnSync([cmd, ...args], { timeoutMs: 90_000 });
|
|
64
|
+
if (r.spawnError) return { status: null, stdout: "", stderr: r.spawnError };
|
|
65
|
+
return { status: r.exitCode, stdout: r.stdout, stderr: r.stderr };
|
|
66
|
+
};
|
|
67
|
+
}
|
|
72
68
|
|
|
73
69
|
export type SigningOutcome = {
|
|
74
70
|
state:
|
|
@@ -120,7 +116,7 @@ function createIdentity(run: SigningRunner): { ok: boolean; detail?: string } {
|
|
|
120
116
|
return { ok: true };
|
|
121
117
|
} finally {
|
|
122
118
|
try {
|
|
123
|
-
|
|
119
|
+
guardedRmSync(dir, { recursive: true, force: true });
|
|
124
120
|
} catch {
|
|
125
121
|
// best-effort cleanup of the throwaway key material
|
|
126
122
|
}
|
|
@@ -134,14 +130,14 @@ function createIdentity(run: SigningRunner): { ok: boolean; detail?: string } {
|
|
|
134
130
|
* grant) stays constant while the bytes change.
|
|
135
131
|
*/
|
|
136
132
|
export function signInstalledBinary(
|
|
133
|
+
egress: Egress,
|
|
137
134
|
binPath: string,
|
|
138
|
-
run: SigningRunner =
|
|
135
|
+
run: SigningRunner = egressRunner(egress),
|
|
139
136
|
): SigningOutcome {
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
) {
|
|
137
|
+
// The keychain is HOST-GLOBAL — same class as live sends. The sandbox
|
|
138
|
+
// decision lives in the egress constructor (deny-by-default §4); a
|
|
139
|
+
// refusing handle maps to the historical skipped-sandbox outcome.
|
|
140
|
+
if (egress.refused) {
|
|
145
141
|
return { state: "skipped-sandbox", detail: "test sandbox — not touching the real keychain" };
|
|
146
142
|
}
|
|
147
143
|
let created = false;
|
package/src/slot.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory-provider slot declaration — the iapeer memory-slot contract (FINAL
|
|
3
|
-
* iapeer docs fc68c54/e2195a7/c968219
|
|
4
|
-
* the three public surfaces (layer-5
|
|
5
|
-
* notifier watcher) are occupied:
|
|
2
|
+
* Memory-provider slot declaration — the iapeer memory-slot contract (FINAL
|
|
3
|
+
* base, iapeer docs fc68c54/e2195a7/c968219; v1.2 revision agreed 11.06).
|
|
4
|
+
* The slot file tells the core that the three public surfaces (layer-5
|
|
5
|
+
* fragments / MCP tools / daemon under a notifier watcher) are occupied:
|
|
6
6
|
*
|
|
7
7
|
* - the PROVIDER writes and removes the file (our init/uninstall), atomic
|
|
8
8
|
* temp+rename; the core only reads it (absent/unreadable = empty slot);
|
|
@@ -12,23 +12,33 @@
|
|
|
12
12
|
* marker, ADR-010); our `update` re-writes it (P4 obligation);
|
|
13
13
|
* - `heartbeat` (optional) = the absolute path whose mtime memoryd touches —
|
|
14
14
|
* the core may show staleness in `iapeer status`, never acts on it;
|
|
15
|
-
* - `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
15
|
+
* - `provision`/`unprovision` (v1.2, ADR-009 v1.2 — boris's birth-joint
|
|
16
|
+
* inversion, schema fixed with the core 11.06): the PROVIDER's OWN command
|
|
17
|
+
* the core shells into at peer birth / verb sweeps / peer removal. The
|
|
18
|
+
* core never learns the surface forms; placeholders {cwd} {runtime}
|
|
19
|
+
* {personality} {occasion} substitute PER-ARGUMENT (argv spawn, no shell,
|
|
20
|
+
* 120s timeout, best-effort + loud warn). Precedence at the core:
|
|
21
|
+
* provision > plugin with NO runtime fallback;
|
|
22
|
+
* - `plugin` (v1.1, deprecated by v1.2): we no longer WRITE it — holding
|
|
23
|
+
* both blocks would make an old core re-install the plugin we swept
|
|
24
|
+
* (agreed 11.06). An old core reads our v1.2 slot as «provider without a
|
|
25
|
+
* plugin» and honestly skips the birth install; the newborn is picked up
|
|
26
|
+
* by the verify --repair sweep. RELEASE ORDER closes even that window on
|
|
27
|
+
* this host: the core ships its v1.2 parser FIRST, our release follows.
|
|
28
|
+
* The type keeps the field so uninstall/update can MIGRATE old slots
|
|
29
|
+
* (plugin off --all while the block is still readable).
|
|
23
30
|
*/
|
|
24
31
|
|
|
25
32
|
import fs from "node:fs";
|
|
26
33
|
import path from "node:path";
|
|
34
|
+
import { IAPEER_BIN, type Egress } from "./egress.js";
|
|
35
|
+
import { guardedWriteFileSync, guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
|
|
27
36
|
|
|
28
37
|
export const SLOT_PROVIDER = "iapeer-memory";
|
|
29
38
|
export const SLOT_PACKAGE = "@agfpd/iapeer-memory";
|
|
30
39
|
|
|
31
|
-
/** Mirror of iapeer's MemoryProviderPlugin (src/status/index.ts).
|
|
40
|
+
/** Mirror of iapeer's MemoryProviderPlugin (src/status/index.ts). v1.1
|
|
41
|
+
* legacy: READ-only here (migration off-path); v1.2 slots no longer carry it. */
|
|
32
42
|
export type MemoryProviderPlugin = {
|
|
33
43
|
/** Plugin id in the marketplace (forms `<name>@<marketplace>`). */
|
|
34
44
|
name: string;
|
|
@@ -38,19 +48,54 @@ export type MemoryProviderPlugin = {
|
|
|
38
48
|
marketplaceRef: string;
|
|
39
49
|
};
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
/** v1.2 provision command block — argv form (§7 req 1: per-argument
|
|
52
|
+
* placeholder substitution, spawn without a shell). */
|
|
53
|
+
export type MemoryProviderCommand = {
|
|
54
|
+
/** Absolute path (§7 req 2: birth-hooks live in a minimal launchd PATH). */
|
|
55
|
+
command: string;
|
|
56
|
+
args: string[];
|
|
45
57
|
};
|
|
46
58
|
|
|
59
|
+
/** The provision/unprovision blocks of OUR slot — built around the stable
|
|
60
|
+
* installed binary (the same path the hooks/watcher rely on). */
|
|
61
|
+
export function slotProvisionBlocks(binaryPath: string): {
|
|
62
|
+
provision: MemoryProviderCommand;
|
|
63
|
+
unprovision: MemoryProviderCommand;
|
|
64
|
+
} {
|
|
65
|
+
return {
|
|
66
|
+
provision: {
|
|
67
|
+
command: binaryPath,
|
|
68
|
+
args: [
|
|
69
|
+
"provision-peer",
|
|
70
|
+
"--cwd", "{cwd}",
|
|
71
|
+
"--runtime", "{runtime}",
|
|
72
|
+
"--personality", "{personality}",
|
|
73
|
+
"--occasion", "{occasion}",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
unprovision: {
|
|
77
|
+
command: binaryPath,
|
|
78
|
+
args: [
|
|
79
|
+
"unprovision-peer",
|
|
80
|
+
"--cwd", "{cwd}",
|
|
81
|
+
"--runtime", "{runtime}",
|
|
82
|
+
"--occasion", "{occasion}",
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
47
88
|
export type MemoryProviderSlot = {
|
|
48
89
|
provider: string;
|
|
49
90
|
package: string;
|
|
50
91
|
version: string;
|
|
51
92
|
registeredAt: string;
|
|
52
93
|
heartbeat?: string;
|
|
94
|
+
/** v1.1 legacy (read for migration; never written by v1.2 code). */
|
|
53
95
|
plugin?: MemoryProviderPlugin;
|
|
96
|
+
/** v1.2 (ADR-009 v1.2). */
|
|
97
|
+
provision?: MemoryProviderCommand;
|
|
98
|
+
unprovision?: MemoryProviderCommand;
|
|
54
99
|
};
|
|
55
100
|
|
|
56
101
|
/** Never throws: missing / unreadable / malformed → null (empty slot). */
|
|
@@ -72,6 +117,8 @@ export type SlotWriteResult = {
|
|
|
72
117
|
export function writeSlot(opts: {
|
|
73
118
|
slotPath: string;
|
|
74
119
|
version: string;
|
|
120
|
+
/** Absolute path of the installed binary — the provision command carrier. */
|
|
121
|
+
binaryPath: string;
|
|
75
122
|
heartbeat?: string;
|
|
76
123
|
/** Injectable for tests. */
|
|
77
124
|
nowIso?: string;
|
|
@@ -80,15 +127,15 @@ export function writeSlot(opts: {
|
|
|
80
127
|
if (existing && existing.provider !== SLOT_PROVIDER) {
|
|
81
128
|
return { action: "refused-foreign", existing };
|
|
82
129
|
}
|
|
130
|
+
const blocks = slotProvisionBlocks(opts.binaryPath);
|
|
83
131
|
if (
|
|
84
132
|
existing &&
|
|
85
133
|
existing.version === opts.version &&
|
|
86
134
|
existing.heartbeat === opts.heartbeat &&
|
|
87
135
|
existing.package === SLOT_PACKAGE &&
|
|
88
|
-
existing.plugin &&
|
|
89
|
-
existing.
|
|
90
|
-
existing.
|
|
91
|
-
existing.plugin.marketplaceRef === SLOT_PLUGIN.marketplaceRef
|
|
136
|
+
existing.plugin === undefined && // a v1.1 slot (plugin block) must MIGRATE to the v1.2 form
|
|
137
|
+
JSON.stringify(existing.provision) === JSON.stringify(blocks.provision) &&
|
|
138
|
+
JSON.stringify(existing.unprovision) === JSON.stringify(blocks.unprovision)
|
|
92
139
|
) {
|
|
93
140
|
return { action: "identical", existing }; // idempotent re-init: no churn
|
|
94
141
|
}
|
|
@@ -98,11 +145,11 @@ export function writeSlot(opts: {
|
|
|
98
145
|
version: opts.version,
|
|
99
146
|
registeredAt: opts.nowIso ?? new Date().toISOString(),
|
|
100
147
|
...(opts.heartbeat ? { heartbeat: opts.heartbeat } : {}),
|
|
101
|
-
|
|
148
|
+
...blocks,
|
|
102
149
|
};
|
|
103
150
|
fs.mkdirSync(path.dirname(opts.slotPath), { recursive: true });
|
|
104
151
|
const tmp = `${opts.slotPath}.tmp`;
|
|
105
|
-
|
|
152
|
+
guardedWriteFileSync(tmp, JSON.stringify(slot, null, 2) + "\n", "utf-8");
|
|
106
153
|
fs.renameSync(tmp, opts.slotPath);
|
|
107
154
|
return { action: "written", existing };
|
|
108
155
|
}
|
|
@@ -114,7 +161,7 @@ export function removeSlot(slotPath: string): SlotRemoveResult {
|
|
|
114
161
|
const existing = readSlot(slotPath);
|
|
115
162
|
if (!existing) return "absent";
|
|
116
163
|
if (existing.provider !== SLOT_PROVIDER) return "refused-foreign";
|
|
117
|
-
|
|
164
|
+
guardedUnlinkSync(slotPath);
|
|
118
165
|
return "removed";
|
|
119
166
|
}
|
|
120
167
|
|
|
@@ -134,40 +181,38 @@ export type MemoryPluginApplyResult = {
|
|
|
134
181
|
* and `off` runs BEFORE removeSlot (agreed order, auto-removal: a dead
|
|
135
182
|
* provider's plugin must not keep injecting).
|
|
136
183
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
184
|
+
* The hard fuse of incident 10.06 (`on --all` mutates the HOST fleet — no
|
|
185
|
+
* sandbox env contains it) lives in the egress constructor now
|
|
186
|
+
* (deny-by-default §4): a refusing handle blocks the spawn here.
|
|
140
187
|
*/
|
|
141
|
-
export function applyMemoryPlugin(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
188
|
+
export function applyMemoryPlugin(
|
|
189
|
+
egress: Egress,
|
|
190
|
+
opts: {
|
|
191
|
+
mode: "on" | "off";
|
|
192
|
+
iapeerBin?: string;
|
|
193
|
+
},
|
|
194
|
+
): MemoryPluginApplyResult {
|
|
195
|
+
const bin = opts.iapeerBin ?? IAPEER_BIN;
|
|
196
|
+
const proc = egress.spawnSync([bin, "memory-plugin", opts.mode, "--all"], {
|
|
197
|
+
explicitBin: opts.iapeerBin !== undefined,
|
|
198
|
+
});
|
|
199
|
+
if (proc.refused) {
|
|
149
200
|
return {
|
|
150
201
|
ok: false,
|
|
151
202
|
suppressed: true,
|
|
152
203
|
detail: "memory-plugin call suppressed (test sandbox)",
|
|
153
204
|
};
|
|
154
205
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
(proc.stderr.toString().trim() || proc.stdout.toString().trim() || "").slice(0, 200) ||
|
|
166
|
-
`iapeer memory-plugin exited ${proc.exitCode}`,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
return { ok: true, detail: proc.stdout.toString().trim() };
|
|
170
|
-
} catch (err) {
|
|
171
|
-
return { ok: false, detail: `${bin} unavailable: ${String(err)}` };
|
|
206
|
+
if (proc.spawnError) {
|
|
207
|
+
return { ok: false, detail: `${bin} unavailable: ${proc.spawnError}` };
|
|
208
|
+
}
|
|
209
|
+
if (proc.exitCode !== 0) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
detail:
|
|
213
|
+
(proc.stderr.trim() || proc.stdout.trim() || "").slice(0, 200) ||
|
|
214
|
+
`iapeer memory-plugin exited ${proc.exitCode}`,
|
|
215
|
+
};
|
|
172
216
|
}
|
|
217
|
+
return { ok: true, detail: proc.stdout.trim() };
|
|
173
218
|
}
|