@agfpd/iapeer-memory 0.2.0 → 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 +26 -8
- package/src/commands/hook.ts +7 -8
- package/src/commands/init.ts +32 -29
- package/src/commands/install-binary.ts +3 -2
- package/src/commands/memoryd.ts +3 -2
- package/src/commands/status.ts +15 -8
- package/src/commands/uninstall.ts +19 -26
- package/src/commands/update.ts +9 -8
- package/src/commands/verify.ts +15 -8
- package/src/egress.ts +181 -0
- package/src/fleet.ts +33 -35
- package/src/provision.ts +3 -2
- package/src/roles.ts +2 -1
- package/src/signing.ts +19 -23
- package/src/slot.ts +30 -30
- package/src/surfaces/claude.ts +9 -6
- package/src/surfaces/codex.ts +3 -2
- package/src/sync-versions.ts +3 -2
- package/src/templates/index.ts +2 -1
- package/src/watcher.ts +72 -68
package/src/commands/update.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type LocaleId,
|
|
41
41
|
} from "@agfpd/iapeer-memory-core";
|
|
42
42
|
import { installBinary } from "../binary.js";
|
|
43
|
+
import type { Egress } from "../egress.js";
|
|
43
44
|
import { readFleetMap, writeFleetMap } from "../fleet.js";
|
|
44
45
|
import { memoryPaths } from "../paths.js";
|
|
45
46
|
import { readRolesManifest } from "../roles.js";
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
} from "../watcher.js";
|
|
60
61
|
import { stopMemorydByPidFile } from "./uninstall.js";
|
|
61
62
|
|
|
62
|
-
export function cmdUpdate(argv: string[]): number {
|
|
63
|
+
export function cmdUpdate(argv: string[], egress: Egress): number {
|
|
63
64
|
let skipBinary = false;
|
|
64
65
|
let iapeerBin: string | undefined;
|
|
65
66
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -92,7 +93,7 @@ export function cmdUpdate(argv: string[]): number {
|
|
|
92
93
|
if (skipBinary) {
|
|
93
94
|
step("binary", "skipped (--skip-binary)");
|
|
94
95
|
} else {
|
|
95
|
-
const bin = installBinary({ outPath: paths.binaryPath });
|
|
96
|
+
const bin = installBinary(egress, { outPath: paths.binaryPath });
|
|
96
97
|
step(
|
|
97
98
|
"binary",
|
|
98
99
|
bin.action === "compiled"
|
|
@@ -139,7 +140,7 @@ export function cmdUpdate(argv: string[]): number {
|
|
|
139
140
|
// sweep below AND memoryd's fragment renderer, docs/05). BEFORE surfaces
|
|
140
141
|
// and BEFORE the memoryd restart: both consume the fresh map.
|
|
141
142
|
{
|
|
142
|
-
const fleet = writeFleetMap({ fleetMapPath: paths.fleetMapPath, iapeerBin });
|
|
143
|
+
const fleet = writeFleetMap(egress, { fleetMapPath: paths.fleetMapPath, iapeerBin });
|
|
143
144
|
step(
|
|
144
145
|
"fleet",
|
|
145
146
|
fleet.action === "written"
|
|
@@ -200,7 +201,7 @@ export function cmdUpdate(argv: string[]): number {
|
|
|
200
201
|
false,
|
|
201
202
|
);
|
|
202
203
|
} else {
|
|
203
|
-
const off = applyMemoryPlugin({ mode: "off" });
|
|
204
|
+
const off = applyMemoryPlugin(egress, { mode: "off" });
|
|
204
205
|
step(
|
|
205
206
|
"plugin-off",
|
|
206
207
|
off.suppressed
|
|
@@ -247,11 +248,11 @@ export function cmdUpdate(argv: string[]): number {
|
|
|
247
248
|
} catch {
|
|
248
249
|
// unprovisioned env — registrations below still re-target
|
|
249
250
|
}
|
|
250
|
-
const w = registerWatcher({ launcherPath: paths.launcherPath });
|
|
251
|
-
const s = registerTimer({
|
|
251
|
+
const w = registerWatcher(egress, { launcherPath: paths.launcherPath });
|
|
252
|
+
const s = registerTimer(egress, {
|
|
252
253
|
message: sweepTimerMessage({ checkScriptPath: paths.checkScriptPath }),
|
|
253
254
|
});
|
|
254
|
-
const d = registerTimer({ message: dreamTimerMessage() });
|
|
255
|
+
const d = registerTimer(egress, { message: dreamTimerMessage() });
|
|
255
256
|
const sandboxed = w.suppressed && s.suppressed && d.suppressed;
|
|
256
257
|
step(
|
|
257
258
|
"triggers",
|
|
@@ -282,7 +283,7 @@ export function cmdUpdate(argv: string[]): number {
|
|
|
282
283
|
}
|
|
283
284
|
|
|
284
285
|
// 9. memoryd managed restart (the watcher relaunches with the new binary)
|
|
285
|
-
step("memoryd", `${stopMemorydByPidFile(paths.pidPath)} — the notifier watcher relaunches it with the new binary`);
|
|
286
|
+
step("memoryd", `${stopMemorydByPidFile(egress, paths.pidPath)} — the notifier watcher relaunches it with the new binary`);
|
|
286
287
|
|
|
287
288
|
console.log(
|
|
288
289
|
failures
|
package/src/commands/verify.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
renderDoctrine,
|
|
26
26
|
renderedVersion,
|
|
27
27
|
} from "@agfpd/iapeer-memory-core";
|
|
28
|
+
import type { Egress } from "../egress.js";
|
|
28
29
|
import { readFleetMap, writeFleetMap } from "../fleet.js";
|
|
29
30
|
import { memoryPaths, type MemoryPaths } from "../paths.js";
|
|
30
31
|
import { readRolesManifest } from "../roles.js";
|
|
@@ -68,7 +69,7 @@ type RolesManifest = {
|
|
|
68
69
|
roles: Array<{ role: string; peerCwd: string; template: string }>;
|
|
69
70
|
};
|
|
70
71
|
|
|
71
|
-
export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
72
|
+
export function runVerify(egress: Egress, opts: VerifyOptions = {}): CheckResult[] {
|
|
72
73
|
const repair = opts.repair ?? false;
|
|
73
74
|
const paths = opts.paths ?? memoryPaths();
|
|
74
75
|
const version = opts.version ?? packageVersion();
|
|
@@ -188,7 +189,7 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
188
189
|
if (!repair) {
|
|
189
190
|
results.push({ name: "fleet-map", status: "fail", detail: problem });
|
|
190
191
|
} else {
|
|
191
|
-
const w = writeFleetMap({ fleetMapPath: paths.fleetMapPath, iapeerBin: opts.iapeerBin });
|
|
192
|
+
const w = writeFleetMap(egress, { fleetMapPath: paths.fleetMapPath, iapeerBin: opts.iapeerBin });
|
|
192
193
|
results.push(
|
|
193
194
|
w.action === "written"
|
|
194
195
|
? { name: "fleet-map", status: "repaired", detail: `${problem} — ${w.detail}` }
|
|
@@ -343,7 +344,7 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
343
344
|
launcherPath: paths.launcherPath,
|
|
344
345
|
binaryPath: paths.binaryPath,
|
|
345
346
|
});
|
|
346
|
-
return registerWatcher({
|
|
347
|
+
return registerWatcher(egress, {
|
|
347
348
|
launcherPath: paths.launcherPath,
|
|
348
349
|
iapeerBin: opts.iapeerBin,
|
|
349
350
|
});
|
|
@@ -373,7 +374,7 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
373
374
|
} catch {
|
|
374
375
|
// unprovisioned env — the registration alone still heals the trigger
|
|
375
376
|
}
|
|
376
|
-
return registerTimer({
|
|
377
|
+
return registerTimer(egress, {
|
|
377
378
|
message: sweepTimerMessage({ checkScriptPath: paths.checkScriptPath }),
|
|
378
379
|
iapeerBin: opts.iapeerBin,
|
|
379
380
|
});
|
|
@@ -386,7 +387,7 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
386
387
|
expect: (t) =>
|
|
387
388
|
t.target !== "index" ? `target is ${t.target ?? "?"}, expected index` : null,
|
|
388
389
|
repairSend: () =>
|
|
389
|
-
registerTimer({ message: dreamTimerMessage(), iapeerBin: opts.iapeerBin }),
|
|
390
|
+
registerTimer(egress, { message: dreamTimerMessage(), iapeerBin: opts.iapeerBin }),
|
|
390
391
|
},
|
|
391
392
|
];
|
|
392
393
|
for (const c of checks) {
|
|
@@ -499,17 +500,23 @@ export function runVerify(opts: VerifyOptions = {}): CheckResult[] {
|
|
|
499
500
|
return results;
|
|
500
501
|
}
|
|
501
502
|
|
|
502
|
-
export function cmdVerify(argv: string[]): number {
|
|
503
|
+
export function cmdVerify(argv: string[], egress: Egress): number {
|
|
503
504
|
let repair = false;
|
|
504
|
-
|
|
505
|
+
let iapeerBin: string | undefined;
|
|
506
|
+
for (let i = 0; i < argv.length; i++) {
|
|
507
|
+
const a = argv[i];
|
|
505
508
|
if (a === "--repair") repair = true;
|
|
509
|
+
// Mirror of `update --iapeer-bin` (fb662ed): the hermetic CLI test class
|
|
510
|
+
// needs an explicitly named core binary — the egress explicit-bin
|
|
511
|
+
// allowance keys on it.
|
|
512
|
+
else if (a === "--iapeer-bin") iapeerBin = argv[++i];
|
|
506
513
|
else {
|
|
507
514
|
console.error(`iapeer-memory verify: unknown flag: ${a}`);
|
|
508
515
|
return 2;
|
|
509
516
|
}
|
|
510
517
|
}
|
|
511
518
|
|
|
512
|
-
const results = runVerify({ repair });
|
|
519
|
+
const results = runVerify(egress, { repair, iapeerBin });
|
|
513
520
|
const width = Math.max(...results.map((r) => r.name.length));
|
|
514
521
|
for (const r of results) {
|
|
515
522
|
const mark =
|
package/src/egress.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Egress hub — the ONE doorway from this package to the live host
|
|
3
|
+
* (docs/_planning/DENY_BY_DEFAULT_DESIGN.md §4, accepted by boris 11.06).
|
|
4
|
+
*
|
|
5
|
+
* Topology, not another fuse: four incidents («тест дотянулся до прода»)
|
|
6
|
+
* shared one root — outbound channels were allowed by default and refused
|
|
7
|
+
* only under a test flag each call site had to remember. This module
|
|
8
|
+
* inverts the default. Modules never spawn/kill/probe the host themselves
|
|
9
|
+
* and never PATH-resolve an external binary — they take an explicit
|
|
10
|
+
* {@link Egress} handle. The single live constructor, {@link liveEgress},
|
|
11
|
+
* is called from `cli.ts main()`; while a test-sandbox env is armed it
|
|
12
|
+
* hands back a REFUSING handle instead, and every channel reports refusal
|
|
13
|
+
* (callers map it to their SKIP semantics — the iapeer `skipped-sandbox`
|
|
14
|
+
* precedent). A module imported directly by a test has no doorway to the
|
|
15
|
+
* host by construction: there is nothing to forget.
|
|
16
|
+
*
|
|
17
|
+
* The grep invariant (И3) pins the topology: no `Bun.spawn*`/`process.kill`
|
|
18
|
+
* outside this file in src/.
|
|
19
|
+
*
|
|
20
|
+
* EXPLICIT ALLOWANCES of the refusing handle — each narrow, each here, all
|
|
21
|
+
* in one place (deny by DEFAULT, authorized consciously):
|
|
22
|
+
*
|
|
23
|
+
* 1. `explicitBin` spawns — argv[0] was NAMED by the operator/test via a
|
|
24
|
+
* flag (`--iapeer-bin <path>`). Same safety class as the sanctioned
|
|
25
|
+
* fake-bin test pattern: a consciously named binary is an authorization,
|
|
26
|
+
* a PATH-resolved default is not. (Closes the old env-juggling dance:
|
|
27
|
+
* fake-bin tests no longer clear the sandbox vars.)
|
|
28
|
+
* 2. Self-runtime spawns — argv[0] === process.execPath (bun): the binary
|
|
29
|
+
* compile (`bun build --compile` to a path-conventioned target) and the
|
|
30
|
+
* hook kick (self `verify --repair`). A child process re-enters through
|
|
31
|
+
* its OWN main() and inherits the sandbox env → its egress refuses too;
|
|
32
|
+
* nothing transitively reaches the host.
|
|
33
|
+
* 3. `ps` probes — read-only process-table lookup feeding the verified-kill
|
|
34
|
+
* guard (`pidLooksLikeOurs`). Refusing it would break the guard whose
|
|
35
|
+
* whole job is to make kill() safe.
|
|
36
|
+
* 4. Loopback fetch — status' own-daemon probes in sandboxed e2e. The
|
|
37
|
+
* host-daemon collision is closed by test port isolation (И3), not by
|
|
38
|
+
* refusing the probe. Non-loopback fetch refuses.
|
|
39
|
+
*
|
|
40
|
+
* kill() stays guarded by the verified-kill contract (owner verification
|
|
41
|
+
* before signalling — accepted at the P3c review), not by refusal: the pid
|
|
42
|
+
* PROVENANCE (sandbox pid file vs prod pid file) is the FS-belt's question
|
|
43
|
+
* (И2), and refusing kill would orphan sandbox daemons in e2e.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
export type EgressSpawnResult = {
|
|
47
|
+
exitCode: number;
|
|
48
|
+
stdout: string;
|
|
49
|
+
stderr: string;
|
|
50
|
+
/** Set when the spawn itself failed (binary missing) or the egress
|
|
51
|
+
* refused the channel — exitCode is 127 by convention then. */
|
|
52
|
+
spawnError?: string;
|
|
53
|
+
/** True when the refusing egress (test sandbox) blocked the channel. */
|
|
54
|
+
refused?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type EgressSpawnOpts = {
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
/** argv[0] was explicitly named by the operator/test (a `--*-bin` flag) —
|
|
60
|
+
* allowance 1 of the refusing handle. */
|
|
61
|
+
explicitBin?: boolean;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export interface Egress {
|
|
65
|
+
/** True for the refusing handle — callers may report a SKIP up front. */
|
|
66
|
+
readonly refused: boolean;
|
|
67
|
+
/** External binary, synchronous (iapeer, ps, openssl, security, codesign,
|
|
68
|
+
* bun). Never throws — a missing binary is a result, not a crash. */
|
|
69
|
+
spawnSync(argv: string[], opts?: EgressSpawnOpts): EgressSpawnResult;
|
|
70
|
+
/** Fire-and-forget detached spawn (hook kick → self `verify --repair`). */
|
|
71
|
+
spawnDetached(argv: string[]): { started: boolean; detail?: string };
|
|
72
|
+
/** Signal a live process. Never throws; `delivered: false` = process gone. */
|
|
73
|
+
kill(pid: number, signal: NodeJS.Signals): { delivered: boolean };
|
|
74
|
+
/** HTTP probe (status' loopback checks) — read-as-egress (П5). */
|
|
75
|
+
fetch(url: string, init?: RequestInit): Promise<Response>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Default name of the ecosystem CLI — the ONE place it lives (П2: no
|
|
79
|
+
* scattered `?? "iapeer"` defaults; the danger moved into the handle). */
|
|
80
|
+
export const IAPEER_BIN = "iapeer";
|
|
81
|
+
|
|
82
|
+
/** The ONE definition of the sandbox-env check lives in core's fs-guard
|
|
83
|
+
* (the FS belt uses it too) — re-exported here for the constructor and
|
|
84
|
+
* its tests. */
|
|
85
|
+
import { sandboxEnvArmed } from "@agfpd/iapeer-memory-core";
|
|
86
|
+
export { sandboxEnvArmed };
|
|
87
|
+
|
|
88
|
+
function rawSpawnSync(argv: string[], opts?: EgressSpawnOpts): EgressSpawnResult {
|
|
89
|
+
try {
|
|
90
|
+
const proc = Bun.spawnSync(argv, {
|
|
91
|
+
stdout: "pipe",
|
|
92
|
+
stderr: "pipe",
|
|
93
|
+
...(opts?.timeoutMs !== undefined ? { timeout: opts.timeoutMs } : {}),
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
exitCode: proc.exitCode,
|
|
97
|
+
stdout: proc.stdout.toString(),
|
|
98
|
+
stderr: proc.stderr.toString(),
|
|
99
|
+
};
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return { exitCode: 127, stdout: "", stderr: "", spawnError: String(err) };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function rawSpawnDetached(argv: string[]): { started: boolean; detail?: string } {
|
|
106
|
+
try {
|
|
107
|
+
const proc = Bun.spawn(argv, {
|
|
108
|
+
stdout: "ignore",
|
|
109
|
+
stderr: "ignore",
|
|
110
|
+
stdin: "ignore",
|
|
111
|
+
});
|
|
112
|
+
proc.unref();
|
|
113
|
+
return { started: true };
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return { started: false, detail: String(err) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function rawKill(pid: number, signal: NodeJS.Signals): { delivered: boolean } {
|
|
120
|
+
try {
|
|
121
|
+
process.kill(pid, signal);
|
|
122
|
+
return { delivered: true };
|
|
123
|
+
} catch {
|
|
124
|
+
return { delivered: false };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isLoopback(url: string): boolean {
|
|
129
|
+
try {
|
|
130
|
+
const host = new URL(url).hostname;
|
|
131
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const REFUSAL = "egress refused (test sandbox) — pass a fake egress";
|
|
138
|
+
|
|
139
|
+
function refusedResult(): EgressSpawnResult {
|
|
140
|
+
return { exitCode: 127, stdout: "", stderr: "", spawnError: REFUSAL, refused: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function refusingEgress(): Egress {
|
|
144
|
+
return {
|
|
145
|
+
refused: true,
|
|
146
|
+
spawnSync(argv, opts) {
|
|
147
|
+
if (opts?.explicitBin) return rawSpawnSync(argv, opts); // allowance 1
|
|
148
|
+
if (argv[0] === process.execPath) return rawSpawnSync(argv, opts); // allowance 2
|
|
149
|
+
if (argv[0] === "ps") return rawSpawnSync(argv, opts); // allowance 3
|
|
150
|
+
return refusedResult();
|
|
151
|
+
},
|
|
152
|
+
spawnDetached(argv) {
|
|
153
|
+
if (argv[0] === process.execPath) return rawSpawnDetached(argv); // allowance 2
|
|
154
|
+
return { started: false, detail: REFUSAL };
|
|
155
|
+
},
|
|
156
|
+
kill: rawKill, // verified-kill contract guards this, not refusal (header)
|
|
157
|
+
fetch(url, init) {
|
|
158
|
+
if (isLoopback(url)) return fetch(url, init); // allowance 4
|
|
159
|
+
return Promise.reject(new Error(REFUSAL));
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function realEgress(): Egress {
|
|
165
|
+
return {
|
|
166
|
+
refused: false,
|
|
167
|
+
spawnSync: rawSpawnSync,
|
|
168
|
+
spawnDetached: rawSpawnDetached,
|
|
169
|
+
kill: rawKill,
|
|
170
|
+
fetch: (url, init) => fetch(url, init),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The ONE live constructor — called from `cli.ts main()` only. Refuses
|
|
176
|
+
* (hands back the refusing handle) while a test-sandbox env is armed; the
|
|
177
|
+
* decision is taken ONCE here, never re-checked at call sites.
|
|
178
|
+
*/
|
|
179
|
+
export function liveEgress(): Egress {
|
|
180
|
+
return sandboxEnvArmed() ? refusingEgress() : realEgress();
|
|
181
|
+
}
|
package/src/fleet.ts
CHANGED
|
@@ -7,18 +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
|
-
* `iapeer list` is read-only, but its RESULT is the target list of the
|
|
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
12
|
* surfaces sweep — a sandboxed `verify --repair` with no fleet map repaired
|
|
13
13
|
* the map from the LIVE registry and then swept the LIVE peers' cwds with
|
|
14
14
|
* direct surfaces (the send-fuse never saw it: no IAP send involved).
|
|
15
|
-
* Querying the live registry from a test IS the leak —
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import fs from "node:fs";
|
|
21
22
|
import path from "node:path";
|
|
23
|
+
import { IAPEER_BIN, type Egress } from "./egress.js";
|
|
24
|
+
import { guardedWriteFileSync } from "@agfpd/iapeer-memory-core";
|
|
22
25
|
|
|
23
26
|
export type FleetMapResult = {
|
|
24
27
|
action: "written" | "failed";
|
|
@@ -67,42 +70,37 @@ export function readFleetMap(fleetMapPath: string): FleetPeer[] | null {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
export function writeFleetMap(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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) {
|
|
81
87
|
return {
|
|
82
88
|
action: "failed",
|
|
83
89
|
count: 0,
|
|
84
90
|
detail: "live-registry query suppressed (test sandbox) — pass a fake iapeerBin",
|
|
85
91
|
};
|
|
86
92
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
action: "failed",
|
|
97
|
-
count: 0,
|
|
98
|
-
detail:
|
|
99
|
-
(proc.stderr.toString().trim() || `iapeer list exited ${proc.exitCode}`).slice(0, 160),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
stdout = proc.stdout.toString();
|
|
103
|
-
} catch (err) {
|
|
104
|
-
return { action: "failed", count: 0, detail: `${bin} unavailable: ${String(err)}` };
|
|
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
|
+
};
|
|
105
102
|
}
|
|
103
|
+
const stdout = proc.stdout;
|
|
106
104
|
|
|
107
105
|
let listed: ListedPeer[];
|
|
108
106
|
try {
|
|
@@ -140,7 +138,7 @@ export function writeFleetMap(opts: {
|
|
|
140
138
|
) + "\n";
|
|
141
139
|
fs.mkdirSync(path.dirname(opts.fleetMapPath), { recursive: true });
|
|
142
140
|
const tmp = `${opts.fleetMapPath}.tmp`;
|
|
143
|
-
|
|
141
|
+
guardedWriteFileSync(tmp, body, "utf-8");
|
|
144
142
|
fs.renameSync(tmp, opts.fleetMapPath); // atomic — memoryd may race a read
|
|
145
143
|
return {
|
|
146
144
|
action: "written",
|
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
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
|
|
32
32
|
import fs from "node:fs";
|
|
33
33
|
import path from "node:path";
|
|
34
|
+
import { IAPEER_BIN, type Egress } from "./egress.js";
|
|
35
|
+
import { guardedWriteFileSync, guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
|
|
34
36
|
|
|
35
37
|
export const SLOT_PROVIDER = "iapeer-memory";
|
|
36
38
|
export const SLOT_PACKAGE = "@agfpd/iapeer-memory";
|
|
@@ -147,7 +149,7 @@ export function writeSlot(opts: {
|
|
|
147
149
|
};
|
|
148
150
|
fs.mkdirSync(path.dirname(opts.slotPath), { recursive: true });
|
|
149
151
|
const tmp = `${opts.slotPath}.tmp`;
|
|
150
|
-
|
|
152
|
+
guardedWriteFileSync(tmp, JSON.stringify(slot, null, 2) + "\n", "utf-8");
|
|
151
153
|
fs.renameSync(tmp, opts.slotPath);
|
|
152
154
|
return { action: "written", existing };
|
|
153
155
|
}
|
|
@@ -159,7 +161,7 @@ export function removeSlot(slotPath: string): SlotRemoveResult {
|
|
|
159
161
|
const existing = readSlot(slotPath);
|
|
160
162
|
if (!existing) return "absent";
|
|
161
163
|
if (existing.provider !== SLOT_PROVIDER) return "refused-foreign";
|
|
162
|
-
|
|
164
|
+
guardedUnlinkSync(slotPath);
|
|
163
165
|
return "removed";
|
|
164
166
|
}
|
|
165
167
|
|
|
@@ -179,40 +181,38 @@ export type MemoryPluginApplyResult = {
|
|
|
179
181
|
* and `off` runs BEFORE removeSlot (agreed order, auto-removal: a dead
|
|
180
182
|
* provider's plugin must not keep injecting).
|
|
181
183
|
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
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.
|
|
185
187
|
*/
|
|
186
|
-
export function applyMemoryPlugin(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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) {
|
|
194
200
|
return {
|
|
195
201
|
ok: false,
|
|
196
202
|
suppressed: true,
|
|
197
203
|
detail: "memory-plugin call suppressed (test sandbox)",
|
|
198
204
|
};
|
|
199
205
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
(proc.stderr.toString().trim() || proc.stdout.toString().trim() || "").slice(0, 200) ||
|
|
211
|
-
`iapeer memory-plugin exited ${proc.exitCode}`,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
return { ok: true, detail: proc.stdout.toString().trim() };
|
|
215
|
-
} catch (err) {
|
|
216
|
-
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
|
+
};
|
|
217
216
|
}
|
|
217
|
+
return { ok: true, detail: proc.stdout.trim() };
|
|
218
218
|
}
|