@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 +1 -1
- package/src/fs-guard.ts +76 -1
- package/src/index.ts +2 -0
- package/src/memoryd.ts +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer-memory-core",
|
|
3
|
-
"version": "0.2.
|
|
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
|
-
/**
|
|
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
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;
|