@remnic/core 9.3.518 → 9.3.520

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/dist/schemas.d.ts CHANGED
@@ -7,13 +7,13 @@ declare const MemoryActionEligibilityContextSchema: z.ZodObject<{
7
7
  importance: z.ZodNumber;
8
8
  source: z.ZodEnum<["extraction", "consolidation", "replay", "manual", "unknown"]>;
9
9
  }, "strict", z.ZodTypeAny, {
10
- source: "manual" | "extraction" | "consolidation" | "replay" | "unknown";
11
10
  confidence: number;
11
+ source: "unknown" | "manual" | "extraction" | "consolidation" | "replay";
12
12
  lifecycleState: "active" | "archived" | "candidate" | "validated" | "stale";
13
13
  importance: number;
14
14
  }, {
15
- source: "manual" | "extraction" | "consolidation" | "replay" | "unknown";
16
15
  confidence: number;
16
+ source: "unknown" | "manual" | "extraction" | "consolidation" | "replay";
17
17
  lifecycleState: "active" | "archived" | "candidate" | "validated" | "stale";
18
18
  importance: number;
19
19
  }>;
@@ -149,10 +149,10 @@ declare const ExtractedFactSchema: z.ZodEffects<z.ZodObject<{
149
149
  observed_outcome?: string | null | undefined;
150
150
  }>>>;
151
151
  }, "strip", z.ZodTypeAny, {
152
- tags: string[];
153
- content: string;
154
152
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
153
+ content: string;
155
154
  confidence: number;
155
+ tags: string[];
156
156
  entityRef?: string | null | undefined;
157
157
  structuredAttributes?: Record<string, string> | null | undefined;
158
158
  promptedByQuestion?: string | null | undefined;
@@ -178,10 +178,10 @@ declare const ExtractedFactSchema: z.ZodEffects<z.ZodObject<{
178
178
  observed_outcome?: string | null | undefined;
179
179
  } | null | undefined;
180
180
  }, {
181
- tags: string[];
182
- content: string;
183
181
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
182
+ content: string;
184
183
  confidence: number;
184
+ tags: string[];
185
185
  entityRef?: string | null | undefined;
186
186
  structuredAttributes?: Record<string, string> | null | undefined;
187
187
  promptedByQuestion?: string | null | undefined;
@@ -207,10 +207,10 @@ declare const ExtractedFactSchema: z.ZodEffects<z.ZodObject<{
207
207
  observed_outcome?: string | null | undefined;
208
208
  } | null | undefined;
209
209
  }>, {
210
- tags: string[];
211
- content: string;
212
210
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
211
+ content: string;
213
212
  confidence: number;
213
+ tags: string[];
214
214
  entityRef?: string | null | undefined;
215
215
  structuredAttributes?: Record<string, string> | null | undefined;
216
216
  promptedByQuestion?: string | null | undefined;
@@ -236,10 +236,10 @@ declare const ExtractedFactSchema: z.ZodEffects<z.ZodObject<{
236
236
  observed_outcome?: string | null | undefined;
237
237
  } | null | undefined;
238
238
  }, {
239
- tags: string[];
240
- content: string;
241
239
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
240
+ content: string;
242
241
  confidence: number;
242
+ tags: string[];
243
243
  entityRef?: string | null | undefined;
244
244
  structuredAttributes?: Record<string, string> | null | undefined;
245
245
  promptedByQuestion?: string | null | undefined;
@@ -457,10 +457,10 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
457
457
  observed_outcome?: string | null | undefined;
458
458
  }>>>;
459
459
  }, "strip", z.ZodTypeAny, {
460
- tags: string[];
461
- content: string;
462
460
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
461
+ content: string;
463
462
  confidence: number;
463
+ tags: string[];
464
464
  entityRef?: string | null | undefined;
465
465
  structuredAttributes?: Record<string, string> | null | undefined;
466
466
  promptedByQuestion?: string | null | undefined;
@@ -486,10 +486,10 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
486
486
  observed_outcome?: string | null | undefined;
487
487
  } | null | undefined;
488
488
  }, {
489
- tags: string[];
490
- content: string;
491
489
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
490
+ content: string;
492
491
  confidence: number;
492
+ tags: string[];
493
493
  entityRef?: string | null | undefined;
494
494
  structuredAttributes?: Record<string, string> | null | undefined;
495
495
  promptedByQuestion?: string | null | undefined;
@@ -515,10 +515,10 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
515
515
  observed_outcome?: string | null | undefined;
516
516
  } | null | undefined;
