sdlc-agent-4-enterprise-server 1.55.3 → 1.55.8

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.
Files changed (31) hide show
  1. package/README.md +1 -1
  2. package/dist/config/ContentProxy.d.ts +22 -1
  3. package/dist/config/ContentProxy.js +47 -1
  4. package/dist/config/ContentProxy.js.map +1 -1
  5. package/dist/modules/orchestration/artifact-cleanup/CleanupSweep.d.ts +60 -0
  6. package/dist/modules/orchestration/artifact-cleanup/CleanupSweep.js +159 -0
  7. package/dist/modules/orchestration/artifact-cleanup/CleanupSweep.js.map +1 -0
  8. package/dist/modules/orchestration/artifact-cleanup/artifact-roots.d.ts +14 -0
  9. package/dist/modules/orchestration/artifact-cleanup/artifact-roots.js +38 -0
  10. package/dist/modules/orchestration/artifact-cleanup/artifact-roots.js.map +1 -0
  11. package/dist/modules/orchestration/artifact-cleanup/cleanup-config.d.ts +23 -0
  12. package/dist/modules/orchestration/artifact-cleanup/cleanup-config.js +35 -0
  13. package/dist/modules/orchestration/artifact-cleanup/cleanup-config.js.map +1 -0
  14. package/dist/modules/orchestration/artifact-cleanup/delete-artifact.d.ts +21 -0
  15. package/dist/modules/orchestration/artifact-cleanup/delete-artifact.js +42 -0
  16. package/dist/modules/orchestration/artifact-cleanup/delete-artifact.js.map +1 -0
  17. package/dist/modules/orchestration/content-proxy/artifact-source.d.ts +8 -0
  18. package/dist/modules/orchestration/content-proxy/artifact-source.js +52 -7
  19. package/dist/modules/orchestration/content-proxy/artifact-source.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/config/ContentProxy.ts +49 -1
  22. package/src/modules/orchestration/__tests__/McpClientManager.content-proxy.test.ts +6 -1
  23. package/src/modules/orchestration/artifact-cleanup/CleanupSweep.ts +185 -0
  24. package/src/modules/orchestration/artifact-cleanup/__tests__/CleanupSweep.test.ts +199 -0
  25. package/src/modules/orchestration/artifact-cleanup/artifact-roots.ts +46 -0
  26. package/src/modules/orchestration/artifact-cleanup/cleanup-config.ts +48 -0
  27. package/src/modules/orchestration/artifact-cleanup/delete-artifact.ts +46 -0
  28. package/src/modules/orchestration/content-proxy/__tests__/content-proxy.cleanup.test.ts +166 -0
  29. package/src/modules/orchestration/content-proxy/__tests__/content-proxy.test.ts +6 -1
  30. package/src/modules/orchestration/content-proxy/artifact-source.ts +54 -7
  31. package/tests/integration/sandbox.it.test.ts +6 -3
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <img src="https://img.shields.io/badge/version-1.55.3-blue?style=for-the-badge" alt="Version">
9
+ <img src="https://img.shields.io/badge/version-1.55.8-blue?style=for-the-badge" alt="Version">
10
10
  <img src="https://img.shields.io/badge/tools-66+-teal?style=for-the-badge" alt="Tools">
11
11
  <img src="https://img.shields.io/badge/node-%3E%3D18-green?style=for-the-badge" alt="Node">
12
12
  </p>
@@ -30,5 +30,26 @@ export interface ContentProxyOverride {
30
30
  /** Disable the adapter entirely for this tool. */
31
31
  enabled?: boolean;
32
32
  }
33
- /** Empty by default — convention detection covers every current exporter. */
33
+ /**
34
+ * Artifact output roots — the dedicated sinks for orphaned artifact files
35
+ * (SA4E-380 F-01/F-05). Shared single source for OI-14 (immediate cleanup +
36
+ * read confinement) and OI-15 (periodic sweep). The raw OS temp root and the
37
+ * agent workspace are NEVER roots.
38
+ *
39
+ * Resolution order:
40
+ * 1. `ARTIFACT_OUTPUT_ROOTS` env override — `path.delimiter`-separated list
41
+ * (operator trust; see SA-08 for the recommended defense-in-depth checks).
42
+ * 2. Defaults: the dedicated `%TEMP%/sa4e-artifacts` sink, the workspace
43
+ * `tmp/artifacts` directory, and the markdown-exporter child's own
44
+ * artifact home (F-01: without it every local-path read fails confinement).
45
+ */
46
+ export declare function artifactOutputRoots(env?: NodeJS.ProcessEnv): readonly string[];
47
+ /**
48
+ * F-01 containment: is `candidate` (already realpath-canonicalised by the
49
+ * caller) located inside `root`? Compares with `path.relative` — case-folded
50
+ * on win32, cross-device resolves to an absolute path → rejected. Separator
51
+ * boundary is honoured (`<root>-evil` is NOT inside `<root>`).
52
+ */
53
+ export declare function isPathWithinRoot(root: string, candidate: string): boolean;
54
+ /** Per-tool escape hatch for tools that defy the naming/description convention. */
34
55
  export declare const CONTENT_PROXY_OVERRIDES: Record<string, ContentProxyOverride>;
