@agfpd/iapeer-memory-core 0.2.5 → 0.2.6

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-core",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "iapeer-memory core — host-neutral TypeScript memory primitive: vault schema/taxonomy config, search engine, memoryd, context renderer, role contracts. Consumed by the @agfpd/iapeer-memory facade; version kept in lockstep by its release flow (docs/10-distribution.md).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/fs-guard.ts CHANGED
@@ -21,6 +21,10 @@
21
21
  *
22
22
  * Deliberately NOT guarded in v1: `mkdirSync` (creates empty dirs — no data
23
23
  * loss / no content leak; the write that would fill them refuses) and reads.
24
+ * v2 (boris GO 11.06 evening) narrows both exceptions where they fed the
25
+ * residual lane: the segment rule refuses writes into harness trees outside
26
+ * the sandbox roots, and `sandboxBlocksProdRead` gates the prod reads that
27
+ * NAME live cwds (fleet.json, the provider slot).
24
28
  */
25
29
 
26
30
  import fs from "node:fs";
@@ -56,7 +60,70 @@ export function isUnderProdAnchor(filePath: string): boolean {
56
60
  );
57
61
  }
58
62
 
59
- /** Throws under an armed sandbox env when the path targets a prod anchor.
63
+ /**
64
+ * v2 segment rule (accepted by boris 11.06 evening — residual lane of the
65
+ * incident-№4 class): a `.iapeer`, `.claude` or `.codex` directory ANYWHERE
66
+ * on disk is a session surface of some peer — peer cwds live in
67
+ * `~/Projects/*`, `~/Peers/*`, OUTSIDE every prod anchor (probe 11.06:
68
+ * 24/29 fleet cwds; a fragments/surfaces write into a live tree passed the
69
+ * v1 anchor belt). Under the sandbox, a write that crosses a harness
70
+ * segment must sit inside a sandbox root: tmp, or an EXPLICIT
71
+ * IAPEER_ROOT / IAPEER_MEMORY_*_DIR override (an explicit override IS the
72
+ * authorisation — same semantics as the egress hub's `--iapeer-bin`
73
+ * allowance).
74
+ */
75
+ const HARNESS_SEGMENTS = new Set([".iapeer", ".claude", ".codex"]);
76
+
77
+ function realpathOrSelf(p: string): string {
78
+ try {
79
+ return fs.realpathSync(p);
80
+ } catch {
81
+ return p;
82
+ }
83
+ }
84
+
85
+ /** Roots a sandboxed test may legitimately write harness trees under.
86
+ * `/tmp` is listed besides os.tmpdir(): on macOS os.tmpdir() is the
87
+ * per-process $TMPDIR (/var/folders/…) while fixtures commonly use a
88
+ * literal /tmp; the realpath twins cover the /var → /private/var symlink. */
89
+ function sandboxRoots(): string[] {
90
+ const roots = [os.tmpdir(), realpathOrSelf(os.tmpdir()), "/tmp", realpathOrSelf("/tmp")];
91
+ for (const key of [
92
+ "IAPEER_ROOT",
93
+ "IAPEER_MEMORY_STATE_DIR",
94
+ "IAPEER_MEMORY_CACHE_DIR",
95
+ "IAPEER_MEMORY_LOGS_DIR",
96
+ ]) {
97
+ const v = process.env[key];
98
+ if (v) roots.push(v);
99
+ }
100
+ const cfg = process.env.IAPEER_MEMORY_CONFIG_FILE;
101
+ if (cfg) roots.push(path.dirname(path.resolve(cfg)));
102
+ return roots.map((r) => path.resolve(r));
103
+ }
104
+
105
+ /** True when the path crosses a harness-surface segment OUTSIDE every
106
+ * sandbox root. Exported for tests (predicate-level, no write armed). */
107
+ export function isHarnessTreeOutsideSandbox(filePath: string): boolean {
108
+ const resolved = path.resolve(filePath);
109
+ if (!resolved.split(path.sep).some((seg) => HARNESS_SEGMENTS.has(seg))) return false;
110
+ const roots = sandboxRoots();
111
+ return !roots.some((r) => resolved === r || resolved.startsWith(r + path.sep));
112
+ }
113
+
114
+ /**
115
+ * Read-as-egress parity for prod STATE (the И4 precedent — live config.env
116
+ * skip in cli.ts): data that NAMES live peer cwds (fleet.json, the
117
+ * memory-provider slot) must not flow into a sandboxed process from the
118
+ * prod store — it is the SOURCE feeding the residual write lane above.
119
+ * Callers treat a blocked read as «file absent» and report honestly.
120
+ */
121
+ export function sandboxBlocksProdRead(filePath: string): boolean {
122
+ return sandboxEnvArmed() && isUnderProdAnchor(filePath);
123
+ }
124
+
125
+ /** Throws under an armed sandbox env when the path targets a prod anchor
126
+ * (v1) or a harness tree outside the sandbox roots (v2 segment rule).
60
127
  * The op name makes the refusal teach: WHICH write was stopped. */