517
517
  }>, {
518
- tags: string[];
519
- content: string;
520
518
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
519
+ content: string;
521
520
  confidence: number;
521
+ tags: string[];
522
522
  entityRef?: string | null | undefined;
523
523
  structuredAttributes?: Record<string, string> | null | undefined;
524
524
  promptedByQuestion?: string | null | undefined;
@@ -544,10 +544,10 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
544
544
  observed_outcome?: string | null | undefined;
545
545
  } | null | undefined;
546
546
  }, {
547
- tags: string[];
548
- content: string;
549
547
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
548
+ content: string;
550
549
  confidence: number;
550
+ tags: string[];
551
551
  entityRef?: string | null | undefined;
552
552
  structuredAttributes?: Record<string, string> | null | undefined;
553
553
  promptedByQuestion?: string | null | undefined;
@@ -631,10 +631,10 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
631
631
  }>, "many">>>;
632
632
  }, "strip", z.ZodTypeAny, {
633
633
  facts: {
634
- tags: string[];
635
- content: string;
636
634
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
635
+ content: string;
637
636
  confidence: number;
637
+ tags: string[];
638
638
  entityRef?: string | null | undefined;
639
639
  structuredAttributes?: Record<string, string> | null | undefined;
640
640
  promptedByQuestion?: string | null | undefined;
@@ -680,10 +680,10 @@ declare const ProactiveExtractionResultSchema: z.ZodObject<{
680
680
  }[] | null | undefined;
681
681
  }, {
682
682
  facts: {
683
- tags: string[];
684
- content: string;
685
683
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
684
+ content: string;
686
685
  confidence: number;
686
+ tags: string[];
687
687
  entityRef?: string | null | undefined;
688
688
  structuredAttributes?: Record<string, string> | null | undefined;
689
689
  promptedByQuestion?: string | null | undefined;
@@ -825,10 +825,10 @@ declare const ExtractionResultSchema: z.ZodObject<{
825
825
  observed_outcome?: string | null | undefined;
826
826
  }>>>;
827
827
  }, "strip", z.ZodTypeAny, {
828
- tags: string[];
829
- content: string;
830
828
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
829
+ content: string;
831
830
  confidence: number;
831
+ tags: string[];
832
832
  entityRef?: string | null | undefined;
833
833
  structuredAttributes?: Record<string, string> | null | undefined;
834
834
  promptedByQuestion?: string | null | undefined;
@@ -854,10 +854,10 @@ declare const ExtractionResultSchema: z.ZodObject<{
854
854
  observed_outcome?: string | null | undefined;
855
855
  } | null | undefined;
856
856
  }, {
857
- tags: string[];
858
- content: string;
859
857
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
858
+ content: string;
860
859
  confidence: number;
860
+ tags: string[];
861
861
  entityRef?: string | null | undefined;
862
862
  structuredAttributes?: Record<string, string> | null | undefined;
863
863
  promptedByQuestion?: string | null | undefined;
@@ -883,10 +883,10 @@ declare const ExtractionResultSchema: z.ZodObject<{
883
883
  observed_outcome?: string | null | undefined;
884
884
  } | null | undefined;
885
885
  }>, {
886
- tags: string[];
887
- content: string;
888
886
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
887
+ content: string;
889
888
  confidence: number;
889
+ tags: string[];
890
890
  entityRef?: string | null | undefined;
891
891
  structuredAttributes?: Record<string, string> | null | undefined;
892
892
  promptedByQuestion?: string | null | undefined;
@@ -912,10 +912,10 @@ declare const ExtractionResultSchema: z.ZodObject<{
912
912
  observed_outcome?: string | null | undefined;
913
913
  } | null | undefined;
914
914
  }, {
915
- tags: string[];
916
- content: string;
917
915
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
916
+ content: string;
918
917
  confidence: number;
918
+ tags: string[];
919
919
  entityRef?: string | null | undefined;
920
920
  structuredAttributes?: Record<string, string> | null | undefined;
921
921
  promptedByQuestion?: string | null | undefined;
@@ -1013,10 +1013,10 @@ declare const ExtractionResultSchema: z.ZodObject<{
1013
1013
  }>, "many">>>;
1014
1014
  }, "strip", z.ZodTypeAny, {
1015
1015
  facts: {
1016
- tags: string[];
1017
- content: string;
1018
1016
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
1017
+ content: string;
1019
1018
  confidence: number;
1019
+ tags: string[];
1020
1020
  entityRef?: string | null | undefined;
1021
1021
  structuredAttributes?: Record<string, string> | null | undefined;
1022
1022
  promptedByQuestion?: string | null | undefined;
@@ -1068,10 +1068,10 @@ declare const ExtractionResultSchema: z.ZodObject<{
1068
1068
  identityReflection?: string | null | undefined;
1069
1069
  }, {
1070
1070
  facts: {
1071
- tags: string[];
1072
- content: string;
1073
1071
  category: "fact" | "preference" | "correction" | "entity" | "decision" | "relationship" | "principle" | "commitment" | "moment" | "skill" | "rule" | "procedure" | "reasoning_trace";
1072
+ content: string;
1074
1073
  confidence: number;
1074
+ tags: string[];
1075
1075
  entityRef?: string | null | undefined;
1076
1076
  structuredAttributes?: Record<string, string> | null | undefined;
1077
1077
  promptedByQuestion?: string | null | undefined;
@@ -1285,13 +1285,13 @@ declare const ContradictionVerificationSchema: z.ZodObject<{
1285
1285
  reasoning: z.ZodString;
1286
1286
  whichIsNewer: z.ZodEnum<["first", "second", "unclear"]>;
1287
1287
  }, "strip", z.ZodTypeAny, {
1288
- reasoning: string;
1289
1288
  confidence: number;
1289
+ reasoning: string;
1290
1290
  isContradiction: boolean;
1291
1291
  whichIsNewer: "first" | "second" | "unclear";
1292
1292
  }, {
1293
- reasoning: string;
1294
1293
  confidence: number;
1294
+ reasoning: string;
1295
1295
  isContradiction: boolean;
1296
1296
  whichIsNewer: "first" | "second" | "unclear";
1297
1297
  }>;
@@ -1386,8 +1386,8 @@ declare const BehaviorLoopAdjustmentSchema: z.ZodObject<{
1386
1386
  reason: z.ZodString;
1387
1387
  appliedAt: z.ZodString;
1388
1388
  }, "strip", z.ZodTypeAny, {
1389
- reason: string;
1390
1389
  confidence: number;
1390
+ reason: string;
1391
1391
  parameter: string;
1392
1392
  previousValue: number;
1393
1393
  nextValue: number;
@@ -1395,8 +1395,8 @@ declare const BehaviorLoopAdjustmentSchema: z.ZodObject<{
1395
1395
  evidenceCount: number;
1396
1396
  appliedAt: string;
1397
1397
  }, {
1398
- reason: string;
1399
1398
  confidence: number;
1399
+ reason: string;
1400
1400
  parameter: string;
1401
1401
  previousValue: number;
1402
1402
  nextValue: number;
@@ -1420,8 +1420,8 @@ declare const BehaviorLoopPolicyStateSchema: z.ZodObject<{
1420
1420
  reason: z.ZodString;
1421
1421
  appliedAt: z.ZodString;
1422
1422
  }, "strip", z.ZodTypeAny, {
1423
- reason: string;
1424
1423
  confidence: number;
1424
+ reason: string;
1425
1425
  parameter: string;
1426
1426
  previousValue: number;
1427
1427
  nextValue: number;
@@ -1429,8 +1429,8 @@ declare const BehaviorLoopPolicyStateSchema: z.ZodObject<{
1429
1429
  evidenceCount: number;
1430
1430
  appliedAt: string;
1431
1431
  }, {
1432
- reason: string;
1433
1432
  confidence: number;
1433
+ reason: string;
1434
1434
  parameter: string;
1435
1435
  previousValue: number;
1436
1436
  nextValue: number;
@@ -1447,8 +1447,8 @@ declare const BehaviorLoopPolicyStateSchema: z.ZodObject<{
1447
1447
  maxDeltaPerCycle: number;
1448
1448
  protectedParams: string[];
1449
1449
  adjustments: {
1450
- reason: string;
1451
1450
  confidence: number;
1451
+ reason: string;
1452
1452
  parameter: string;
1453
1453
  previousValue: number;
1454
1454
  nextValue: number;
@@ -1464,8 +1464,8 @@ declare const BehaviorLoopPolicyStateSchema: z.ZodObject<{
1464
1464
  maxDeltaPerCycle: number;
1465
1465
  protectedParams: string[];
1466
1466
  adjustments: {
1467
- reason: string;
1468
1467
  confidence: number;
1468
+ reason: string;
1469
1469
  parameter: string;
1470
1470
  previousValue: number;
1471
1471
  nextValue: number;
@@ -22,9 +22,9 @@ declare const SharedFeedbackEntrySchema: z.ZodObject<{
22
22
  agent: string;
23
23
  date: string;
24
24
  reason: string;
25
+ confidence?: number | undefined;
25
26
  workflow?: string | undefined;
26
27
  tags?: string[] | undefined;
27
- confidence?: number | undefined;
28
28
  severity?: "low" | "medium" | "high" | undefined;
29
29
  outcome?: string | undefined;
30
30
  refs?: string[] | undefined;
@@ -36,9 +36,9 @@ declare const SharedFeedbackEntrySchema: z.ZodObject<{
36
36
  agent: string;
37
37
  date: string;
38
38
  reason: string;
39
+ confidence?: number | undefined;
39
40
  workflow?: string | undefined;
40
41
  tags?: string[] | undefined;
41
- confidence?: number | undefined;
42
42
  severity?: "low" | "medium" | "high" | undefined;
43
43
  outcome?: string | undefined;
44
44
  refs?: string[] | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remnic/core",
3
- "version": "9.3.518",
3
+ "version": "9.3.520",
4
4
  "description": "Framework-agnostic Remnic memory engine — orchestrator, storage, extraction, search, trust zones",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -6,7 +6,7 @@
6
6
  * so swapping storage providers requires no pipeline changes.
7
7
  */
8
8
 
9
- import fs from "node:fs";
9
+ import type { Stats } from "node:fs";
10
10
  import fsp from "node:fs/promises";
11
11
  import path from "node:path";
12
12
  import type { BinaryStorageBackendConfig } from "./types.js";
@@ -27,6 +27,8 @@ export interface BinaryStorageBackend {
27
27
  exists(remotePath: string): Promise<boolean>;
28
28
  /** Delete a file from the backend. */
29
29
  delete(remotePath: string): Promise<void>;
30
+ /** Return the user-resolvable markdown target for a stored backend path. */
31
+ getRedirectTarget?(remotePath: string): string;
30
32
  }
31
33
 
32
34
  // ---------------------------------------------------------------------------
@@ -45,10 +47,9 @@ export class FilesystemBackend implements BinaryStorageBackend {
45
47
  }
46
48
 
47
49
  private resolveRemotePath(remotePath: string): string {
48
- if (path.isAbsolute(remotePath)) {
49
- throw new Error(`FilesystemBackend remotePath must be relative: ${JSON.stringify(remotePath)}`);
50
- }
51
- const resolved = path.resolve(this.basePath, remotePath);
50
+ const resolved = path.isAbsolute(remotePath)
51
+ ? path.resolve(remotePath)
52
+ : path.resolve(this.basePath, remotePath);
52
53
  const relative = path.relative(this.basePath, resolved);
53
54
  if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
54
55
  throw new Error(`FilesystemBackend remotePath escapes basePath: ${JSON.stringify(remotePath)}`);
@@ -56,26 +57,169 @@ export class FilesystemBackend implements BinaryStorageBackend {
56
57
  return resolved;
57
58
  }
58
59
 
59
- async upload(localPath: string, remotePath: string): Promise<string> {
60
+ private isInsideBase(candidate: string, realBase: string): boolean {
61
+ const relative = path.relative(realBase, candidate);
62
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
63
+ }
64
+
65
+ private async realBasePathIfExists(): Promise<string | null> {
66
+ try {
67
+ const stat = await fsp.lstat(this.basePath);
68
+ if (stat.isSymbolicLink()) {
69
+ throw new Error(`FilesystemBackend basePath must not be a symlink: ${this.basePath}`);
70
+ }
71
+ if (!stat.isDirectory()) {
72
+ throw new Error(`FilesystemBackend basePath must be a directory: ${this.basePath}`);
73
+ }
74
+ return await fsp.realpath(this.basePath);
75
+ } catch (err) {
76
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
77
+ return null;
78
+ }
79
+ throw err;
80
+ }
81
+ }
82
+
83
+ private async ensureBaseDirectory(): Promise<string> {
84
+ await fsp.mkdir(this.basePath, { recursive: true });
85
+ const realBase = await this.realBasePathIfExists();
86
+ if (realBase === null) {
87
+ throw new Error(`FilesystemBackend failed to create basePath: ${this.basePath}`);
88
+ }
89
+ return realBase;
90
+ }
91
+
92
+ private async ensureSafeParentDirectory(dest: string): Promise<string> {
93
+ const realBase = await this.ensureBaseDirectory();
94
+ const destDir = path.dirname(dest);
95
+ const relativeDir = path.relative(this.basePath, destDir);
96
+ const segments = relativeDir === "" ? [] : relativeDir.split(path.sep);
97
+ let current = this.basePath;
98
+
99
+ for (const segment of segments) {
100
+ if (segment === "." || segment === "") continue;
101
+ current = path.join(current, segment);
102
+ try {
103
+ const stat = await fsp.lstat(current);
104
+ if (stat.isSymbolicLink()) {
105
+ throw new Error(`FilesystemBackend remotePath traverses symlink: ${current}`);
106
+ }
107
+ if (!stat.isDirectory()) {
108
+ throw new Error(`FilesystemBackend remotePath parent is not a directory: ${current}`);
109
+ }
110
+ } catch (err) {
111
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
112
+ throw err;
113
+ }
114
+ await fsp.mkdir(current);
115
+ }
116
+ }
117
+
118
+ const realParent = await fsp.realpath(destDir);
119
+ if (!this.isInsideBase(realParent, realBase)) {
120
+ throw new Error(`FilesystemBackend remotePath parent escapes basePath: ${dest}`);
121
+ }
122
+ return realBase;
123
+ }
124
+
125
+ private async resolveExistingRemotePath(remotePath: string): Promise<string | null> {
60
126
  const dest = this.resolveRemotePath(remotePath);
127
+ const realBase = await this.realBasePathIfExists();
128
+ if (realBase === null) {
129
+ return null;
130
+ }
131
+
61
132
  const destDir = path.dirname(dest);
62
- await fsp.mkdir(destDir, { recursive: true });
63
- await fsp.copyFile(localPath, dest);
133
+ const relativeDir = path.relative(this.basePath, destDir);
134
+ const segments = relativeDir === "" ? [] : relativeDir.split(path.sep);
135
+ let current = this.basePath;
136
+ for (const segment of segments) {
137
+ if (segment === "." || segment === "") continue;
138
+ current = path.join(current, segment);
139
+ let stat: Stats;
140
+ try {
141
+ stat = await fsp.lstat(current);
142
+ } catch (err) {
143
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
144
+ return null;
145
+ }
146
+ throw err;
147
+ }
148
+ if (stat.isSymbolicLink()) {
149
+ throw new Error(`FilesystemBackend remotePath traverses symlink: ${current}`);
150
+ }
151
+ if (!stat.isDirectory()) {
152
+ return null;
153
+ }
154
+ }
155
+
156
+ const realParent = await fsp.realpath(destDir).catch((err: NodeJS.ErrnoException) => {
157
+ if (err.code === "ENOENT") return null;
158
+ throw err;
159
+ });
160
+ if (realParent === null) {
161
+ return null;
162
+ }
163
+ if (!this.isInsideBase(realParent, realBase)) {
164
+ throw new Error(`FilesystemBackend remotePath parent escapes basePath: ${JSON.stringify(remotePath)}`);
165
+ }
166
+
167
+ try {
168
+ const stat = await fsp.lstat(dest);
169
+ if (stat.isSymbolicLink()) {
170
+ throw new Error(`FilesystemBackend remotePath points to symlink: ${dest}`);
171
+ }
172
+ if (!stat.isFile()) {
173
+ return null;
174
+ }
175
+ } catch (err) {
176
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
177
+ return null;
178
+ }
179
+ throw err;
180
+ }
181
+
182
+ const realDest = await fsp.realpath(dest);
183
+ if (!this.isInsideBase(realDest, realBase)) {
184
+ throw new Error(`FilesystemBackend remotePath escapes basePath: ${JSON.stringify(remotePath)}`);
185
+ }
64
186
  return dest;
65
187
  }
66
188
 
67
- async exists(remotePath: string): Promise<boolean> {
189
+ async upload(localPath: string, remotePath: string): Promise<string> {
190
+ if (path.isAbsolute(remotePath)) {
191
+ throw new Error(`FilesystemBackend upload remotePath must be relative: ${JSON.stringify(remotePath)}`);
192
+ }
68
193
  const dest = this.resolveRemotePath(remotePath);
194
+ const realBase = await this.ensureSafeParentDirectory(dest);
69
195
  try {
70
- await fsp.access(dest, fs.constants.F_OK);
71
- return true;
72
- } catch {
73
- return false;
196
+ const stat = await fsp.lstat(dest);
197
+ if (stat.isSymbolicLink()) {
198
+ throw new Error(`FilesystemBackend remotePath points to symlink: ${dest}`);
199
+ }
200
+ } catch (err) {
201
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
202
+ throw err;
203
+ }
74
204
  }
205
+ await fsp.copyFile(localPath, dest);
206
+ const realDest = await fsp.realpath(dest);
207
+ if (!this.isInsideBase(realDest, realBase)) {
208
+ throw new Error(`FilesystemBackend remotePath escapes basePath: ${JSON.stringify(remotePath)}`);
209
+ }
210
+ return remotePath;
211
+ }
212
+
213
+ async exists(remotePath: string): Promise<boolean> {
214
+ const dest = await this.resolveExistingRemotePath(remotePath);
215
+ return dest !== null;
75
216
  }
76
217
 
77
218
  async delete(remotePath: string): Promise<void> {
78
- const dest = this.resolveRemotePath(remotePath);
219
+ const dest = await this.resolveExistingRemotePath(remotePath);
220
+ if (dest === null) {
221
+ return;
222
+ }
79
223
  try {
80
224
  await fsp.unlink(dest);
81
225
  } catch (err: unknown) {
@@ -83,6 +227,10 @@ export class FilesystemBackend implements BinaryStorageBackend {
83
227
  if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
84
228
  }
85
229
  }
230
+
231
+ getRedirectTarget(remotePath: string): string {
232
+ return this.resolveRemotePath(remotePath);
233
+ }
86
234
  }
87
235
 
88
236
  // ---------------------------------------------------------------------------
@@ -24,25 +24,37 @@ export function manifestPath(memoryDir: string): string {
24
24
 
25
25
  /**
26
26
  * Read the manifest from disk. Returns a fresh empty manifest if the file
27
- * does not exist or contains invalid JSON (CLAUDE.md #18).
27
+ * does not exist. Existing invalid manifests fail closed so the pipeline does
28
+ * not overwrite state needed for safe cleanup.
28
29
  */
29
30
  export async function readManifest(memoryDir: string): Promise<BinaryLifecycleManifest> {
30
31
  const filePath = manifestPath(memoryDir);
32
+ let raw: string;
31
33
  try {
32
- const raw = await fsp.readFile(filePath, "utf-8");
33
- const parsed: unknown = JSON.parse(raw);
34
- // CLAUDE.md #18: validate the parsed result is a non-null object.
35
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
34
+ raw = await fsp.readFile(filePath, "utf-8");
35
+ } catch (err) {
36
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
36
37
  return emptyManifest();
37
38
  }
38
- const obj = parsed as Record<string, unknown>;
39
- if (obj.version !== 1 || !Array.isArray(obj.assets)) {
40
- return emptyManifest();
41
- }
42
- return parsed as BinaryLifecycleManifest;
43
- } catch {
44
- return emptyManifest();
39
+ throw new Error(`Failed to read binary lifecycle manifest at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
40
+ }
41
+
42
+ let parsed: unknown;
43
+ try {
44
+ parsed = JSON.parse(raw);
45
+ } catch (err) {
46
+ throw new Error(`Invalid binary lifecycle manifest JSON at ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
47
+ }
48
+
49
+ // CLAUDE.md #18: validate the parsed result is a non-null object.
50
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
51
+ throw new Error(`Invalid binary lifecycle manifest shape at ${filePath}: expected object`);
52
+ }
53
+ const obj = parsed as Record<string, unknown>;
54
+ if (obj.version !== 1 || !Array.isArray(obj.assets)) {
55
+ throw new Error(`Invalid binary lifecycle manifest shape at ${filePath}: expected version 1 with assets array`);
45
56
  }
57
+ return parsed as BinaryLifecycleManifest;
46
58
  }
47
59
 
48
60
  /**