@@ -11,6 +11,8 @@
11
11
  *
12
12
  * Overrides exist only for tools that do not follow the convention.
13
13
  */
14
+ import * as os from 'os';
15
+ import * as path from 'path';
14
16
  /** Tool-name prefixes whose result is a file written by the child server. */
15
17
  export const FILE_TOOL_NAME_PREFIXES = ['export_', 'render_', 'generate_'];
16
18
  /** Description fallback: "Convert ... to PDF/DOCX/…" (target must be a file format). */
@@ -31,6 +33,50 @@ export const CONTENT_PARAM_ALIASES = [
31
33
  export const NON_CONTENT_PARAM = /^(content_base64|.*_base64|file_path|.*_path|path|url|uri|dir|.*_id|id|name|.*_name|key|.*_key|token|secret|password|mime|.*_type|type|format|mode|encoding)$/i;
32
34
  /** Maximum artifact size inlined as base64 (mirrors the extension pre-read cap). */
33
35
  export const MAX_ARTIFACT_BYTES = 25 * 1024 * 1024;
34
- /** Empty by default — convention detection covers every current exporter. */
36
+ /**
37
+ * Artifact output roots — the dedicated sinks for orphaned artifact files
38
+ * (SA4E-380 F-01/F-05). Shared single source for OI-14 (immediate cleanup +
39
+ * read confinement) and OI-15 (periodic sweep). The raw OS temp root and the
40
+ * agent workspace are NEVER roots.
41
+ *
42
+ * Resolution order:
43
+ * 1. `ARTIFACT_OUTPUT_ROOTS` env override — `path.delimiter`-separated list
44
+ * (operator trust; see SA-08 for the recommended defense-in-depth checks).
45
+ * 2. Defaults: the dedicated `%TEMP%/sa4e-artifacts` sink, the workspace
46
+ * `tmp/artifacts` directory, and the markdown-exporter child's own
47
+ * artifact home (F-01: without it every local-path read fails confinement).
48
+ */
49
+ export function artifactOutputRoots(env = process.env) {
50
+ const override = parseArtifactRootsEnv(env.ARTIFACT_OUTPUT_ROOTS);
51
+ if (override)
52
+ return override;
53
+ const workspace = path.resolve(process.cwd());
54
+ const childHome = process.platform === 'win32'
55
+ ? path.join(process.env.LOCALAPPDATA ?? '', 'markdown-exporter-mcp', 'artifacts')
56
+ : path.join(process.env.HOME ?? os.homedir(), '.cache', 'markdown-exporter-mcp', 'artifacts');
57
+ return [
58
+ path.join(os.tmpdir(), 'sa4e-artifacts'),
59
+ path.join(workspace, 'tmp', 'artifacts'),
60
+ path.resolve(childHome),
61
+ ];
62
+ }
63
+ /** SA-08 surface: parse the env override; empty/garbage → fall back to defaults. */
64
+ function parseArtifactRootsEnv(raw) {
65
+ if (!raw)
66
+ return undefined;
67
+ const roots = raw.split(path.delimiter).map((s) => s.trim()).filter(Boolean);
68
+ return roots.length ? roots : undefined;
69
+ }
70
+ /**
71
+ * F-01 containment: is `candidate` (already realpath-canonicalised by the
72
+ * caller) located inside `root`? Compares with `path.relative` — case-folded
73
+ * on win32, cross-device resolves to an absolute path → rejected. Separator
74
+ * boundary is honoured (`<root>-evil` is NOT inside `<root>`).
75
+ */
76
+ export function isPathWithinRoot(root, candidate) {
77
+ const rel = path.relative(path.resolve(root), path.resolve(candidate));
78
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel));
79
+ }
80
+ /** Per-tool escape hatch for tools that defy the naming/description convention. */
35
81
  export const CONTENT_PROXY_OVERRIDES = {};
36
82
  //# sourceMappingURL=ContentProxy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ContentProxy.js","sourceRoot":"","sources":["../../src/config/ContentProxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,uBAAuB,GAAsB,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AAE9F,wFAAwF;AACxF,MAAM,CAAC,MAAM,uBAAuB,GAAG,gEAAgE,CAAC;AAExG,qEAAqE;AACrE,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,UAAU;IACV,kBAAkB;IAClB,SAAS;IACT,MAAM;IACN,KAAK;IACL,MAAM;IACN,MAAM;IACN,QAAQ;IACR,UAAU;CACX,CAAC;AAEF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAC5B,gKAAgK,CAAC;AAEnK,oFAAoF;AACpF,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAYnD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,uBAAuB,GAAyC,EAAE,CAAC"}