61
128
  export function assertSandboxWritablePath(filePath: string, op: string): void {
62
129
  if (!sandboxEnvArmed()) return;
@@ -67,6 +134,14 @@ export function assertSandboxWritablePath(filePath: string, op: string): void {
67
134
  "inside its own tmp root; if the path came from the env ladder, the ladder drifted.",
68
135
  );
69
136
  }
137
+ if (isHarnessTreeOutsideSandbox(filePath)) {
138
+ throw new Error(
139
+ `fs-guard: ${op} refused under the test sandbox — "${filePath}" crosses a .iapeer/.claude/.codex ` +
140
+ "segment OUTSIDE the sandbox roots (tmp, IAPEER_ROOT, IAPEER_MEMORY_*_DIR). Harness surfaces " +
141
+ "of live peers live in arbitrary cwds — a test may only touch such trees inside its own sandbox; " +
142
+ "if the cwd came from fleet.json/the registry, the sandbox read-gate drifted.",
143
+ );
144
+ }
70
145
  }
71
146
 
72
147
  export function guardedWriteFileSync(
package/src/index.ts CHANGED
@@ -74,6 +74,8 @@ export { makeLogger, type Logger } from "./log.js";
74
74
  export {
75
75
  sandboxEnvArmed,
76
76
  isUnderProdAnchor,
77
+ isHarnessTreeOutsideSandbox,
78
+ sandboxBlocksProdRead,
77
79
  assertSandboxWritablePath,
78
80
  guardedWriteFileSync,
79
81
  guardedUnlinkSync,
package/src/memoryd.ts CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  type RenderContext,
59
59
  } from "./index-render.js";
60
60
  import { renderPeerFragment, type FragmentEnv } from "./context-render.js";
61
- import { guardedWriteFileSync, guardedUnlinkSync } from "./fs-guard.js";
61
+ import { guardedWriteFileSync, guardedUnlinkSync, sandboxBlocksProdRead } from "./fs-guard.js";
62
62
  import {
63
63
  isSilentEdit,
64
64
  readStampRecord,
@@ -659,11 +659,23 @@ export async function startMemoryd(opts: MemorydOptions): Promise<MemorydHandle>
659
659
  // ── per-peer fragments (docs/05: свежесть за секунды от FS-изменений) ──
660
660
  const fragments = opts.fragments ?? null;
661
661
  let fleetCache: { mtimeMs: number; peers: FleetPeer[] } | null = null;
662
+ let warnedFleetReadBlocked = false;
662
663
 
663
664
  /** Fail-open fleet map reader with an mtime cache: missing/malformed file
664
665
  * → empty fleet (rendering quietly off), never throws. */
665
666
  function readFleetMap(): FleetPeer[] {
666
667
  if (!fragments) return [];
668
+ // Read-as-egress (И4 parity): the prod fleet map NAMES live cwds — a
669
+ // sandboxed process must not learn them. Empty fleet = rendering off.
670
+ if (sandboxBlocksProdRead(fragments.fleetMapPath)) {
671
+ if (!warnedFleetReadBlocked) {
672
+ warnedFleetReadBlocked = true;
673
+ logger.warn(
674
+ `fragments: live fleet map skipped under the test sandbox (${fragments.fleetMapPath}) — set IAPEER_MEMORY_STATE_DIR/IAPEER_ROOT`,
675
+ );
676
+ }
677
+ return [];
678
+ }
667
679
  try {
668
680
  const st = fs.statSync(fragments.fleetMapPath);
669
681
  if (fleetCache && fleetCache.mtimeMs === st.mtimeMs) return fleetCache.peers;