@agfpd/iapeer-memory-core 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/package.json +1 -1
- package/src/context-render.ts +3 -2
- package/src/frontmatter-fill.ts +3 -2
- package/src/fs-guard.ts +92 -0
- package/src/index-render.ts +3 -2
- package/src/index.ts +9 -0
- package/src/memoryd.ts +8 -7
- package/src/migrate-auto-memory.ts +5 -4
- package/src/render-doctrine.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer-memory-core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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/context-render.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import fs from "node:fs";
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
import crypto from "node:crypto";
|
|
30
|
+
import { guardedWriteFileSync, guardedUnlinkSync } from "./fs-guard.js";
|
|
30
31
|
|
|
31
32
|
export const FRAGMENT_STEM = "iapeer-memory.md";
|
|
32
33
|
|
|
@@ -140,11 +141,11 @@ export function writeFragmentAtomic(
|
|
|
140
141
|
`.${stem}.${crypto.randomBytes(6).toString("hex")}.tmp`,
|
|
141
142
|
);
|
|
142
143
|
try {
|
|
143
|
-
|
|
144
|
+
guardedWriteFileSync(tmp, text, "utf-8");
|
|
144
145
|
fs.renameSync(tmp, target);
|
|
145
146
|
} catch (err) {
|
|
146
147
|
try {
|
|
147
|
-
if (fs.existsSync(tmp))
|
|
148
|
+
if (fs.existsSync(tmp)) guardedUnlinkSync(tmp);
|
|
148
149
|
} catch {
|
|
149
150
|
// best effort
|
|
150
151
|
}
|
package/src/frontmatter-fill.ts
CHANGED
|
@@ -38,6 +38,7 @@ import path from "node:path";
|
|
|
38
38
|
import crypto from "node:crypto";
|
|
39
39
|
import type { TaxonomyPreset } from "./taxonomy.js";
|
|
40
40
|
import { DEFAULT_CURATOR_SET } from "./taxonomy.js";
|
|
41
|
+
import { guardedWriteFileSync, guardedUnlinkSync } from "./fs-guard.js";
|
|
41
42
|
|
|
42
43
|
const FRONTMATTER_RE = /^---[^\S\n]*\n([\s\S]*?\n)---[^\S\n]*(?:\n|$)/;
|
|
43
44
|
|
|
@@ -417,11 +418,11 @@ export function atomicWrite(filePath: string, content: string): void {
|
|
|
417
418
|
`.fm-${crypto.randomBytes(6).toString("hex")}.tmp`,
|
|
418
419
|
);
|
|
419
420
|
try {
|
|
420
|
-
|
|
421
|
+
guardedWriteFileSync(tmp, content, "utf-8");
|
|
421
422
|
fs.renameSync(tmp, filePath);
|
|
422
423
|
} catch (err) {
|
|
423
424
|
try {
|
|
424
|
-
if (fs.existsSync(tmp))
|
|
425
|
+
if (fs.existsSync(tmp)) guardedUnlinkSync(tmp);
|
|
425
426
|
} catch {
|
|
426
427
|
// best effort
|
|
427
428
|
}
|
package/src/fs-guard.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FS belt — the file-system half of deny-by-default
|
|
3
|
+
* (iapeer-memory docs/_planning/DENY_BY_DEFAULT_DESIGN.md §4 П4, accepted
|
|
4
|
+
* by boris 11.06).
|
|
5
|
+
*
|
|
6
|
+
* Incident class №2 («лестница рассинхронилась»): the db path ladder lived
|
|
7
|
+
* in TWO copies (core config + package paths) and a drift wrote a sandbox
|
|
8
|
+
* SQLite into the PROD `~/.iapeer/cache`. Path conventions cannot be the
|
|
9
|
+
* only belt — so every raw write/unlink/rm in BOTH src trees goes through
|
|
10
|
+
* the wrappers below, and under an armed test-sandbox env they REFUSE any
|
|
11
|
+
* path under a production anchor, no matter what the ladder computed:
|
|
12
|
+
*
|
|
13
|
+
* ~/.iapeer — ecosystem state/cache/config of the live host
|
|
14
|
+
* ~/.claude, ~/.codex — harness config surfaces of the live fleet
|
|
15
|
+
* ~/Library/Mobile Documents — the iCloud root (the live vault lives there)
|
|
16
|
+
*
|
|
17
|
+
* Outside the sandbox env the wrappers are pass-through: live init/update/
|
|
18
|
+
* migrate write exactly where they always did. The grep invariant (И3) pins
|
|
19
|
+
* the funnel: no raw `fs.writeFileSync`/`Bun.write`/`fs.rmSync`/
|
|
20
|
+
* `fs.unlinkSync` outside this file in either src tree.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately NOT guarded in v1: `mkdirSync` (creates empty dirs — no data
|
|
23
|
+
* loss / no content leak; the write that would fill them refuses) and reads.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
|
|
30
|
+
/** Both test belts — the ecosystem-wide var survives generic
|
|
31
|
+
* IAPEER_MEMORY_* env-stripping (incident 10.06 №3). The ONE definition
|
|
32
|
+
* for both packages: the package egress hub imports this. */
|
|
33
|
+
export function sandboxEnvArmed(): boolean {
|
|
34
|
+
return (
|
|
35
|
+
process.env.IAPEER_MEMORY_SUPPRESS_IAP_SEND === "1" ||
|
|
36
|
+
process.env.IAPEER_TEST_SANDBOX === "1"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function prodAnchors(): string[] {
|
|
41
|
+
const home = os.homedir();
|
|
42
|
+
return [
|
|
43
|
+
path.join(home, ".iapeer"),
|
|
44
|
+
path.join(home, ".claude"),
|
|
45
|
+
path.join(home, ".codex"),
|
|
46
|
+
path.join(home, "Library", "Mobile Documents"),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** True when the resolved path sits under a production anchor. Exported for
|
|
51
|
+
* tests: the predicate is checkable without arming any write. */
|
|
52
|
+
export function isUnderProdAnchor(filePath: string): boolean {
|
|
53
|
+
const resolved = path.resolve(filePath);
|
|
54
|
+
return prodAnchors().some(
|
|
55
|
+
(a) => resolved === a || resolved.startsWith(a + path.sep),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Throws under an armed sandbox env when the path targets a prod anchor.
|
|
60
|
+
* The op name makes the refusal teach: WHICH write was stopped. */
|
|
61
|
+
export function assertSandboxWritablePath(filePath: string, op: string): void {
|
|
62
|
+
if (!sandboxEnvArmed()) return;
|
|
63
|
+
if (isUnderProdAnchor(filePath)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`fs-guard: ${op} refused under the test sandbox — "${filePath}" is under a production anchor ` +
|
|
66
|
+
"(~/.iapeer, ~/.claude, ~/.codex or the iCloud root). A test must write " +
|
|
67
|
+
"inside its own tmp root; if the path came from the env ladder, the ladder drifted.",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function guardedWriteFileSync(
|
|
73
|
+
filePath: string,
|
|
74
|
+
data: string | NodeJS.ArrayBufferView,
|
|
75
|
+
options?: Parameters<typeof fs.writeFileSync>[2],
|
|
76
|
+
): void {
|
|
77
|
+
assertSandboxWritablePath(filePath, "write");
|
|
78
|
+
fs.writeFileSync(filePath, data, options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function guardedUnlinkSync(filePath: string): void {
|
|
82
|
+
assertSandboxWritablePath(filePath, "unlink");
|
|
83
|
+
fs.unlinkSync(filePath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function guardedRmSync(
|
|
87
|
+
filePath: string,
|
|
88
|
+
options?: Parameters<typeof fs.rmSync>[1],
|
|
89
|
+
): void {
|
|
90
|
+
assertSandboxWritablePath(filePath, "rm");
|
|
91
|
+
fs.rmSync(filePath, options);
|
|
92
|
+
}
|
package/src/index-render.ts
CHANGED
|
@@ -28,6 +28,7 @@ import path from "node:path";
|
|
|
28
28
|
import crypto from "node:crypto";
|
|
29
29
|
import type { RankingConfig, TaxonomyPreset } from "./taxonomy.js";
|
|
30
30
|
import { statusGroup as taxonomyStatusGroup } from "./taxonomy.js";
|
|
31
|
+
import { guardedWriteFileSync, guardedUnlinkSync } from "./fs-guard.js";
|
|
31
32
|
|
|
32
33
|
const WIKILINK_RE = /\[\[([^\]|#]+)/g;
|
|
33
34
|
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/;
|
|
@@ -821,11 +822,11 @@ export function atomicWrite(filePath: string, content: string): void {
|
|
|
821
822
|
const dir = path.dirname(filePath) || ".";
|
|
822
823
|
const tmp = path.join(dir, `.vault-index-${crypto.randomBytes(6).toString("hex")}.tmp`);
|
|
823
824
|
try {
|
|
824
|
-
|
|
825
|
+
guardedWriteFileSync(tmp, content, "utf-8");
|
|
825
826
|
fs.renameSync(tmp, filePath);
|
|
826
827
|
} catch (err) {
|
|
827
828
|
try {
|
|
828
|
-
if (fs.existsSync(tmp))
|
|
829
|
+
if (fs.existsSync(tmp)) guardedUnlinkSync(tmp);
|
|
829
830
|
} catch {
|
|
830
831
|
// best effort
|
|
831
832
|
}
|
package/src/index.ts
CHANGED
|
@@ -70,3 +70,12 @@ export { prepareSqliteRuntime, type SqliteRuntime } from "./sqlite-loader.js";
|
|
|
70
70
|
|
|
71
71
|
// logging
|
|
72
72
|
export { makeLogger, type Logger } from "./log.js";
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
sandboxEnvArmed,
|
|
76
|
+
isUnderProdAnchor,
|
|
77
|
+
assertSandboxWritablePath,
|
|
78
|
+
guardedWriteFileSync,
|
|
79
|
+
guardedUnlinkSync,
|
|
80
|
+
guardedRmSync,
|
|
81
|
+
} from "./fs-guard.js";
|
package/src/memoryd.ts
CHANGED
|
@@ -58,6 +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
62
|
|
|
62
63
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
63
64
|
|
|
@@ -442,7 +443,7 @@ export function persistBatchState(
|
|
|
442
443
|
): void {
|
|
443
444
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
444
445
|
const tmp = `${filePath}.tmp`;
|
|
445
|
-
|
|
446
|
+
guardedWriteFileSync(
|
|
446
447
|
tmp,
|
|
447
448
|
JSON.stringify({
|
|
448
449
|
inbox: Object.fromEntries(state.inbox),
|
|
@@ -470,7 +471,7 @@ export function loadHashState(filePath: string): Map<string, string> {
|
|
|
470
471
|
export function persistHashState(filePath: string, map: Map<string, string>): void {
|
|
471
472
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
472
473
|
const tmp = `${filePath}.tmp`;
|
|
473
|
-
|
|
474
|
+
guardedWriteFileSync(tmp, JSON.stringify(Object.fromEntries(map)), "utf-8");
|
|
474
475
|
fs.renameSync(tmp, filePath);
|
|
475
476
|
}
|
|
476
477
|
|
|
@@ -729,7 +730,7 @@ export async function startMemoryd(opts: MemorydOptions): Promise<MemorydHandle>
|
|
|
729
730
|
if (decision.action !== "write") return;
|
|
730
731
|
fs.mkdirSync(path.dirname(tagsMirrorPath), { recursive: true });
|
|
731
732
|
const tmp = `${tagsMirrorPath}.tmp`;
|
|
732
|
-
|
|
733
|
+
guardedWriteFileSync(tmp, srcContent!, "utf-8");
|
|
733
734
|
fs.renameSync(tmp, tagsMirrorPath);
|
|
734
735
|
logger.info(`tags mirror updated (${decision.reason})`);
|
|
735
736
|
}
|
|
@@ -776,13 +777,13 @@ export async function startMemoryd(opts: MemorydOptions): Promise<MemorydHandle>
|
|
|
776
777
|
}
|
|
777
778
|
const tmp = `${filePath}.memoryd.tmp`;
|
|
778
779
|
try {
|
|
779
|
-
|
|
780
|
+
guardedWriteFileSync(tmp, decision.newContent, "utf-8");
|
|
780
781
|
fs.renameSync(tmp, filePath);
|
|
781
782
|
lastSeenHashes.set(filePath, decision.recordHash);
|
|
782
783
|
logger.info(`human-edit ${decision.reason}: ${path.relative(config.vaultPath, filePath)}`);
|
|
783
784
|
} catch (err) {
|
|
784
785
|
try {
|
|
785
|
-
|
|
786
|
+
guardedUnlinkSync(tmp);
|
|
786
787
|
} catch {
|
|
787
788
|
// best effort
|
|
788
789
|
}
|
|
@@ -905,7 +906,7 @@ export async function startMemoryd(opts: MemorydOptions): Promise<MemorydHandle>
|
|
|
905
906
|
function touchHeartbeat(): void {
|
|
906
907
|
try {
|
|
907
908
|
fs.mkdirSync(path.dirname(heartbeatPath), { recursive: true });
|
|
908
|
-
|
|
909
|
+
guardedWriteFileSync(heartbeatPath, `${new Date().toISOString()} ${os.hostname()}\n`);
|
|
909
910
|
} catch (err) {
|
|
910
911
|
logger.error(`heartbeat write failed: ${String(err)}`);
|
|
911
912
|
}
|
|
@@ -1020,7 +1021,7 @@ export async function startMemoryd(opts: MemorydOptions): Promise<MemorydHandle>
|
|
|
1020
1021
|
persistQuiet();
|
|
1021
1022
|
if (mcp) await mcp.close();
|
|
1022
1023
|
try {
|
|
1023
|
-
|
|
1024
|
+
guardedUnlinkSync(heartbeatPath);
|
|
1024
1025
|
} catch {
|
|
1025
1026
|
// best effort
|
|
1026
1027
|
}
|
|
@@ -32,6 +32,7 @@ import fs from "node:fs";
|
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import type { TaxonomyPreset } from "./taxonomy.js";
|
|
34
34
|
import { yamlSafeScalar } from "./fm-update.js";
|
|
35
|
+
import { guardedWriteFileSync, guardedUnlinkSync } from "./fs-guard.js";
|
|
35
36
|
|
|
36
37
|
/** Source files that are backed up but never copied into the vault. */
|
|
37
38
|
export const SKIP_FILES: ReadonlySet<string> = new Set(["MEMORY.md"]);
|
|
@@ -217,7 +218,7 @@ export function applyMigration(opts: {
|
|
|
217
218
|
// 2a. Non-md and SKIP_FILES: backup-only, removed from the source.
|
|
218
219
|
if (!name.endsWith(".md") || SKIP_FILES.has(name)) {
|
|
219
220
|
try {
|
|
220
|
-
|
|
221
|
+
guardedUnlinkSync(srcPath);
|
|
221
222
|
} catch (err) {
|
|
222
223
|
errors.push(`${name}: unlink after backup failed — ${String(err)}`);
|
|
223
224
|
}
|
|
@@ -229,7 +230,7 @@ export function applyMigration(opts: {
|
|
|
229
230
|
if (fs.existsSync(targetFile)) {
|
|
230
231
|
skipped.push(name);
|
|
231
232
|
try {
|
|
232
|
-
|
|
233
|
+
guardedUnlinkSync(srcPath);
|
|
233
234
|
} catch (err) {
|
|
234
235
|
errors.push(`${name}: unlink (already migrated) failed — ${String(err)}`);
|
|
235
236
|
}
|
|
@@ -261,7 +262,7 @@ export function applyMigration(opts: {
|
|
|
261
262
|
|
|
262
263
|
try {
|
|
263
264
|
const tmp = `${targetFile}.tmp`;
|
|
264
|
-
|
|
265
|
+
guardedWriteFileSync(tmp, newText, "utf-8");
|
|
265
266
|
fs.renameSync(tmp, targetFile);
|
|
266
267
|
} catch (err) {
|
|
267
268
|
errors.push(`${name}: write failed — ${String(err)}`);
|
|
@@ -269,7 +270,7 @@ export function applyMigration(opts: {
|
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
try {
|
|
272
|
-
|
|
273
|
+
guardedUnlinkSync(srcPath);
|
|
273
274
|
migrated.push(name);
|
|
274
275
|
} catch (err) {
|
|
275
276
|
errors.push(`${name}: written to target but source unlink failed — ${String(err)}`);
|
package/src/render-doctrine.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import fs from "node:fs";
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import crypto from "node:crypto";
|
|
27
|
+
import { guardedWriteFileSync, guardedUnlinkSync } from "./fs-guard.js";
|
|
27
28
|
|
|
28
29
|
/** `<!-- iapeer-memory doctrine v<version> -->` — machine-checkable. */
|
|
29
30
|
export function versionMarker(version: string): string {
|
|
@@ -93,11 +94,11 @@ export function renderDoctrine(opts: {
|
|
|
93
94
|
`.IAPEER.md.${crypto.randomBytes(6).toString("hex")}.tmp`,
|
|
94
95
|
);
|
|
95
96
|
try {
|
|
96
|
-
|
|
97
|
+
guardedWriteFileSync(tmp, rendered, "utf-8");
|
|
97
98
|
fs.renameSync(tmp, target);
|
|
98
99
|
} catch (err) {
|
|
99
100
|
try {
|
|
100
|
-
if (fs.existsSync(tmp))
|
|
101
|
+
if (fs.existsSync(tmp)) guardedUnlinkSync(tmp);
|
|
101
102
|
} catch {
|
|
102
103
|
// best effort
|
|
103
104
|
}
|