@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/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
- * `iapeer list` is READ-ONLY on the host no test fuse needed here
11
- * (the fuse class guards host MUTATIONS); deterministic tests pass a
12
- * fake `iapeerBin` instead.
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 = { personality?: unknown; cwd?: unknown };
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
- export function writeFleetMap(opts: {
27
- fleetMapPath: string;
28
- iapeerBin?: string;
29
- /** Injectable for tests. */
30
- nowIso?: string;
31
- }): FleetMapResult {
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 proc = Bun.spawnSync([bin, "list", "--json"], {
36
- stdout: "pipe",
37
- stderr: "pipe",
38
- });
39
- if (proc.exitCode !== 0) {
40
- return {
41
- action: "failed",
42
- count: 0,
43
- detail:
44
- (proc.stderr.toString().trim() || `iapeer list exited ${proc.exitCode}`).slice(0, 160),
45
- };
46
- }
47
- stdout = proc.stdout.toString();
48
- } catch (err) {
49
- return { action: "failed", count: 0, detail: `${bin} unavailable: ${String(err)}` };
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) => ({ personality: p.personality.trim(), cwd: p.cwd.trim() }));
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
- fs.writeFileSync(tmp, body, "utf-8");
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
- fs.writeFileSync(file, content, "utf-8");
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
- fs.writeFileSync(opts.configFile, defaultConfigContent(opts), "utf-8");
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
- fs.writeFileSync(
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
- const defaultRunner: SigningRunner = (cmd, args) => {
57
- try {
58
- const r = Bun.spawnSync([cmd, ...args], {
59
- stdout: "pipe",
60
- stderr: "pipe",
61
- timeout: 90_000,
62
- });
63
- return {
64
- status: r.exitCode,
65
- stdout: r.stdout.toString(),
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
- fs.rmSync(dir, { recursive: true, force: true });
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 = defaultRunner,
135
+ run: SigningRunner = egressRunner(egress),
139
136
  ): SigningOutcome {
140
- // Both test belts (keychain is HOST-GLOBAL, same class as live sends).
141
- if (
142
- process.env.IAPEER_TEST_SANDBOX === "1" ||
143
- process.env.IAPEER_MEMORY_SUPPRESS_IAP_SEND === "1"
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). The slot file tells the core that
4
- * the three public surfaces (layer-5 fragments / MCP tools / daemon under a
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
- * - `plugin` (v1.1, agreed 10.06 + live in iapeer 0.2.25) = the marketplace
16
- * identity of the session plugin. The core DERIVES installs from this block:
17
- * birth-hook installs for new peers, `iapeer memory-plugin on|off (--peer|
18
- * --all)` is the operator verb (built-in marketplace ensure + stale-cache
19
- * retry). Reference reader: iapeer src/status/index.ts parsePluginBlock
20
- * all three fields required non-empty, anything less = treated as a v1
21
- * declaration (no install). marketplaceRef matches iapeer onboard's
22
- * MARKETPLACE_REF for the distribution default.
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
- export const SLOT_PLUGIN: MemoryProviderPlugin = {
42
- name: "iapeer-memory",
43
- marketplace: "agfpd",
44
- marketplaceRef: "agfpd/agfpd-marketplace",
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.plugin.name === SLOT_PLUGIN.name &&
90
- existing.plugin.marketplace === SLOT_PLUGIN.marketplace &&
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
- plugin: SLOT_PLUGIN,
148
+ ...blocks,
102
149
  };
103
150
  fs.mkdirSync(path.dirname(opts.slotPath), { recursive: true });
104
151
  const tmp = `${opts.slotPath}.tmp`;
105
- fs.writeFileSync(tmp, JSON.stringify(slot, null, 2) + "\n", "utf-8");
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
- fs.unlinkSync(slotPath);
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
- * Hard fuse first (same class as iapSend, incident 10.06): `on --all`
138
- * mutates the HOST fleet — no sandbox env contains it, tests must never
139
- * reach the live core. Both belts honoured.
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(opts: {
142
- mode: "on" | "off";
143
- iapeerBin?: string;
144
- }): MemoryPluginApplyResult {
145
- if (
146
- process.env.IAPEER_MEMORY_SUPPRESS_IAP_SEND === "1" ||
147
- process.env.IAPEER_TEST_SANDBOX === "1"
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
- const bin = opts.iapeerBin ?? "iapeer";
156
- try {
157
- const proc = Bun.spawnSync([bin, "memory-plugin", opts.mode, "--all"], {
158
- stdout: "pipe",
159
- stderr: "pipe",
160
- });
161
- if (proc.exitCode !== 0) {
162
- return {
163
- ok: false,
164
- detail:
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
  }