1
+ {"version":3,"file":"ContentProxy.js","sourceRoot":"","sources":["../../src/config/ContentProxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,6EAA6E;AAC7E,MAAM,CAAC,MAAM,uBAAuB,GAAsB,CAAC,SAAS,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AAE9F,wFAAwF;AACxF,MAAM,CAAC,MAAM,uBAAuB,GAAG,gEAAgE,CAAC;AAExG,qEAAqE;AACrE,MAAM,CAAC,MAAM,qBAAqB,GAAsB;IACtD,UAAU;IACV,kBAAkB;IAClB,SAAS;IACT,MAAM;IACN,KAAK;IACL,MAAM;IACN,MAAM;IACN,QAAQ;IACR,UAAU;CACX,CAAC;AAEF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,iBAAiB,GAC5B,gKAAgK,CAAC;AAEnK,oFAAoF;AACpF,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAYnD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,MAAM,QAAQ,GAAG,qBAAqB,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAClE,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC9C,MAAM,SAAS,GACb,OAAO,CAAC,QAAQ,KAAK,OAAO;QAC1B,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,EAAE,EAAE,uBAAuB,EAAE,WAAW,CAAC;QACjF,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,uBAAuB,EAAE,WAAW,CAAC,CAAC;IAClG,OAAO;QACL,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC;QACxC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,WAAW,CAAC;QACxC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;KACxB,CAAC;AACJ,CAAC;AAED,oFAAoF;AACpF,SAAS,qBAAqB,CAAC,GAAuB;IACpD,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC7E,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,SAAiB;IAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IACvE,OAAO,GAAG,KAAK,EAAE,IAAI,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;AACnG,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,MAAM,uBAAuB,GAAyC,EAAE,CAAC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * CleanupSweep — OI-15 periodic orphan reaper (TDD §9.2, BRD STORY 3 RD-8).
3
+ *
4
+ * Safety net for artifacts that escaped immediate cleanup (OI-14): crashes,
5
+ * power loss, permission errors. Per tick it scans ONLY the dedicated
6
+ * ARTIFACT_OUTPUT_ROOTS (SECURITY-REVIEW approval condition 2 — never the raw
7
+ * OS temp root, never the agent workspace). Roots are dedicated artifact
8
+ * sinks, so EVERY regular non-symlink file older than the TTL is swept —
9
+ * including job-dir nesting (`<root>/<job>/file`, how markdown-exporter
10
+ * writes) and arbitrary names (`STP-v1-SA4E-380.docx`); filename prefixes
11
+ * are NOT a criterion. Directories, symlinks, young files and anything
12
+ * outside the roots are always kept. Job directories emptied by the sweep
13
+ * are removed too. Each run is hard-bounded by maxRunMs and logs one
14
+ * summary line `{ removed, failed, skipped, durationMs }` with basenames only.
15
+ *
16
+ * Lifecycle: started/stopped next to the health monitor (OrchestrationModule);
17
+ * `enabled: false` never starts a timer.
18
+ */
19
+ import type { Logger } from 'pino';
20
+ import { type ArtifactCleanupConfig } from './cleanup-config.js';
21
+ export interface SweepResult {
22
+ removed: number;
23
+ failed: number;
24
+ skipped: number;
25
+ durationMs: number;
26
+ /** True when the tick stopped early because maxRunMs elapsed (UT-48). */
27
+ bounded: boolean;
28
+ }
29
+ export interface CleanupSweepDeps {
30
+ /** Overrides loaded on top of the env-derived config (tests, ops). */
31
+ config?: Partial<ArtifactCleanupConfig>;
32
+ /** Scan roots override (tests); defaults to the configured artifact roots. */
33
+ roots?: readonly string[];
34
+ /** Deletion hook override (tests); defaults to fs.unlinkSync. */
35
+ removeFile?: (filePath: string) => void;
36
+ }
37
+ export declare class CleanupSweep {
38
+ private readonly logger;
39
+ private readonly config;
40
+ private readonly roots;
41
+ private readonly removeFile;
42
+ private timer?;
43
+ constructor(logger: Logger, deps?: CleanupSweepDeps);
44
+ /** Start the interval timer; no-op when disabled or already running. */
45
+ start(): void;
46
+ /** Stop the timer (idempotent). */
47
+ stop(): void;
48
+ /** Run one bounded sweep tick across all configured roots. */
49
+ runOnce(): SweepResult;
50
+ /** @returns false when the maxRunMs deadline was reached mid-scan. */
51
+ private scanRoot;
52
+ /** @returns false when the maxRunMs deadline was reached mid-scan. */
53
+ private scanJobDir;
54
+ private removeDirIfEmpty;
55
+ private sweepEntry;
56
+ /** Inside dedicated roots every regular non-symlink file qualifies — the
57
+ * confinement boundary is the safety criterion, not the file name. */
58
+ private isCandidate;
59
+ private runTick;
60
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * CleanupSweep — OI-15 periodic orphan reaper (TDD §9.2, BRD STORY 3 RD-8).
3
+ *
4
+ * Safety net for artifacts that escaped immediate cleanup (OI-14): crashes,
5
+ * power loss, permission errors. Per tick it scans ONLY the dedicated
6
+ * ARTIFACT_OUTPUT_ROOTS (SECURITY-REVIEW approval condition 2 — never the raw
7
+ * OS temp root, never the agent workspace). Roots are dedicated artifact
8
+ * sinks, so EVERY regular non-symlink file older than the TTL is swept —
9
+ * including job-dir nesting (`<root>/<job>/file`, how markdown-exporter
10
+ * writes) and arbitrary names (`STP-v1-SA4E-380.docx`); filename prefixes
11
+ * are NOT a criterion. Directories, symlinks, young files and anything
12
+ * outside the roots are always kept. Job directories emptied by the sweep
13
+ * are removed too. Each run is hard-bounded by maxRunMs and logs one
14
+ * summary line `{ removed, failed, skipped, durationMs }` with basenames only.
15
+ *
16
+ * Lifecycle: started/stopped next to the health monitor (OrchestrationModule);
17
+ * `enabled: false` never starts a timer.
18
+ */
19
+ import * as fs from 'fs';
20
+ import * as path from 'path';
21
+ import { artifactOutputRoots } from '../../../config/ContentProxy.js';
22
+ import { loadArtifactCleanupConfig } from './cleanup-config.js';
23
+ export class CleanupSweep {
24
+ logger;
25
+ config;
26
+ roots;
27
+ removeFile;
28
+ timer;
29
+ constructor(logger, deps = {}) {
30
+ this.logger = logger.child({ component: 'CleanupSweep' });
31
+ this.config = { ...loadArtifactCleanupConfig(), ...deps.config };
32
+ this.roots = deps.roots ?? artifactOutputRoots();
33
+ this.removeFile = deps.removeFile ?? ((filePath) => fs.unlinkSync(filePath));
34
+ }
35
+ /** Start the interval timer; no-op when disabled or already running. */
36
+ start() {
37
+ if (!this.config.enabled || this.timer)
38
+ return;
39
+ this.timer = setInterval(() => this.runTick(), this.config.intervalMs);
40
+ this.timer.unref?.();
41
+ this.logger.info({ intervalMs: this.config.intervalMs, ttlMs: this.config.ttlMs, roots: this.roots.length }, 'Artifact cleanup sweep started');
42
+ }
43
+ /** Stop the timer (idempotent). */
44
+ stop() {
45
+ if (!this.timer)
46
+ return;
47
+ clearInterval(this.timer);
48
+ this.timer = undefined;
49
+ this.logger.info('Artifact cleanup sweep stopped');
50
+ }
51
+ /** Run one bounded sweep tick across all configured roots. */
52
+ runOnce() {
53
+ const startedAt = Date.now();
54
+ const deadline = startedAt + this.config.maxRunMs;
55
+ const now = Date.now();
56
+ const result = { removed: 0, failed: 0, skipped: 0, durationMs: 0, bounded: false };
57
+ for (const root of this.roots) {
58
+ if (!this.scanRoot(root, now, deadline, result)) {
59
+ result.bounded = true;
60
+ break;
61
+ }
62
+ }
63
+ result.durationMs = Date.now() - startedAt;
64
+ this.logger.info({ removed: result.removed, failed: result.failed, skipped: result.skipped, durationMs: result.durationMs }, 'Artifact cleanup sweep tick completed');
65
+ if (result.bounded)
66
+ this.logger.warn({ maxRunMs: this.config.maxRunMs }, 'Artifact cleanup sweep stopped at maxRunMs bound');
67
+ return result;
68
+ }
69
+ /** @returns false when the maxRunMs deadline was reached mid-scan. */
70
+ scanRoot(root, now, deadline, result) {
71
+ let entries;
72
+ try {
73
+ entries = fs.readdirSync(root, { withFileTypes: true });
74
+ }
75
+ catch (err) {
76
+ this.logger.debug({ err }, 'Artifact cleanup sweep skipped unreadable root');
77
+ return true;
78
+ }
79
+ for (const entry of entries) {
80
+ if (Date.now() >= deadline)
81
+ return false;
82
+ if (entry.isDirectory() && !entry.isSymbolicLink()) {
83
+ // Child job dirs nest one level: <root>/<job>/<artifact>. Sweep their
84
+ // files, then remove the dir when the sweep emptied it.
85
+ const jobDir = path.join(root, entry.name);
86
+ if (!this.scanJobDir(jobDir, now, deadline, result))
87
+ return false;
88
+ this.removeDirIfEmpty(jobDir, result);
89
+ }
90
+ else {
91
+ this.sweepEntry(root, entry, now, result);
92
+ }
93
+ }
94
+ return true;
95
+ }
96
+ /** @returns false when the maxRunMs deadline was reached mid-scan. */
97
+ scanJobDir(jobDir, now, deadline, result) {
98
+ let entries;
99
+ try {
100
+ entries = fs.readdirSync(jobDir, { withFileTypes: true });
101
+ }
102
+ catch (err) {
103
+ this.logger.debug({ err }, 'Artifact cleanup sweep skipped unreadable job dir');
104
+ return true;
105
+ }
106
+ for (const entry of entries) {
107
+ if (Date.now() >= deadline)
108
+ return false;
109
+ this.sweepEntry(jobDir, entry, now, result);
110
+ }
111
+ return true;
112
+ }
113
+ removeDirIfEmpty(jobDir, result) {
114
+ try {
115
+ const remaining = fs.readdirSync(jobDir);
116
+ if (remaining.length === 0) {
117
+ fs.rmdirSync(jobDir);
118
+ result.removed += 1;
119
+ }
120
+ }
121
+ catch {
122
+ // Non-empty or locked job dir stays — the next tick re-evaluates.
123
+ }
124
+ }
125
+ sweepEntry(dir, entry, now, result) {
126
+ const filePath = path.join(dir, entry.name);
127
+ if (!this.isCandidate(entry)) {
128
+ result.skipped += 1;
129
+ return;
130
+ }
131
+ try {
132
+ const stat = fs.lstatSync(filePath);
133
+ if (now - stat.mtimeMs <= this.config.ttlMs) {
134
+ result.skipped += 1;
135
+ return;
136
+ }
137
+ this.removeFile(filePath);
138
+ result.removed += 1;
139
+ }
140
+ catch (err) {
141
+ result.failed += 1;
142
+ this.logger.warn({ artifact: entry.name, err }, 'Artifact cleanup sweep failed to remove file');
143
+ }
144
+ }
145
+ /** Inside dedicated roots every regular non-symlink file qualifies — the
146
+ * confinement boundary is the safety criterion, not the file name. */
147
+ isCandidate(entry) {
148
+ return entry.isFile() && !entry.isSymbolicLink();
149
+ }
150
+ runTick() {
151
+ try {
152
+ this.runOnce();
153
+ }
154
+ catch (err) {
155
+ this.logger.error({ err }, 'Artifact cleanup sweep tick failed');
156
+ }
157
+ }
158
+ }
159
+ //# sourceMappingURL=CleanupSweep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CleanupSweep.js","sourceRoot":"","sources":["../../../../src/modules/orchestration/artifact-cleanup/CleanupSweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AACtE,OAAO,EAAE,yBAAyB,EAA8B,MAAM,qBAAqB,CAAC;AAoB5F,MAAM,OAAO,YAAY;IACN,MAAM,CAAS;IACf,MAAM,CAAwB;IAC9B,KAAK,CAAoB;IACzB,UAAU,CAA6B;IAChD,KAAK,CAAkB;IAE/B,YAAY,MAAc,EAAE,OAAyB,EAAE;QACrD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,yBAAyB,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACjE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,mBAAmB,EAAE,CAAC;QACjD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,wEAAwE;IACxE,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QAC/C,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACvE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QACrB,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,EAC1F,gCAAgC,CACjC,CAAC;IACJ,CAAC;IAED,mCAAmC;IACnC,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACvB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;IACrD,CAAC;IAED,8DAA8D;IAC9D,OAAO;QACL,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC;QAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAgB,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAEjG,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC9B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;gBAChD,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;gBACtB,MAAM;YACR,CAAC;QACH,CAAC;QAED,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;QAC3C,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,EAC1G,uCAAuC,CACxC,CAAC;QACF,IAAI,MAAM,CAAC,OAAO;YAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,EAAE,kDAAkD,CAAC,CAAC;QAC7H,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,sEAAsE;IAC9D,QAAQ,CAAC,IAAY,EAAE,GAAW,EAAE,QAAgB,EAAE,MAAmB;QAC/E,IAAI,OAAoB,CAAC;QACzB,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,gDAAgD,CAAC,CAAC;YAC7E,OAAO,IAAI,CAAC;QACd,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ;gBAAE,OAAO,KAAK,CAAC;YACzC,IAAI,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;gBACnD,sEAAsE;gBACtE,wDAAwD;gBACxD,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC3C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,CAAC;oBAAE,OAAO,KAAK,CAAC;gBAClE,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACxC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;YAC5C,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sEAAsE;IAC9D,UAAU,CAAC,MAAc,EAAE,GAAW,EAAE,QAAgB,EAAE,MAAmB;QACnF,IAAI,OAAoB,CAAC;QACzB,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,mDAAmD,CAAC,CAAC;YAChF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ;gBAAE,OAAO,KAAK,CAAC;YACzC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,gBAAgB,CAAC,MAAc,EAAE,MAAmB;QAC1D,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YACzC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3B,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBACrB,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,kEAAkE;QACpE,CAAC;IACH,CAAC;IAEO,UAAU,CAAC,GAAW,EAAE,KAAgB,EAAE,GAAW,EAAE,MAAmB;QAChF,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;YACpB,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;YACpC,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;gBAC5C,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;gBACpB,OAAO;YACT,CAAC;YACD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;YAC1B,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;YACnB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,8CAA8C,CAAC,CAAC;QAClG,CAAC;IACH,CAAC;IAED;2EACuE;IAC/D,WAAW,CAAC,KAAgB;QAClC,OAAO,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC;IACnD,CAAC;IAEO,OAAO;QACb,IAAI,CAAC;YACH,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,oCAAoC,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Artifact root helpers shared by OI-14 (immediate cleanup, content proxy) and
3
+ * OI-15 (periodic sweep). SECURITY-REVIEW F-01/F-05: local artifact access is
4
+ * confined to the dedicated ARTIFACT_OUTPUT_ROOTS — a path whose real location
5
+ * (symlinks resolved) falls outside them is never read and never deleted.
6
+ */
7
+ /**
8
+ * F-01 — true only when the candidate's real path (fs.realpathSync, so symlink
9
+ * and `..` tricks are resolved away) sits inside one of the artifact roots.
10
+ * A missing path (ENOENT) or unreadable chain is not within the roots.
11
+ */
12
+ export declare function isWithinArtifactRoots(candidate: string, roots?: readonly string[]): boolean;
13
+ /** OI-15 — artifact file-name pattern: `export_*`, `render_*`, `generate_*`. */
14
+ export declare function isArtifactFileName(name: string): boolean;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Artifact root helpers shared by OI-14 (immediate cleanup, content proxy) and
3
+ * OI-15 (periodic sweep). SECURITY-REVIEW F-01/F-05: local artifact access is
4
+ * confined to the dedicated ARTIFACT_OUTPUT_ROOTS — a path whose real location
5
+ * (symlinks resolved) falls outside them is never read and never deleted.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { FILE_TOOL_NAME_PREFIXES, artifactOutputRoots, isPathWithinRoot, } from '../../../config/ContentProxy.js';
10
+ /**
11
+ * F-01 — true only when the candidate's real path (fs.realpathSync, so symlink
12
+ * and `..` tricks are resolved away) sits inside one of the artifact roots.
13
+ * A missing path (ENOENT) or unreadable chain is not within the roots.
14
+ */
15
+ export function isWithinArtifactRoots(candidate, roots = artifactOutputRoots()) {
16
+ let real;
17
+ try {
18
+ real = fs.realpathSync(candidate);
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ return roots.some((root) => isPathWithinRoot(resolveRoot(root), real));
24
+ }
25
+ /** OI-15 — artifact file-name pattern: `export_*`, `render_*`, `generate_*`. */
26
+ export function isArtifactFileName(name) {
27
+ const lower = name.toLowerCase();
28
+ return FILE_TOOL_NAME_PREFIXES.some((prefix) => lower.startsWith(prefix));
29
+ }
30
+ function resolveRoot(root) {
31
+ try {
32
+ return fs.realpathSync(root);
33
+ }
34
+ catch {
35
+ return path.resolve(root);
36
+ }
37
+ }
38
+ //# sourceMappingURL=artifact-roots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-roots.js","sourceRoot":"","sources":["../../../../src/modules/orchestration/artifact-cleanup/artifact-roots.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EACL,uBAAuB,EACvB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,iCAAiC,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,SAAiB,EACjB,QAA2B,mBAAmB,EAAE;IAEhD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,OAAO,uBAAuB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;AACH,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * OI-15 — cleanup sweep configuration (TDD §9.2).
3
+ *
4
+ * BRD config keys → environment variables (backend reads config via env, the
5
+ * platform's existing pattern):
6
+ * artifact.cleanup.enabled → ARTIFACT_CLEANUP_ENABLED (default true)
7
+ * artifact.cleanup.intervalMs → ARTIFACT_CLEANUP_INTERVAL_MS (default 300_000)
8
+ * artifact.cleanup.ttlMs → ARTIFACT_CLEANUP_TTL_MS (default 600_000)
9
+ * artifact.cleanup.maxRunMs → ARTIFACT_CLEANUP_MAX_RUN_MS (default 30_000)
10
+ */
11
+ export interface ArtifactCleanupConfig {
12
+ /** Master switch — false never starts the sweep timer. */
13
+ enabled: boolean;
14
+ /** Sweep cadence (timer-based). */
15
+ intervalMs: number;
16
+ /** Minimum file age before a matching artifact counts as orphaned. */
17
+ ttlMs: number;
18
+ /** Hard bound per sweep run; overrun → stop the tick and log. */
19
+ maxRunMs: number;
20
+ }
21
+ export declare const ARTIFACT_CLEANUP_DEFAULTS: Readonly<ArtifactCleanupConfig>;
22
+ /** Load the cleanup config from the given environment (defaults when unset). */
23
+ export declare function loadArtifactCleanupConfig(env?: NodeJS.ProcessEnv): ArtifactCleanupConfig;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * OI-15 — cleanup sweep configuration (TDD §9.2).
3
+ *
4
+ * BRD config keys → environment variables (backend reads config via env, the
5
+ * platform's existing pattern):
6
+ * artifact.cleanup.enabled → ARTIFACT_CLEANUP_ENABLED (default true)
7
+ * artifact.cleanup.intervalMs → ARTIFACT_CLEANUP_INTERVAL_MS (default 300_000)
8
+ * artifact.cleanup.ttlMs → ARTIFACT_CLEANUP_TTL_MS (default 600_000)
9
+ * artifact.cleanup.maxRunMs → ARTIFACT_CLEANUP_MAX_RUN_MS (default 30_000)
10
+ */
11
+ export const ARTIFACT_CLEANUP_DEFAULTS = Object.freeze({
12
+ enabled: true,
13
+ intervalMs: 300_000,
14
+ ttlMs: 600_000,
15
+ maxRunMs: 30_000,
16
+ });
17
+ /** Load the cleanup config from the given environment (defaults when unset). */
18
+ export function loadArtifactCleanupConfig(env = process.env) {
19
+ return {
20
+ enabled: boolEnv(env['ARTIFACT_CLEANUP_ENABLED'], ARTIFACT_CLEANUP_DEFAULTS.enabled),
21
+ intervalMs: intEnv(env['ARTIFACT_CLEANUP_INTERVAL_MS'], ARTIFACT_CLEANUP_DEFAULTS.intervalMs),
22
+ ttlMs: intEnv(env['ARTIFACT_CLEANUP_TTL_MS'], ARTIFACT_CLEANUP_DEFAULTS.ttlMs),
23
+ maxRunMs: intEnv(env['ARTIFACT_CLEANUP_MAX_RUN_MS'], ARTIFACT_CLEANUP_DEFAULTS.maxRunMs),
24
+ };
25
+ }
26
+ function boolEnv(value, fallback) {
27
+ if (value === undefined || value === '')
28
+ return fallback;
29
+ return value === '1' || value.toLowerCase() === 'true';
30
+ }
31
+ function intEnv(value, fallback) {
32
+ const parsed = Number.parseInt(value ?? '', 10);
33
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
34
+ }
35
+ //# sourceMappingURL=cleanup-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup-config.js","sourceRoot":"","sources":["../../../../src/modules/orchestration/artifact-cleanup/cleanup-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAaH,MAAM,CAAC,MAAM,yBAAyB,GAAoC,MAAM,CAAC,MAAM,CAAC;IACtF,OAAO,EAAE,IAAI;IACb,UAAU,EAAE,OAAO;IACnB,KAAK,EAAE,OAAO;IACd,QAAQ,EAAE,MAAM;CACjB,CAAC,CAAC;AAEH,gFAAgF;AAChF,MAAM,UAAU,yBAAyB,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC5E,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,EAAE,yBAAyB,CAAC,OAAO,CAAC;QACpF,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,8BAA8B,CAAC,EAAE,yBAAyB,CAAC,UAAU,CAAC;QAC7F,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,EAAE,yBAAyB,CAAC,KAAK,CAAC;QAC9E,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,6BAA6B,CAAC,EAAE,yBAAyB,CAAC,QAAQ,CAAC;KACzF,CAAC;AACJ,CAAC;AAED,SAAS,OAAO,CAAC,KAAyB,EAAE,QAAiB;IAC3D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IACzD,OAAO,KAAK,KAAK,GAAG,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;AACzD,CAAC;AAED,SAAS,MAAM,CAAC,KAAyB,EAAE,QAAgB;IACzD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAChD,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC;AACnE,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * OI-14 — delete-after-inline for local artifacts (TDD §9.1).
3
+ *
4
+ * Contract: after the backend has fully materialised an artifact's bytes, the
5
+ * source file is deleted. Best-effort with identity verification (F-06):
6
+ * - the file is re-stat'ed and only unlinked when inode/size/mtime still match
7
+ * the pre-read snapshot, so a concurrently replaced file is never deleted;
8
+ * - ENOENT counts as success (already gone);
9
+ * - any other failure is logged as a warning and never fails the tool call —
10
+ * the bytes are already delivered and the periodic sweep (OI-15) retries.
11
+ */
12
+ import * as fs from 'fs';
13
+ /** Minimal pino-shaped logger so this module stays decoupled from wiring. */
14
+ export interface CleanupLog {
15
+ warn(obj: unknown, msg?: string): void;
16
+ }
17
+ /**
18
+ * Delete the artifact at `filePath` after a successful read. `before` is the
19
+ * stat snapshot taken immediately before the read (F-06 identity check).
20
+ */
21
+ export declare function deleteArtifactAfterRead(filePath: string, before: fs.Stats, log?: CleanupLog): void;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * OI-14 — delete-after-inline for local artifacts (TDD §9.1).
3
+ *
4
+ * Contract: after the backend has fully materialised an artifact's bytes, the
5
+ * source file is deleted. Best-effort with identity verification (F-06):
6
+ * - the file is re-stat'ed and only unlinked when inode/size/mtime still match
7
+ * the pre-read snapshot, so a concurrently replaced file is never deleted;
8
+ * - ENOENT counts as success (already gone);
9
+ * - any other failure is logged as a warning and never fails the tool call —
10
+ * the bytes are already delivered and the periodic sweep (OI-15) retries.
11
+ */
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ /**
15
+ * Delete the artifact at `filePath` after a successful read. `before` is the
16
+ * stat snapshot taken immediately before the read (F-06 identity check).
17
+ */
18
+ export function deleteArtifactAfterRead(filePath, before, log) {
19
+ try {
20
+ const after = fs.lstatSync(filePath);
21
+ if (!identityMatches(before, after)) {
22
+ warn(log, { artifact: path.basename(filePath) }, 'Artifact changed during read; skipped cleanup');
23
+ return;
24
+ }
25
+ fs.unlinkSync(filePath);
26
+ }
27
+ catch (err) {
28
+ if (err?.code === 'ENOENT')
29
+ return; // already gone — success per TDD §9.1
30
+ warn(log, { artifact: path.basename(filePath), err }, 'Artifact cleanup failed after inline');
31
+ }
32
+ }
33
+ function identityMatches(before, after) {
34
+ return after.ino === before.ino && after.size === before.size && after.mtimeMs === before.mtimeMs;
35
+ }
36
+ function warn(log, obj, msg) {
37
+ if (log)
38
+ log.warn(obj, msg);
39
+ else
40
+ console.warn(`[artifact-cleanup] ${msg}`, obj);
41
+ }
42
+ //# sourceMappingURL=delete-artifact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delete-artifact.js","sourceRoot":"","sources":["../../../../src/modules/orchestration/artifact-cleanup/delete-artifact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAO7B;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAAgB,EAAE,MAAgB,EAAE,GAAgB;IAC1F,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC;YACpC,IAAI,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,EAAE,+CAA+C,CAAC,CAAC;YAClG,OAAO;QACT,CAAC;QACD,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,IAAI,GAAG,EAAE,IAAI,KAAK,QAAQ;YAAE,OAAO,CAAC,sCAAsC;QAC1E,IAAI,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,EAAE,sCAAsC,CAAC,CAAC;IAChG,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,MAAgB,EAAE,KAAe;IACxD,OAAO,KAAK,CAAC,GAAG,KAAK,MAAM,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO,CAAC;AACpG,CAAC;AAED,SAAS,IAAI,CAAC,GAA2B,EAAE,GAAY,EAAE,GAAW;IAClE,IAAI,GAAG;QAAE,GAAG,CAAC,IAAI,CAAC,GAAa,EAAE,GAAG,CAAC,CAAC;;QACjC,OAAO,CAAC,IAAI,CAAC,sBAAsB,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC;AACtD,CAAC"}
@@ -11,11 +11,19 @@
11
11
  * live on another machine. That case is reported as a tool error by the caller,
12
12
  * never silently downgraded to a path.
13
13
  */
14
+ import * as fs from 'fs';
14
15
  export interface ArtifactDeps {
15
16
  /** Read a resource from the child server (works across hosts). */
16
17
  readResource?: (uri: string) => Promise<Buffer | null>;
17
18
  /** Fetch an http(s) URL. Overridable for tests; defaults to global fetch. */
18
19
  fetchUrl?: (url: string) => Promise<Buffer | null>;
20
+ /**
21
+ * OI-14 local-read cleanup hook. Receives the absolute path and the
22
+ * pre-read stat; the default (no hook) deletes the artifact after its
23
+ * bytes are inlined. Only local reads invoke it — resource/URL/blob
24
+ * resolutions never do (UT-46).
25
+ */
26
+ cleanup?: (filePath: string, stat: fs.Stats) => void;
19
27
  }
20
28
  export interface FetchedArtifact {
21
29
  buffer: Buffer;