@agfpd/iapeer-memory 0.2.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer-memory",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "iapeer-memory — peer memory for the iapeer ecosystem: vault, memoryd (index/search/MCP-http), layer-5 context fragments, role doctrines. The package IS the system; the claude/codex plugins are thin session sockets (docs/10-distribution.md, ADR-009).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@agfpd/iapeer-memory-core": "0.2.0"
30
+ "@agfpd/iapeer-memory-core": "0.2.2"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/bun": "^1.2.0",
package/src/binary.ts CHANGED
@@ -23,7 +23,9 @@
23
23
 
24
24
  import fs from "node:fs";
25
25
  import path from "node:path";
26
+ import type { Egress } from "./egress.js";
26
27
  import { signInstalledBinary, type SigningOutcome } from "./signing.js";
28
+ import { guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
27
29
 
28
30
  export function isCompiledRuntime(): boolean {
29
31
  return import.meta.url.includes("/$bunfs/");
@@ -34,7 +36,10 @@ export type InstallBinaryOutcome =
34
36
  | { action: "skipped-compiled"; outPath: string }
35
37
  | { action: "failed"; outPath: string; detail: string };
36
38
 
37
- export function installBinary(opts: { outPath: string }): InstallBinaryOutcome {
39
+ export function installBinary(
40
+ egress: Egress,
41
+ opts: { outPath: string },
42
+ ): InstallBinaryOutcome {
38
43
  const { outPath } = opts;
39
44
  if (isCompiledRuntime()) {
40
45
  return { action: "skipped-compiled", outPath };
@@ -47,20 +52,22 @@ export function installBinary(opts: { outPath: string }): InstallBinaryOutcome {
47
52
  `.${path.basename(outPath)}.build.tmp`,
48
53
  );
49
54
 
50
- const proc = Bun.spawnSync(
51
- [process.execPath, "build", "--compile", cliPath, "--outfile", tmp],
52
- { stdout: "pipe", stderr: "pipe" },
53
- );
54
- if (proc.exitCode !== 0 || !fs.existsSync(tmp)) {
55
+ // Self-runtime spawn (egress allowance 2): compiling OUR cli with OUR bun
56
+ // to a path-conventioned target — works in sandboxed e2e by design.
57
+ const proc = egress.spawnSync([
58
+ process.execPath, "build", "--compile", cliPath, "--outfile", tmp,
59
+ ]);
60
+ if (proc.spawnError || proc.exitCode !== 0 || !fs.existsSync(tmp)) {
55
61
  try {
56
- if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
62
+ if (fs.existsSync(tmp)) guardedUnlinkSync(tmp);
57
63
  } catch {
58
64
  // best effort
59
65
  }
60
66
  return {
61
67
  action: "failed",
62
68
  outPath,
63
- detail: proc.stderr.toString().trim() || `bun build exited ${proc.exitCode}`,
69
+ detail:
70
+ proc.spawnError || proc.stderr.trim() || `bun build exited ${proc.exitCode}`,
64
71
  };
65
72
  }
66
73
 
@@ -68,12 +75,12 @@ export function installBinary(opts: { outPath: string }): InstallBinaryOutcome {
68
75
  fs.renameSync(tmp, outPath); // atomic swap — safe over a running binary on macOS
69
76
  // Stable-identity re-sign on EVERY compile path (TCC grants survive
70
77
  // updates — contract with iapeer, see signing.ts). Soft-fail by design.
71
- const signing = signInstalledBinary(outPath);
78
+ const signing = signInstalledBinary(egress, outPath);
72
79
  return { action: "compiled", outPath, bytes: fs.statSync(outPath).size, signing };
73
80
  }
74
81
 
75
82
  export function removeBinary(outPath: string): "removed" | "absent" {
76
83
  if (!fs.existsSync(outPath)) return "absent";
77
- fs.unlinkSync(outPath);
84
+ guardedUnlinkSync(outPath);
78
85
  return "removed";
79
86
  }
package/src/cli.ts CHANGED
@@ -13,7 +13,9 @@
13
13
  * verify found problems, 2 usage error or not-yet-implemented stage.
14
14
  */
15
15
 
16
+ import { isUnderProdAnchor, sandboxEnvArmed } from "@agfpd/iapeer-memory-core";
16
17
  import { loadConfigFile } from "./config-env.js";
18
+ import { liveEgress } from "./egress.js";
17
19
  import { memoryPaths } from "./paths.js";
18
20
  import { packageVersion } from "./version.js";
19
21
  import { cmdFmUpdate } from "./commands/fm-update.js";
@@ -78,10 +80,26 @@ export async function main(argv: string[]): Promise<number> {
78
80
 
79
81
  // The config file is the env context of every command (except pure
80
82
  // help/version, where a broken file must not block the output).
83
+ // Deny-by-default §7.2 (accepted): under a test sandbox the LIVE host's
84
+ // config.env is never read — it would pull the live vault path, the
85
+ // embedding/reranker endpoints and the production port into a sandboxed
86
+ // process. A test must pass ITS OWN IAPEER_MEMORY_CONFIG_FILE.
81
87
  if (cmd && !["help", "--help", "-h", "version", "--version", "-V"].includes(cmd)) {
82
- loadConfigFile(memoryPaths().configFile);
88
+ const configFile = memoryPaths().configFile;
89
+ if (sandboxEnvArmed() && isUnderProdAnchor(configFile)) {
90
+ console.error(
91
+ `iapeer-memory: live config.env skipped under the test sandbox (${configFile}) — pass IAPEER_MEMORY_CONFIG_FILE`,
92
+ );
93
+ } else {
94
+ loadConfigFile(configFile);
95
+ }
83
96
  }
84
97
 
98
+ // The ONE egress construction point (deny-by-default §4 П2): under a
99
+ // test-sandbox env this is a refusing handle — commands degrade to their
100
+ // SKIP semantics; nothing below re-checks the env.
101
+ const egress = liveEgress();
102
+
85
103
  switch (cmd) {
86
104
  case undefined:
87
105
  case "help":
@@ -95,17 +113,17 @@ export async function main(argv: string[]): Promise<number> {
95
113
  console.log(packageVersion());
96
114
  return 0;
97
115
  case "init":
98
- return cmdInit(rest);
116
+ return cmdInit(rest, egress);
99
117
  case "uninstall":
100
- return cmdUninstall(rest);
118
+ return cmdUninstall(rest, egress);
101
119
  case "status":
102
- return cmdStatus(rest);
120
+ return cmdStatus(rest, egress);
103
121
  case "verify":
104
- return cmdVerify(rest);
122
+ return cmdVerify(rest, egress);
105
123
  case "update":
106
- return cmdUpdate(rest);
124
+ return cmdUpdate(rest, egress);
107
125
  case "install-binary":
108
- return cmdInstallBinary(rest);
126
+ return cmdInstallBinary(rest, egress);
109
127
  case "provision-peer":
110
128
  return cmdProvisionPeer(rest);
111
129
  case "unprovision-peer":
@@ -119,7 +137,7 @@ export async function main(argv: string[]): Promise<number> {
119
137
  case "memoryd":
120
138
  return cmdMemoryd(rest);
121
139
  case "hook":
122
- return cmdHook(rest);
140
+ return cmdHook(rest, egress);
123
141
  default:
124
142
  console.error(`iapeer-memory: unknown command: ${cmd}\n`);
125
143
  console.error(USAGE);
@@ -32,6 +32,7 @@
32
32
 
33
33
  import fs from "node:fs";
34
34
  import path from "node:path";
35
+ import type { Egress } from "../egress.js";
35
36
  import {
36
37
  DEFAULT_CURATOR_SET,
37
38
  fmUpdate,
@@ -41,6 +42,7 @@ import {
41
42
  } from "@agfpd/iapeer-memory-core";
42
43
  import { memoryPaths, type MemoryPaths } from "../paths.js";
43
44
  import { DEFAULT_HEARTBEAT_STALE_MS } from "./verify.js";
45
+ import { guardedWriteFileSync } from "@agfpd/iapeer-memory-core";
44
46
 
45
47
  /** Tools whose writes stamp frontmatter. P5 adds "apply_patch" (codex). */
46
48
  export const POST_WRITE_TOOLS: ReadonlySet<string> = new Set([
@@ -193,7 +195,7 @@ export function runSessionStart(opts: {
193
195
  if (!recentKick) {
194
196
  try {
195
197
  fs.mkdirSync(paths.stateDir, { recursive: true });
196
- fs.writeFileSync(stamp, `${new Date(nowMs).toISOString()}\n`);
198
+ guardedWriteFileSync(stamp, `${new Date(nowMs).toISOString()}\n`);
197
199
  opts.kick?.();
198
200
  kicked = true;
199
201
  } catch {
@@ -227,7 +229,7 @@ function logHookError(err: unknown): void {
227
229
  }
228
230
  }
229
231
 
230
- export async function cmdHook(argv: string[]): Promise<number> {
232
+ export async function cmdHook(argv: string[], egress: Egress): Promise<number> {
231
233
  const [event] = argv;
232
234
  try {
233
235
  switch (event) {
@@ -239,13 +241,10 @@ export async function cmdHook(argv: string[]): Promise<number> {
239
241
  case "session-start": {
240
242
  const result = runSessionStart({
241
243
  kick: () => {
244
+ // Self-runtime detached spawn (egress allowance 2): the child
245
+ // re-enters main() with its own egress — sandbox env inherits.
242
246
  const cli = new URL("../cli.ts", import.meta.url).pathname;
243
- const proc = Bun.spawn([process.execPath, cli, "verify", "--repair"], {
244
- stdout: "ignore",
245
- stderr: "ignore",
246
- stdin: "ignore",
247
- });
248
- proc.unref();
247
+ egress.spawnDetached([process.execPath, cli, "verify", "--repair"]);
249
248
  },
250
249
  });
251
250
  if (result.output) console.log(result.output);
@@ -32,6 +32,7 @@ import {
32
32
  type LocaleId,
33
33
  } from "@agfpd/iapeer-memory-core";
34
34
  import { installBinary } from "../binary.js";
35
+ import { IAPEER_BIN, type Egress } from "../egress.js";
35
36
  import { memoryPaths } from "../paths.js";
36
37
  import { provisionVault, writeDefaultConfig } from "../provision.js";
37
38
  import { writeRolesManifest, type RoleEntry } from "../roles.js";
@@ -73,7 +74,9 @@ type InitFlags = {
73
74
  * the fleet may still carry the predecessor's guide; ours lands by a separate
74
75
  * decision after the plugin swap). */
75
76
  skipGuide: boolean;
76
- iapeerBin: string;
77
+ /** Explicitly named core binary (--iapeer-bin) — undefined means the PATH
78
+ * default; the distinction feeds the egress explicit-bin allowance. */
79
+ iapeerBin?: string;
77
80
  };
78
81
 
79
82
  function parseFlags(argv: string[]): InitFlags | null {
@@ -83,7 +86,6 @@ function parseFlags(argv: string[]): InitFlags | null {
83
86
  skipEcosystem: false,
84
87
  skipBinary: false,
85
88
  skipGuide: false,
86
- iapeerBin: "iapeer",
87
89
  };
88
90
  for (let i = 0; i < argv.length; i++) {
89
91
  const a = argv[i];
@@ -99,7 +101,7 @@ function parseFlags(argv: string[]): InitFlags | null {
99
101
  case "--skip-ecosystem": f.skipEcosystem = true; break;
100
102
  case "--skip-binary": f.skipBinary = true; break;
101
103
  case "--skip-guide": f.skipGuide = true; break;
102
- case "--iapeer-bin": f.iapeerBin = take() ?? "iapeer"; break;
104
+ case "--iapeer-bin": f.iapeerBin = take(); break;
103
105
  default:
104
106
  console.error(`iapeer-memory init: unknown flag: ${a}`);
105
107
  return null;
@@ -114,22 +116,23 @@ type PeerInfo = { personality: string; intelligence?: string; cwd?: string };
114
116
 
115
117
  type RunResult = { exitCode: number; stdout: string; stderr: string };
116
118
 
117
- /** spawnSync that never throws — a missing binary is a result, not a crash. */
118
- function run(cmd: string[]): RunResult {
119
- try {
120
- const proc = Bun.spawnSync(cmd, { stdout: "pipe", stderr: "pipe" });
121
- return {
122
- exitCode: proc.exitCode,
123
- stdout: proc.stdout.toString(),
124
- stderr: proc.stderr.toString(),
125
- };
126
- } catch (err) {
127
- return { exitCode: 127, stdout: "", stderr: String(err) };
128
- }
119
+ /** Egress spawn that never throws — a missing binary (and a refusing test
120
+ * egress) is a result, not a crash: the ecosystem half degrades to the
121
+ * same skip path as «iapeer not on this host». */
122
+ function run(
123
+ egress: Egress,
124
+ cmd: string[],
125
+ opts?: { explicitBin?: boolean },
126
+ ): RunResult {
127
+ const proc = egress.spawnSync(cmd, { explicitBin: opts?.explicitBin });
128
+ if (proc.spawnError) return { exitCode: 127, stdout: "", stderr: proc.spawnError };
129
+ return { exitCode: proc.exitCode, stdout: proc.stdout, stderr: proc.stderr };
129
130
  }
130
131
 
131
- function listPeers(iapeerBin: string): PeerInfo[] | null {
132
- const proc = run([iapeerBin, "list", "--json"]);
132
+ function listPeers(egress: Egress, iapeerBin?: string): PeerInfo[] | null {
133
+ const proc = run(egress, [iapeerBin ?? IAPEER_BIN, "list", "--json"], {
134
+ explicitBin: iapeerBin !== undefined,
135
+ });
133
136
  if (proc.exitCode !== 0) return null;
134
137
  try {
135
138
  return JSON.parse(proc.stdout) as PeerInfo[];
@@ -155,7 +158,7 @@ function ask(question: string, fallback: string): string {
155
158
 
156
159
  // ── the command ──────────────────────────────────────────────────────────────
157
160
 
158
- export async function cmdInit(argv: string[]): Promise<number> {
161
+ export async function cmdInit(argv: string[], egress: Egress): Promise<number> {
159
162
  const flags = parseFlags(argv);
160
163
  if (!flags) return 2;
161
164
 
@@ -169,7 +172,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
169
172
  let embeddingEndpoint = flags.embeddingEndpoint ?? "";
170
173
  const rerankerEndpoint = flags.rerankerEndpoint ?? "";
171
174
 
172
- const peers = flags.skipEcosystem ? null : listPeers(flags.iapeerBin);
175
+ const peers = flags.skipEcosystem ? null : listPeers(egress, flags.iapeerBin);
173
176
  const humanDefault = flags.human ?? naturalPeerDefault(peers) ?? "";
174
177
 
175
178
  if (!vault) {
@@ -215,7 +218,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
215
218
  if (flags.skipDeps) {
216
219
  step("deps", "skipped (--skip-deps)");
217
220
  } else {
218
- const ver = run([flags.iapeerBin, "version"]);
221
+ const ver = run(egress, [flags.iapeerBin ?? IAPEER_BIN, "version"], { explicitBin: flags.iapeerBin !== undefined });
219
222
  if (ver.exitCode !== 0) {
220
223
  step("deps", "iapeer foundation not found on PATH — install it first (npx @agfpd/iapeer)", false);
221
224
  } else {
@@ -259,7 +262,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
259
262
  if (flags.skipBinary) {
260
263
  step("binary", "skipped (--skip-binary)");
261
264
  } else {
262
- const bin = installBinary({ outPath: paths.binaryPath });
265
+ const bin = installBinary(egress, { outPath: paths.binaryPath });
263
266
  step(
264
267
  "binary",
265
268
  bin.action === "compiled"
@@ -290,7 +293,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
290
293
  const personality = rolePersonality(role);
291
294
  const exists = (peers ?? []).some((p) => p.personality === personality);
292
295
  if (!exists) {
293
- const created = run([flags.iapeerBin, "create", personality]);
296
+ const created = run(egress, [flags.iapeerBin ?? IAPEER_BIN, "create", personality], { explicitBin: flags.iapeerBin !== undefined });
294
297
  if (created.exitCode !== 0) {
295
298
  rolesOk = false;
296
299
  console.log(` roles create ${personality} failed: ${created.stderr.trim()}`);
@@ -303,7 +306,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
303
306
  // `iapeer list --json` — iapeer 0.2.14), otherwise the core's
304
307
  // DOCUMENTED create default (no --path — требование Артура;
305
308
  // IAPEER_ROOT-aware).
306
- const freshPeers = createdAny ? listPeers(flags.iapeerBin) : peers;
309
+ const freshPeers = createdAny ? listPeers(egress, flags.iapeerBin) : peers;
307
310
  for (const role of ROLE_NAMES) {
308
311
  const personality = rolePersonality(role);
309
312
  const registryCwd = (freshPeers ?? []).find((p) => p.personality === personality)?.cwd;
@@ -354,7 +357,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
354
357
  if (flags.skipEcosystem) {
355
358
  step("fleet", "skipped (--skip-ecosystem)");
356
359
  } else {
357
- const fleet = writeFleetMap({
360
+ const fleet = writeFleetMap(egress, {
358
361
  fleetMapPath: paths.fleetMapPath,
359
362
  iapeerBin: flags.iapeerBin,
360
363
  });
@@ -376,7 +379,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
376
379
  step("timers", "skipped (--skip-ecosystem)");
377
380
  } else {
378
381
  writeLauncherScript({ launcherPath: paths.launcherPath, binaryPath: paths.binaryPath });
379
- const sent = registerWatcher({
382
+ const sent = registerWatcher(egress, {
380
383
  launcherPath: paths.launcherPath,
381
384
  iapeerBin: flags.iapeerBin,
382
385
  });
@@ -395,11 +398,11 @@ export async function cmdInit(argv: string[]): Promise<number> {
395
398
  vaultPath: vault,
396
399
  inboxFolders: [getTaxonomy(locale).folders.inbox, getTaxonomy(locale).folders.inboxHuman],
397
400
  });
398
- const sweep = registerTimer({
401
+ const sweep = registerTimer(egress, {
399
402
  message: sweepTimerMessage({ checkScriptPath: paths.checkScriptPath }),
400
403
  iapeerBin: flags.iapeerBin,
401
404
  });
402
- const dream = registerTimer({
405
+ const dream = registerTimer(egress, {
403
406
  message: dreamTimerMessage(),
404
407
  iapeerBin: flags.iapeerBin,
405
408
  });
@@ -480,7 +483,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
480
483
  false,
481
484
  );
482
485
  } else {
483
- const off = applyMemoryPlugin({ mode: "off", iapeerBin: flags.iapeerBin });
486
+ const off = applyMemoryPlugin(egress, { mode: "off", iapeerBin: flags.iapeerBin });
484
487
  step(
485
488
  "plugin-off",
486
489
  off.suppressed
@@ -517,7 +520,7 @@ export async function cmdInit(argv: string[]): Promise<number> {
517
520
  if (flags.skipEcosystem) {
518
521
  step("sweep", "skipped (--skip-ecosystem)");
519
522
  } else {
520
- const sweep = run([flags.iapeerBin, "native-memory", "off", "--all"]);
523
+ const sweep = run(egress, [flags.iapeerBin ?? IAPEER_BIN, "native-memory", "off", "--all"], { explicitBin: flags.iapeerBin !== undefined });
521
524
  // One line per peer-runtime — summarise ALL peers, not the last line
522
525
  // (e2e §A finding: ".pop()" named one peer while three were swept).
523
526
  const sweepLines = sweep.stdout.trim().split("\n").filter(Boolean);
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import { installBinary } from "../binary.js";
9
+ import type { Egress } from "../egress.js";
9
10
  import { memoryPaths } from "../paths.js";
10
11
 
11
- export function cmdInstallBinary(argv: string[]): number {
12
+ export function cmdInstallBinary(argv: string[], egress: Egress): number {
12
13
  let outPath = memoryPaths().binaryPath;
13
14
  for (let i = 0; i < argv.length; i++) {
14
15
  if (argv[i] === "--out") {
@@ -24,7 +25,7 @@ export function cmdInstallBinary(argv: string[]): number {
24
25
  }
25
26
  }
26
27
 
27
- const outcome = installBinary({ outPath });
28
+ const outcome = installBinary(egress, { outPath });
28
29
  switch (outcome.action) {
29
30
  case "compiled":
30
31
  console.log(
@@ -19,6 +19,7 @@
19
19
  import fs from "node:fs";
20
20
  import { configFromEnv, startMemoryd } from "@agfpd/iapeer-memory-core";
21
21
  import { authorIndexPath, memoryPaths } from "../paths.js";
22
+ import { guardedWriteFileSync, guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
22
23
 
23
24
  export async function cmdMemoryd(argv: string[]): Promise<number> {
24
25
  let mcpPort: number | undefined;
@@ -56,7 +57,7 @@ export async function cmdMemoryd(argv: string[]): Promise<number> {
56
57
  }
57
58
  // pid file — uninstall's stop handle (best-effort; stale files are
58
59
  // harmless: the reader checks liveness before signalling).
59
- fs.writeFileSync(paths.pidPath, `${process.pid}\n`);
60
+ guardedWriteFileSync(paths.pidPath, `${process.pid}\n`);
60
61
 
61
62
  const freshEditWindowRaw = process.env.IAPEER_MEMORY_FRESH_EDIT_WINDOW_S;
62
63
  const freshEditWindowS =
@@ -101,7 +102,7 @@ export async function cmdMemoryd(argv: string[]): Promise<number> {
101
102
  .close()
102
103
  .then(() => {
103
104
  try {
104
- fs.unlinkSync(paths.pidPath);
105
+ guardedUnlinkSync(paths.pidPath);
105
106
  } catch {
106
107
  // best effort
107
108
  }
@@ -14,6 +14,7 @@ import {
14
14
  isLocaleId,
15
15
  prepareSqliteRuntime,
16
16
  } from "@agfpd/iapeer-memory-core";
17
+ import type { Egress } from "../egress.js";
17
18
  import { memoryPaths } from "../paths.js";
18
19
  import { readSlot } from "../slot.js";
19
20
  import { packageVersion } from "../version.js";
@@ -50,10 +51,13 @@ export function searchPipelineLine(env: Record<string, string | undefined>): str
50
51
 
51
52
  /** Live pipeline from the running memoryd — the same per-component statuses
52
53
  * every vault_search returns. Null when memoryd is unreachable. */
53
- export async function probeSearchPipeline(port: number): Promise<string | null> {
54
+ export async function probeSearchPipeline(
55
+ egress: Egress,
56
+ port: number,
57
+ ): Promise<string | null> {
54
58
  ensureLoopbackNotProxied(); // fleet-class: proxy-env lies about live loopback ports
55
59
  try {
56
- const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
60
+ const res = await egress.fetch(`http://127.0.0.1:${port}/mcp`, {
57
61
  method: "POST",
58
62
  headers: {
59
63
  "content-type": "application/json",
@@ -85,10 +89,13 @@ export async function probeSearchPipeline(port: number): Promise<string | null>
85
89
  }
86
90
  }
87
91
 
88
- async function probeMcp(port: number): Promise<{ line: string; alive: boolean }> {
92
+ async function probeMcp(
93
+ egress: Egress,
94
+ port: number,
95
+ ): Promise<{ line: string; alive: boolean }> {
89
96
  ensureLoopbackNotProxied(); // fleet-class: proxy-env lies about live loopback ports
90
97
  try {
91
- const res = await fetch(`http://127.0.0.1:${port}/mcp`, {
98
+ const res = await egress.fetch(`http://127.0.0.1:${port}/mcp`, {
92
99
  method: "POST",
93
100
  headers: { "content-type": "application/json" },
94
101
  body: "{}",
@@ -102,7 +109,7 @@ async function probeMcp(port: number): Promise<{ line: string; alive: boolean }>
102
109
  }
103
110
  }
104
111
 
105
- export async function cmdStatus(argv: string[]): Promise<number> {
112
+ export async function cmdStatus(argv: string[], egress: Egress): Promise<number> {
106
113
  if (argv.length) {
107
114
  console.error(`iapeer-memory status: unknown flag: ${argv[0]}`);
108
115
  return 2;
@@ -111,7 +118,7 @@ export async function cmdStatus(argv: string[]): Promise<number> {
111
118
  const version = packageVersion();
112
119
  console.log(`iapeer-memory v${version}`);
113
120
 
114
- const results = runVerify({ repair: false });
121
+ const results = runVerify(egress, { repair: false });
115
122
  const width = Math.max(...results.map((r) => r.name.length), 12);
116
123
  for (const r of results) {
117
124
  const mark =
@@ -128,11 +135,11 @@ export async function cmdStatus(argv: string[]): Promise<number> {
128
135
  );
129
136
 
130
137
  const port = Number(process.env.IAPEER_MEMORY_MCP_PORT || "") || 8766;
131
- const mcp = await probeMcp(port);
138
+ const mcp = await probeMcp(egress, port);
132
139
  console.log(` ${"mcp-endpoint".padEnd(width)} ${mcp.line}`);
133
140
  // The live pipeline is only probed when the endpoint is alive — a dead
134
141
  // port already told us everything (and the static view says the rest).
135
- const livePipeline = mcp.alive ? await probeSearchPipeline(port) : null;
142
+ const livePipeline = mcp.alive ? await probeSearchPipeline(egress, port) : null;
136
143
  console.log(
137
144
  ` ${"search".padEnd(width)} ` +
138
145
  (livePipeline ?? `${searchPipelineLine(process.env)} (memoryd down — static view)`),
@@ -17,12 +17,14 @@
17
17
  */
18
18
 
19
19
  import fs from "node:fs";
20
+ import type { Egress } from "../egress.js";
20
21
  import { memoryPaths } from "../paths.js";
21
22
  import { removeBinary } from "../binary.js";
22
23
  import { readFleetMap } from "../fleet.js";
23
24
  import { applyMemoryPlugin, readSlot, removeSlot, SLOT_PROVIDER } from "../slot.js";
24
25
  import { withProvisionLock } from "../surfaces/lock.js";
25
26
  import { sweepUnprovision } from "../surfaces/sweep.js";
27
+ import { guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
26
28
  import {
27
29
  DREAM_TRIGGER_ID,
28
30
  SWEEP_TRIGGER_ID,
@@ -38,18 +40,12 @@ import {
38
40
  * the command closes the "signal a stranger" class. Probe failure → false
39
41
  * (never signal on uncertainty).
40
42
  */
41
- export function pidLooksLikeOurs(pid: number): boolean {
42
- try {
43
- const proc = Bun.spawnSync(["ps", "-o", "command=", "-p", String(pid)], {
44
- stdout: "pipe",
45
- stderr: "pipe",
46
- });
47
- if (proc.exitCode !== 0) return false;
48
- const command = proc.stdout.toString().trim();
49
- return command.includes("memoryd");
50
- } catch {
51
- return false;
52
- }
43
+ export function pidLooksLikeOurs(egress: Egress, pid: number): boolean {
44
+ // `ps` probe — egress allowance 3 (read-only lookup FEEDING the verified
45
+ // kill; refusing it would break the guard itself). Never throws.
46
+ const proc = egress.spawnSync(["ps", "-o", "command=", "-p", String(pid)]);
47
+ if (proc.spawnError || proc.exitCode !== 0) return false;
48
+ return proc.stdout.trim().includes("memoryd");
53
49
  }
54
50
 
55
51
  /**
@@ -60,30 +56,27 @@ export function pidLooksLikeOurs(pid: number): boolean {
60
56
  * by uninstall (stop) and update (managed restart: SIGTERM → the notifier
61
57
  * watcher relaunches via the launcher with the fresh binary, ADR-010).
62
58
  */
63
- export function stopMemorydByPidFile(pidPath: string): string {
59
+ export function stopMemorydByPidFile(egress: Egress, pidPath: string): string {
64
60
  let line = "not running (no pid file)";
65
61
  try {
66
62
  const pid = Number(fs.readFileSync(pidPath, "utf-8").trim());
67
63
  if (Number.isInteger(pid) && pid > 1) {
68
- if (!pidLooksLikeOurs(pid)) {
64
+ if (!pidLooksLikeOurs(egress, pid)) {
69
65
  line = `pid file points at a non-memoryd process (${pid}) — NOT signalling; stale file removed`;
70
66
  } else {
71
- try {
72
- process.kill(pid, "SIGTERM");
73
- line = `SIGTERM sent to pid ${pid} (command verified)`;
74
- } catch {
75
- line = `stale pid file (process ${pid} gone) — removed`;
76
- }
67
+ line = egress.kill(pid, "SIGTERM").delivered
68
+ ? `SIGTERM sent to pid ${pid} (command verified)`
69
+ : `stale pid file (process ${pid} gone) — removed`;
77
70
  }
78
71
  }
79
- fs.unlinkSync(pidPath);
72
+ guardedUnlinkSync(pidPath);
80
73
  } catch {
81
74
  // no pid file — nothing to stop
82
75
  }
83
76
  return line;
84
77
  }
85
78
 
86
- export function cmdUninstall(argv: string[]): number {
79
+ export function cmdUninstall(argv: string[], egress: Egress): number {
87
80
  let keepBinary = false;
88
81
  let iapeerBin = "iapeer";
89
82
  for (let i = 0; i < argv.length; i++) {
@@ -141,7 +134,7 @@ export function cmdUninstall(argv: string[]): number {
141
134
  // session plugin off via the core verb WHILE the declaration is alive
142
135
  // (it derives the identity from it; agreed order, auto-removal).
143
136
  if (declared.plugin) {
144
- const off = applyMemoryPlugin({ mode: "off", iapeerBin });
137
+ const off = applyMemoryPlugin(egress, { mode: "off", iapeerBin });
145
138
  console.log(
146
139
  `plugin : ${
147
140
  off.suppressed
@@ -164,7 +157,7 @@ export function cmdUninstall(argv: string[]): number {
164
157
 
165
158
  // notifier wiring: best-effort unregister of all three triggers (not-found
166
159
  // is soft on the notifier side; teaching replies go to the index session).
167
- const unreg = unregisterWatcher({ iapeerBin });
160
+ const unreg = unregisterWatcher(egress, { iapeerBin });
168
161
  console.log(
169
162
  `watcher : ${
170
163
  unreg.ok
@@ -173,7 +166,7 @@ export function cmdUninstall(argv: string[]): number {
173
166
  }`,
174
167
  );
175
168
  for (const id of [SWEEP_TRIGGER_ID, DREAM_TRIGGER_ID]) {
176
- const t = unregisterTimer({ id, iapeerBin });
169
+ const t = unregisterTimer(egress, { id, iapeerBin });
177
170
  console.log(
178
171
  `timer : ${
179
172
  t.ok
@@ -183,7 +176,7 @@ export function cmdUninstall(argv: string[]): number {
183
176
  );
184
177
  }
185
178
 
186
- console.log(`memoryd : ${stopMemorydByPidFile(paths.pidPath)}`);
179
+ console.log(`memoryd : ${stopMemorydByPidFile(egress, paths.pidPath)}`);
187
180
 
188
181
  if (keepBinary) {
189
182
  console.log(`binary : kept (${paths.binaryPath})`);