@stigmer/runner 3.11.1-dev.20260812192248 → 3.12.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.
Files changed (108) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/attachment-resolver.d.ts +11 -0
  3. package/dist/activities/execute-cursor/attachment-resolver.js +8 -0
  4. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  5. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  6. package/dist/activities/execute-cursor/blueprint-resolver.js +0 -1
  7. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  8. package/dist/activities/execute-cursor/index.d.ts +6 -2
  9. package/dist/activities/execute-cursor/index.js +16 -32
  10. package/dist/activities/execute-cursor/index.js.map +1 -1
  11. package/dist/activities/execute-cursor/prompt-builder.d.ts +15 -11
  12. package/dist/activities/execute-cursor/prompt-builder.js +20 -10
  13. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  14. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +34 -8
  15. package/dist/activities/execute-deep-agent/attachment-injector.js +111 -106
  16. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  17. package/dist/activities/execute-deep-agent/index.js +0 -15
  18. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  19. package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -2
  20. package/dist/activities/execute-deep-agent/mcp-gate.js +0 -1
  21. package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -1
  22. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -8
  23. package/dist/activities/execute-deep-agent/prompt-builder.js +25 -8
  24. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  25. package/dist/activities/execute-deep-agent/setup.d.ts +0 -10
  26. package/dist/activities/execute-deep-agent/setup.js +13 -45
  27. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  28. package/dist/config.d.ts +6 -5
  29. package/dist/config.js.map +1 -1
  30. package/dist/middleware/path-normalization.d.ts +15 -2
  31. package/dist/middleware/path-normalization.js +39 -5
  32. package/dist/middleware/path-normalization.js.map +1 -1
  33. package/dist/shared/artifact-storage.d.ts +11 -0
  34. package/dist/shared/artifact-storage.js +2 -0
  35. package/dist/shared/artifact-storage.js.map +1 -1
  36. package/dist/shared/attachment-download-urls.d.ts +62 -0
  37. package/dist/shared/attachment-download-urls.js +79 -0
  38. package/dist/shared/attachment-download-urls.js.map +1 -0
  39. package/dist/shared/channel-attachment.d.ts +1 -1
  40. package/dist/shared/channel-attachment.js +1 -1
  41. package/dist/shared/conversation-attachment.d.ts +2 -3
  42. package/dist/shared/conversation-attachment.js +2 -3
  43. package/dist/shared/conversation-attachment.js.map +1 -1
  44. package/dist/shared/mcp-enabled-tools.d.ts +6 -2
  45. package/dist/shared/mcp-enabled-tools.js +6 -2
  46. package/dist/shared/mcp-enabled-tools.js.map +1 -1
  47. package/dist/shared/mcp-transport-guard.d.ts +2 -2
  48. package/dist/shared/mcp-transport-guard.js +2 -2
  49. package/dist/shared/plan-mode-permissions.d.ts +46 -10
  50. package/dist/shared/plan-mode-permissions.js +56 -12
  51. package/dist/shared/plan-mode-permissions.js.map +1 -1
  52. package/dist/shared/synthesized-attachment.d.ts +4 -4
  53. package/dist/shared/synthesized-attachment.js +4 -4
  54. package/dist/shared/zip-extract.d.ts +24 -6
  55. package/dist/shared/zip-extract.js +31 -90
  56. package/dist/shared/zip-extract.js.map +1 -1
  57. package/dist/shared/zip-structure.d.ts +61 -0
  58. package/dist/shared/zip-structure.js +128 -0
  59. package/dist/shared/zip-structure.js.map +1 -0
  60. package/package.json +2 -2
  61. package/src/__test-utils__/fake-artifact-storage.ts +10 -1
  62. package/src/__test-utils__/zip-fixtures.ts +206 -0
  63. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +63 -3
  64. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +62 -29
  65. package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -42
  66. package/src/activities/execute-cursor/attachment-resolver.ts +24 -0
  67. package/src/activities/execute-cursor/blueprint-resolver.ts +1 -10
  68. package/src/activities/execute-cursor/index.ts +22 -37
  69. package/src/activities/execute-cursor/prompt-builder.ts +38 -22
  70. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +270 -126
  71. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -2
  72. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -1
  73. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -1
  74. package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +2 -7
  75. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +246 -37
  76. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +78 -2
  77. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -1
  78. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +35 -4
  79. package/src/activities/execute-deep-agent/attachment-injector.ts +166 -143
  80. package/src/activities/execute-deep-agent/index.ts +0 -19
  81. package/src/activities/execute-deep-agent/mcp-gate.ts +0 -3
  82. package/src/activities/execute-deep-agent/prompt-builder.ts +38 -16
  83. package/src/activities/execute-deep-agent/setup.ts +13 -68
  84. package/src/config.ts +6 -5
  85. package/src/middleware/__tests__/path-normalization.test.ts +29 -3
  86. package/src/middleware/path-normalization.ts +42 -5
  87. package/src/shared/__tests__/artifact-storage.test.ts +9 -0
  88. package/src/shared/__tests__/attachment-download-urls.test.ts +70 -0
  89. package/src/shared/__tests__/channel-attachment.test.ts +2 -2
  90. package/src/shared/__tests__/plan-mode-permissions.test.ts +58 -0
  91. package/src/shared/__tests__/synthesized-attachment.test.ts +19 -41
  92. package/src/shared/__tests__/zip-extract.test.ts +106 -89
  93. package/src/shared/artifact-storage.ts +11 -0
  94. package/src/shared/attachment-download-urls.ts +98 -0
  95. package/src/shared/channel-attachment.ts +1 -1
  96. package/src/shared/conversation-attachment.ts +2 -3
  97. package/src/shared/mcp-enabled-tools.ts +6 -2
  98. package/src/shared/mcp-transport-guard.ts +2 -2
  99. package/src/shared/plan-mode-permissions.ts +59 -12
  100. package/src/shared/synthesized-attachment.ts +4 -4
  101. package/src/shared/zip-extract.ts +35 -117
  102. package/src/shared/zip-structure.ts +181 -0
  103. package/dist/shared/datastore-attachment.d.ts +0 -99
  104. package/dist/shared/datastore-attachment.js +0 -184
  105. package/dist/shared/datastore-attachment.js.map +0 -1
  106. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +0 -104
  107. package/src/shared/__tests__/datastore-attachment.test.ts +0 -293
  108. package/src/shared/datastore-attachment.ts +0 -209
@@ -1,10 +1,24 @@
1
1
  /**
2
- * Minimal, dependency-free ZIP archive parser.
2
+ * Skill-artifact ZIP extraction: text-decoded entries with non-fatal
3
+ * structural failure.
3
4
  *
4
- * Handles the local file header format, supporting stored (method 0) and
5
- * deflated (method 8) entries. Returns parsed entries as path + content
6
- * pairs consumers bring their own write mechanism (WorkspaceBackend,
7
- * node:fs, etc.).
5
+ * Structural parsing lives in zip-structure.ts (central-directory-based
6
+ * see that module's doc for why local-header walks are never acceptable,
7
+ * issue #450). This module owns the skill-artifact *policy* on top of it:
8
+ *
9
+ * Input without a readable central directory is not given a second
10
+ * chance on purpose: every artifact reaching the runner was validated at
11
+ * push by a central-directory-based reader on both editions (OSS:
12
+ * `safearchive/zip` in stigmer-server's skill storage; cloud:
13
+ * commons-compress `ZipFile` in SkillArtifactExtractor), so a missing
14
+ * EOCD here can only mean a truncated or corrupted download — and the
15
+ * honest result for that is the documented empty return, not a guess.
16
+ * (The attachment injector consumes the same structural layer under the
17
+ * opposite policy — fail-hard — because its input is untrusted.)
18
+ *
19
+ * Handles stored (method 0) and deflated (method 8) entries. Returns
20
+ * parsed entries as path + content pairs — consumers bring their own
21
+ * write mechanism (WorkspaceBackend, node:fs, etc.).
8
22
  *
9
23
  * Extracted from skill-writer.ts so both the deep-agent and Cursor
10
24
  * harnesses can share the same ZIP parsing logic without coupling to
@@ -12,6 +26,7 @@
12
26
  */
13
27
 
14
28
  import { createInflateRaw } from "node:zlib";
29
+ import { EOCD_MIN_SIZE, parseZipStructure, type ZipStructuralEntry } from "./zip-structure.js";
15
30
 
16
31
  // ─── Public API ──────────────────────────────────────────────────────────
17
32
 
@@ -29,18 +44,26 @@ export interface ZipFileEntry {
29
44
  * entries whose filename (basename or full path) matches any excluded name.
30
45
  *
31
46
  * Returns an empty array for empty, truncated, or non-ZIP input rather
32
- * than throwing — callers treat missing artifacts as non-fatal.
47
+ * than throwing — callers treat missing artifacts as non-fatal. Errors
48
+ * while *decompressing* a structurally valid entry (corrupt deflate
49
+ * stream, unsupported compression method) do propagate: at that point
50
+ * the archive's structure vouched for the entry, and silently dropping
51
+ * it would be the exact corruption this module exists to prevent.
33
52
  */
34
53
  export async function extractZipFileEntries(
35
54
  zipBytes: Uint8Array,
36
55
  options?: { exclude?: readonly string[] },
37
56
  ): Promise<ZipFileEntry[]> {
38
- if (zipBytes.length < 4) return [];
57
+ if (zipBytes.length < EOCD_MIN_SIZE) return [];
39
58
 
40
- let entries: ZipEntry[];
59
+ let entries: ZipStructuralEntry[];
41
60
  try {
42
- entries = parseZipEntries(zipBytes);
43
- } catch {
61
+ entries = parseZipStructure(zipBytes);
62
+ } catch (err) {
63
+ console.warn(
64
+ "[zip-extract] archive has no readable central directory " +
65
+ `(truncated or corrupt download?): ${err instanceof Error ? err.message : String(err)}`,
66
+ );
44
67
  return [];
45
68
  }
46
69
 
@@ -58,16 +81,6 @@ export async function extractZipFileEntries(
58
81
  return results;
59
82
  }
60
83
 
61
- // ─── Internals ───────────────────────────────────────────────────────────
62
-
63
- interface ZipEntry {
64
- name: string;
65
- isDirectory: boolean;
66
- compressedData: Uint8Array;
67
- compressionMethod: number;
68
- uncompressedSize: number;
69
- }
70
-
71
84
  function isExcluded(name: string, excludeSet: ReadonlySet<string>): boolean {
72
85
  if (excludeSet.size === 0) return false;
73
86
  if (excludeSet.has(name)) return true;
@@ -76,104 +89,9 @@ function isExcluded(name: string, excludeSet: ReadonlySet<string>): boolean {
76
89
  return excludeSet.has(basename);
77
90
  }
78
91
 
79
- function parseZipEntries(data: Uint8Array): ZipEntry[] {
80
- const entries: ZipEntry[] = [];
81
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
82
- let offset = 0;
83
-
84
- while (offset < data.length - 4) {
85
- const signature = view.getUint32(offset, true);
86
- if (signature !== 0x04034b50) break; // Local file header signature
87
-
88
- const generalFlags = view.getUint16(offset + 6, true);
89
- const hasDataDescriptor = (generalFlags & 0x08) !== 0;
90
- const compressionMethod = view.getUint16(offset + 8, true);
91
- let compressedSize = view.getUint32(offset + 18, true);
92
- let uncompressedSize = view.getUint32(offset + 22, true);
93
- const fileNameLength = view.getUint16(offset + 26, true);
94
- const extraFieldLength = view.getUint16(offset + 28, true);
95
-
96
- const fileNameStart = offset + 30;
97
- const fileName = new TextDecoder().decode(
98
- data.subarray(fileNameStart, fileNameStart + fileNameLength),
99
- );
100
-
101
- const dataStart = fileNameStart + fileNameLength + extraFieldLength;
102
-
103
- if (hasDataDescriptor && compressedSize === 0) {
104
- const sizes = findDataDescriptor(data, view, dataStart, compressionMethod);
105
- compressedSize = sizes.compressedSize;
106
- uncompressedSize = sizes.uncompressedSize;
107
- }
108
-
109
- const compressedData = data.subarray(dataStart, dataStart + compressedSize);
110
-
111
- entries.push({
112
- name: fileName,
113
- isDirectory: fileName.endsWith("/"),
114
- compressedData,
115
- compressionMethod,
116
- uncompressedSize,
117
- });
118
-
119
- let nextOffset = dataStart + compressedSize;
120
- if (hasDataDescriptor) {
121
- if (nextOffset + 4 <= data.length && view.getUint32(nextOffset, true) === 0x08074b50) {
122
- nextOffset += 16; // signature(4) + crc(4) + compressedSize(4) + uncompressedSize(4)
123
- } else {
124
- nextOffset += 12; // crc(4) + compressedSize(4) + uncompressedSize(4)
125
- }
126
- }
127
- offset = nextOffset;
128
- }
129
-
130
- return entries;
131
- }
132
-
133
- /**
134
- * Scan forward from dataStart to find the data descriptor that contains
135
- * the actual compressed and uncompressed sizes. Looks for either the
136
- * optional signature 0x08074b50 or falls back to scanning the central
137
- * directory for the matching entry.
138
- */
139
- function findDataDescriptor(
140
- data: Uint8Array,
141
- view: DataView,
142
- dataStart: number,
143
- _compressionMethod: number,
144
- ): { compressedSize: number; uncompressedSize: number } {
145
- for (let pos = dataStart; pos < data.length - 16; pos++) {
146
- const sig = view.getUint32(pos, true);
147
- if (sig === 0x08074b50) {
148
- return {
149
- compressedSize: view.getUint32(pos + 8, true),
150
- uncompressedSize: view.getUint32(pos + 12, true),
151
- };
152
- }
153
- if (sig === 0x04034b50 || sig === 0x02014b50) {
154
- const descStart = pos - 12;
155
- if (descStart >= dataStart) {
156
- return {
157
- compressedSize: view.getUint32(descStart + 4, true),
158
- uncompressedSize: view.getUint32(descStart + 8, true),
159
- };
160
- }
161
- break;
162
- }
163
- }
164
- for (let pos = dataStart; pos < data.length - 4; pos++) {
165
- const sig = view.getUint32(pos, true);
166
- if (sig === 0x04034b50 || sig === 0x02014b50 || sig === 0x08074b50) {
167
- const compressedSize = sig === 0x08074b50
168
- ? view.getUint32(pos + 8, true)
169
- : pos - dataStart;
170
- return { compressedSize, uncompressedSize: 0 };
171
- }
172
- }
173
- return { compressedSize: data.length - dataStart, uncompressedSize: 0 };
174
- }
92
+ // ─── Decompression ───────────────────────────────────────────────────────
175
93
 
176
- async function decompressEntry(entry: ZipEntry): Promise<string> {
94
+ async function decompressEntry(entry: ZipStructuralEntry): Promise<string> {
177
95
  if (entry.compressionMethod === 0) {
178
96
  return new TextDecoder().decode(entry.compressedData);
179
97
  }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Policy-free structural ZIP parsing: end-of-central-directory (EOCD)
3
+ * location plus central-directory walk, producing one record per entry.
4
+ *
5
+ * Parsing is *central-directory-based* — never a front-to-back walk of
6
+ * local file headers. This is a correctness decision, not a style choice
7
+ * (issues #450 and #567): local headers of streaming entries
8
+ * (general-purpose flag bit 3) carry zeroed sizes, and the only
9
+ * local-only way to recover them is scanning the payload for the
10
+ * data-descriptor signature — which silently truncates any stored entry
11
+ * whose *content* happens to contain those four bytes, and
12
+ * desynchronizes every entry after it. The central directory always
13
+ * carries the real sizes, and every consumer here holds the complete
14
+ * archive bytes, so nothing a local-header walk could offer is needed.
15
+ *
16
+ * This layer maps bytes to entry records and nothing else. Policy — what
17
+ * a structural failure means, which entries are acceptable, how payloads
18
+ * are decoded — belongs to the consumers, and they differ on purpose:
19
+ *
20
+ * - shared/zip-extract.ts (skill artifacts): structural failure is
21
+ * NON-FATAL — both editions' push gates validated every artifact
22
+ * with a central-directory-based reader before storage, so a defect
23
+ * here can only be a truncated or corrupted download.
24
+ * - execute-deep-agent/attachment-injector.ts (user attachments):
25
+ * structural failure is FAIL-HARD — the input is an untrusted
26
+ * upload and nothing upstream vouched for it.
27
+ *
28
+ * ZIP64 is deliberately unsupported: both consumers cap input far below
29
+ * every ZIP64 threshold (skill push gates: 100MB / 10,000 files;
30
+ * attachment uploads: 10MB).
31
+ *
32
+ * Throws plain `Error`s on structural defects (no valid EOCD, a central
33
+ * directory or local header record that does not parse, a payload slice
34
+ * that runs past the end of the buffer); consumers translate those into
35
+ * their own error models.
36
+ */
37
+
38
+ // ─── Entry records ───────────────────────────────────────────────────────
39
+
40
+ export interface ZipStructuralEntry {
41
+ /** Entry path exactly as recorded in the central directory. */
42
+ readonly name: string;
43
+ readonly isDirectory: boolean;
44
+ readonly compressionMethod: number;
45
+ /**
46
+ * The central directory's declared uncompressed size — authoritative for
47
+ * pre-extraction accounting (e.g. ZIP-bomb budgeting), but still a
48
+ * *declaration*: a consumer that distrusts its input must enforce it
49
+ * against the actual decompressed output.
50
+ */
51
+ readonly uncompressedSize: number;
52
+ /** The entry's raw payload slice (a view, not a copy) of the archive buffer. */
53
+ readonly compressedData: Uint8Array;
54
+ }
55
+
56
+ // ─── Format constants ────────────────────────────────────────────────────
57
+
58
+ const LOCAL_FILE_HEADER_SIG = 0x04034b50;
59
+ const CENTRAL_DIRECTORY_SIG = 0x02014b50;
60
+ const EOCD_SIG = 0x06054b50;
61
+
62
+ /** Fixed size of the EOCD record, excluding the variable-length comment. */
63
+ export const EOCD_MIN_SIZE = 22;
64
+ /** The archive comment length is a u16, so the EOCD sits within the last 64KB + 22 bytes. */
65
+ const EOCD_MAX_COMMENT = 0xffff;
66
+
67
+ const LOCAL_HEADER_SIZE = 30;
68
+ const CD_RECORD_SIZE = 46;
69
+
70
+ // ─── Parsing ─────────────────────────────────────────────────────────────
71
+
72
+ /**
73
+ * Enumerate the archive's entries from its central directory.
74
+ *
75
+ * Throws on any structural defect: no valid EOCD, a central directory or
76
+ * local header record that doesn't parse, or a payload slice that runs
77
+ * past the end of the buffer.
78
+ */
79
+ export function parseZipStructure(data: Uint8Array): ZipStructuralEntry[] {
80
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
81
+ const eocd = findEndOfCentralDirectory(data, view);
82
+
83
+ const entries: ZipStructuralEntry[] = [];
84
+ let offset = eocd.centralDirectoryOffset;
85
+
86
+ for (let i = 0; i < eocd.entryCount; i++) {
87
+ if (offset + CD_RECORD_SIZE > data.length || view.getUint32(offset, true) !== CENTRAL_DIRECTORY_SIG) {
88
+ throw new Error(`invalid central directory record at offset ${offset}`);
89
+ }
90
+
91
+ const compressionMethod = view.getUint16(offset + 10, true);
92
+ const compressedSize = view.getUint32(offset + 20, true);
93
+ const uncompressedSize = view.getUint32(offset + 24, true);
94
+ const fileNameLength = view.getUint16(offset + 28, true);
95
+ const extraFieldLength = view.getUint16(offset + 30, true);
96
+ const commentLength = view.getUint16(offset + 32, true);
97
+ const localHeaderOffset = view.getUint32(offset + 42, true);
98
+
99
+ const fileName = new TextDecoder().decode(
100
+ data.subarray(offset + CD_RECORD_SIZE, offset + CD_RECORD_SIZE + fileNameLength),
101
+ );
102
+
103
+ // The local header is consulted for one thing only: the payload start.
104
+ // Its name/extra field lengths can legitimately differ from the central
105
+ // directory's (streaming writers pad the extra field), so the offset
106
+ // cannot be derived from CD fields alone.
107
+ const dataStart = payloadStart(data, view, localHeaderOffset, fileName);
108
+ if (dataStart + compressedSize > data.length) {
109
+ throw new Error(`entry "${fileName}" payload runs past end of archive`);
110
+ }
111
+
112
+ entries.push({
113
+ name: fileName,
114
+ isDirectory: fileName.endsWith("/"),
115
+ compressionMethod,
116
+ uncompressedSize,
117
+ compressedData: data.subarray(dataStart, dataStart + compressedSize),
118
+ });
119
+
120
+ offset += CD_RECORD_SIZE + fileNameLength + extraFieldLength + commentLength;
121
+ }
122
+
123
+ return entries;
124
+ }
125
+
126
+ interface EndOfCentralDirectory {
127
+ centralDirectoryOffset: number;
128
+ entryCount: number;
129
+ }
130
+
131
+ /**
132
+ * Locate and validate the end-of-central-directory record.
133
+ *
134
+ * Scans backward from the end of the archive across the maximum comment
135
+ * span. A signature match alone is not trusted (the four bytes can occur
136
+ * inside a trailing comment): the record must also point at an offset
137
+ * that actually holds a central directory record, or be a genuinely
138
+ * empty archive.
139
+ */
140
+ function findEndOfCentralDirectory(data: Uint8Array, view: DataView): EndOfCentralDirectory {
141
+ const scanFloor = Math.max(0, data.length - EOCD_MIN_SIZE - EOCD_MAX_COMMENT);
142
+
143
+ for (let pos = data.length - EOCD_MIN_SIZE; pos >= scanFloor; pos--) {
144
+ if (view.getUint32(pos, true) !== EOCD_SIG) continue;
145
+
146
+ const entryCount = view.getUint16(pos + 10, true);
147
+ const centralDirectorySize = view.getUint32(pos + 12, true);
148
+ const centralDirectoryOffset = view.getUint32(pos + 16, true);
149
+
150
+ const directoryEndsAtRecord = centralDirectoryOffset + centralDirectorySize <= pos;
151
+ const directoryLooksReal =
152
+ entryCount === 0 ||
153
+ (centralDirectoryOffset + 4 <= data.length &&
154
+ view.getUint32(centralDirectoryOffset, true) === CENTRAL_DIRECTORY_SIG);
155
+
156
+ if (directoryEndsAtRecord && directoryLooksReal) {
157
+ return { centralDirectoryOffset, entryCount };
158
+ }
159
+ }
160
+
161
+ throw new Error("no end-of-central-directory record found");
162
+ }
163
+
164
+ /** Resolve where an entry's payload begins, from its local file header. */
165
+ function payloadStart(
166
+ data: Uint8Array,
167
+ view: DataView,
168
+ localHeaderOffset: number,
169
+ fileName: string,
170
+ ): number {
171
+ if (
172
+ localHeaderOffset + LOCAL_HEADER_SIZE > data.length ||
173
+ view.getUint32(localHeaderOffset, true) !== LOCAL_FILE_HEADER_SIG
174
+ ) {
175
+ throw new Error(`entry "${fileName}" has no local file header at offset ${localHeaderOffset}`);
176
+ }
177
+
178
+ const localNameLength = view.getUint16(localHeaderOffset + 26, true);
179
+ const localExtraLength = view.getUint16(localHeaderOffset + 28, true);
180
+ return localHeaderOffset + LOCAL_HEADER_SIZE + localNameLength + localExtraLength;
181
+ }
@@ -1,99 +0,0 @@
1
- /**
2
- * The runner-synthesized datastore records attachment (T05, DD-001 SD-2).
3
- *
4
- * When an agent declares `datastore_usages`, the runner synthesizes ONE
5
- * MCP attachment serving the five record tools (DD-005) — the agent
6
- * builder writes a usage line and gets tools; no McpServer resource, no
7
- * Environment, no credential in any manifest. Harness-agnostic like
8
- * mcp-resolver.ts: both ExecuteCursor and ExecuteDeepAgent inject the
9
- * synthesized entry through their existing ResolvedMcpServer paths.
10
- *
11
- * Two connection shapes, one roster (the bridge's records-only roster,
12
- * T05 R1):
13
- * - Bridge endpoint configured (cloud): Streamable HTTP against the
14
- * bridge's /records route, with the execution's own session-scoped
15
- * credential as the Bearer token — the server's reach chain (DD-006
16
- * Path 1) authorizes from that token; the bridge stays a
17
- * non-validating passthrough.
18
- * - No bridge endpoint (OSS/local): a spawned `stigmer mcp-server`
19
- * stdio child with STIGMER_MCP_ROSTER=records against the local
20
- * backend (zero new distribution — the CLI already embeds the
21
- * bridge). No credential: the local backend is unauthenticated and
22
- * OSS callers resolve as the local principal (T05 R2).
23
- *
24
- * Approval-free by construction (DD-001 SD-3): empty toolApprovals +
25
- * pinnedToolApprovals + toolApprovalOverrides mean mergeApprovalPolicies
26
- * emits no entries for this server — zero classifier involvement, and no
27
- * proto surface can target it (the attachment has no McpServerUsage, and
28
- * since issue #349 an agent's overrides are scoped to their own usage's
29
- * server, so a same-named override elsewhere cannot reach this one).
30
- * `discoveredCapabilitiesEmpty`
31
- * is false and the attachment has no McpServerUsage, so the connect
32
- * backfill (whose destructiveHint tightener would force-gate
33
- * delete_record — silently skipped on channels under UNATTENDED mode)
34
- * is structurally unable to touch it. Callers must still inject AFTER
35
- * resolve + backfill; both harness call sites do.
36
- */
37
- import type { DatastoreUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
38
- import type { ResolvedMcpServer } from "./mcp-resolver.js";
39
- import { type SynthesizedAttachmentOptions } from "./synthesized-attachment.js";
40
- /**
41
- * The synthesized attachment's slug. Reserved: a user McpServer with
42
- * this slug is shadowed by the synthesized attachment (the record tools
43
- * must exist whenever `datastore_usages` says so), with a warning.
44
- */
45
- export declare const DATASTORE_ATTACHMENT_SLUG = "stigmer-records";
46
- /** The bridge route serving the records-only roster (mcp-server T05 R1). */
47
- export declare const RECORDS_ROUTE = "/records";
48
- /**
49
- * Synthesize the records attachment for an agent's datastore usages.
50
- * Returns undefined when the agent uses no datastores — the attachment
51
- * exists exactly when the usage edge does.
52
- */
53
- export declare function synthesizeDatastoreAttachment(datastoreUsages: DatastoreUsage[], options: SynthesizedAttachmentOptions): ResolvedMcpServer | undefined;
54
- /**
55
- * The five record tools the records roster always serves (DD-005; the
56
- * mcp-server's records roster registers all five unconditionally for the
57
- * agent audience — only the `org` argument shape varies by audience, see
58
- * mcp-server/src/domains/records/tools.ts). Because the roster never
59
- * legitimately narrows, any of these missing from the connected toolset
60
- * means the store is degraded (issue #325) — the prompt section and the
61
- * reconciliation both derive from this one list so they cannot drift.
62
- */
63
- export declare const EXPECTED_RECORD_TOOLS: readonly ["describe_datastore", "find_records", "insert_record", "update_record", "delete_record"];
64
- /**
65
- * Reconcile the record tools actually connected against the roster
66
- * contract (issue #325). Returns the expected tools absent from
67
- * `actualToolNames` — empty means healthy. Names are bare tool names
68
- * exactly as reported by tools/list (the mcp-enabled-tools identity
69
- * space); extraneous names are ignored.
70
- */
71
- export declare function missingRecordTools(actualToolNames: Iterable<string>): string[];
72
- /**
73
- * The operator-facing degradation notice (issue #325): pushed as a
74
- * MESSAGE_SYSTEM row on the execution status so "declared N datastores,
75
- * X/5 record tools connected" is visible without reading a transcript.
76
- * Lives HERE with the rest of the datastore-attachment wording — the
77
- * harness only threads it.
78
- */
79
- export declare function formatDatastoreDegradationNotice(declaredCount: number, missing: readonly string[]): string;
80
- /**
81
- * The datastores prompt section (DD-005 SD-5, the skills-section
82
- * precedent). Shared by both harnesses so the section text cannot drift
83
- * (the sender-identity precedent). Two honest renderings (issue #325):
84
- *
85
- * - Healthy (`missingToolNames` empty or omitted — the Cursor harness
86
- * always calls it this way: the Cursor SDK connects MCP itself, so
87
- * that harness can never observe the live roster):
88
- * `<available_datastores>` names the attached datastores, points the
89
- * model at describe_datastore first, and carries a standing
90
- * failure-disclosure instruction — the only mechanism that covers
91
- * tools which connected but fail at call time (the WhatsApp-pilot
92
- * outage shape).
93
- * - Degraded (deep-agent only, from missingRecordTools against the
94
- * connected roster): `<unavailable_datastores>` instead — the section
95
- * must not promise tools the agent does not have. Names the declared
96
- * datastores, states which record tools are missing, and instructs
97
- * plain disclosure over improvisation.
98
- */
99
- export declare function formatDatastoresSection(datastoreUsages: DatastoreUsage[], missingToolNames?: readonly string[]): string;
@@ -1,184 +0,0 @@
1
- /**
2
- * The runner-synthesized datastore records attachment (T05, DD-001 SD-2).
3
- *
4
- * When an agent declares `datastore_usages`, the runner synthesizes ONE
5
- * MCP attachment serving the five record tools (DD-005) — the agent
6
- * builder writes a usage line and gets tools; no McpServer resource, no
7
- * Environment, no credential in any manifest. Harness-agnostic like
8
- * mcp-resolver.ts: both ExecuteCursor and ExecuteDeepAgent inject the
9
- * synthesized entry through their existing ResolvedMcpServer paths.
10
- *
11
- * Two connection shapes, one roster (the bridge's records-only roster,
12
- * T05 R1):
13
- * - Bridge endpoint configured (cloud): Streamable HTTP against the
14
- * bridge's /records route, with the execution's own session-scoped
15
- * credential as the Bearer token — the server's reach chain (DD-006
16
- * Path 1) authorizes from that token; the bridge stays a
17
- * non-validating passthrough.
18
- * - No bridge endpoint (OSS/local): a spawned `stigmer mcp-server`
19
- * stdio child with STIGMER_MCP_ROSTER=records against the local
20
- * backend (zero new distribution — the CLI already embeds the
21
- * bridge). No credential: the local backend is unauthenticated and
22
- * OSS callers resolve as the local principal (T05 R2).
23
- *
24
- * Approval-free by construction (DD-001 SD-3): empty toolApprovals +
25
- * pinnedToolApprovals + toolApprovalOverrides mean mergeApprovalPolicies
26
- * emits no entries for this server — zero classifier involvement, and no
27
- * proto surface can target it (the attachment has no McpServerUsage, and
28
- * since issue #349 an agent's overrides are scoped to their own usage's
29
- * server, so a same-named override elsewhere cannot reach this one).
30
- * `discoveredCapabilitiesEmpty`
31
- * is false and the attachment has no McpServerUsage, so the connect
32
- * backfill (whose destructiveHint tightener would force-gate
33
- * delete_record — silently skipped on channels under UNATTENDED mode)
34
- * is structurally unable to touch it. Callers must still inject AFTER
35
- * resolve + backfill; both harness call sites do.
36
- */
37
- import { grpcTarget } from "./synthesized-attachment.js";
38
- /**
39
- * The synthesized attachment's slug. Reserved: a user McpServer with
40
- * this slug is shadowed by the synthesized attachment (the record tools
41
- * must exist whenever `datastore_usages` says so), with a warning.
42
- */
43
- export const DATASTORE_ATTACHMENT_SLUG = "stigmer-records";
44
- /** The bridge route serving the records-only roster (mcp-server T05 R1). */
45
- export const RECORDS_ROUTE = "/records";
46
- /**
47
- * Synthesize the records attachment for an agent's datastore usages.
48
- * Returns undefined when the agent uses no datastores — the attachment
49
- * exists exactly when the usage edge does.
50
- */
51
- export function synthesizeDatastoreAttachment(datastoreUsages, options) {
52
- if (datastoreUsages.length === 0) {
53
- return undefined;
54
- }
55
- // Approval-free by construction + backfill-proof: see file header.
56
- const base = {
57
- slug: DATASTORE_ATTACHMENT_SLUG,
58
- toolApprovals: [],
59
- pinnedToolApprovals: [],
60
- toolApprovalOverrides: [],
61
- discoveredCapabilitiesEmpty: false,
62
- };
63
- if (options.bridgeEndpoint !== null && options.bridgeEndpoint !== "") {
64
- return {
65
- ...base,
66
- connectionType: "http",
67
- url: options.bridgeEndpoint.replace(/\/+$/, "") + RECORDS_ROUTE,
68
- headers: options.credential !== null && options.credential !== ""
69
- ? { Authorization: `Bearer ${options.credential}` }
70
- : undefined,
71
- };
72
- }
73
- return {
74
- ...base,
75
- connectionType: "stdio",
76
- command: "stigmer",
77
- args: ["mcp-server"],
78
- env: {
79
- STIGMER_MCP_ROSTER: "records",
80
- STIGMER_SERVER_ADDRESS: grpcTarget(options.backendEndpoint),
81
- },
82
- };
83
- }
84
- /**
85
- * The five record tools the records roster always serves (DD-005; the
86
- * mcp-server's records roster registers all five unconditionally for the
87
- * agent audience — only the `org` argument shape varies by audience, see
88
- * mcp-server/src/domains/records/tools.ts). Because the roster never
89
- * legitimately narrows, any of these missing from the connected toolset
90
- * means the store is degraded (issue #325) — the prompt section and the
91
- * reconciliation both derive from this one list so they cannot drift.
92
- */
93
- export const EXPECTED_RECORD_TOOLS = [
94
- "describe_datastore",
95
- "find_records",
96
- "insert_record",
97
- "update_record",
98
- "delete_record",
99
- ];
100
- /**
101
- * Reconcile the record tools actually connected against the roster
102
- * contract (issue #325). Returns the expected tools absent from
103
- * `actualToolNames` — empty means healthy. Names are bare tool names
104
- * exactly as reported by tools/list (the mcp-enabled-tools identity
105
- * space); extraneous names are ignored.
106
- */
107
- export function missingRecordTools(actualToolNames) {
108
- const present = new Set(actualToolNames);
109
- return EXPECTED_RECORD_TOOLS.filter((name) => !present.has(name));
110
- }
111
- /**
112
- * The operator-facing degradation notice (issue #325): pushed as a
113
- * MESSAGE_SYSTEM row on the execution status so "declared N datastores,
114
- * X/5 record tools connected" is visible without reading a transcript.
115
- * Lives HERE with the rest of the datastore-attachment wording — the
116
- * harness only threads it.
117
- */
118
- export function formatDatastoreDegradationNotice(declaredCount, missing) {
119
- const connected = EXPECTED_RECORD_TOOLS.length - missing.length;
120
- return (`Datastore record tools unavailable this turn: declared ` +
121
- `${declaredCount} datastore(s), ${connected}/${EXPECTED_RECORD_TOOLS.length} ` +
122
- `record tools connected (missing: ${missing.join(", ")}). The agent has ` +
123
- `been instructed to disclose this instead of answering from memory.`);
124
- }
125
- /**
126
- * The datastores prompt section (DD-005 SD-5, the skills-section
127
- * precedent). Shared by both harnesses so the section text cannot drift
128
- * (the sender-identity precedent). Two honest renderings (issue #325):
129
- *
130
- * - Healthy (`missingToolNames` empty or omitted — the Cursor harness
131
- * always calls it this way: the Cursor SDK connects MCP itself, so
132
- * that harness can never observe the live roster):
133
- * `<available_datastores>` names the attached datastores, points the
134
- * model at describe_datastore first, and carries a standing
135
- * failure-disclosure instruction — the only mechanism that covers
136
- * tools which connected but fail at call time (the WhatsApp-pilot
137
- * outage shape).
138
- * - Degraded (deep-agent only, from missingRecordTools against the
139
- * connected roster): `<unavailable_datastores>` instead — the section
140
- * must not promise tools the agent does not have. Names the declared
141
- * datastores, states which record tools are missing, and instructs
142
- * plain disclosure over improvisation.
143
- */
144
- export function formatDatastoresSection(datastoreUsages, missingToolNames = []) {
145
- const entries = datastoreUsages
146
- .map((u) => u.datastoreRef?.slug)
147
- .filter((s) => !!s)
148
- .map((slug) => `- ${slug}`);
149
- if (missingToolNames.length > 0) {
150
- return [
151
- "<unavailable_datastores>",
152
- "This agent is configured to use the datastores listed below, but the",
153
- "following record tools failed to connect this turn:",
154
- missingToolNames.join(", ") + ".",
155
- "Treat these datastores as unreachable for any operation that needs a",
156
- "missing tool.",
157
- "",
158
- ...entries,
159
- "",
160
- "When the user's request depends on one of these datastores, tell them",
161
- "plainly that the datastore cannot be reached right now. Never answer",
162
- "from memory, guess, or improvise a substitute for what the datastore",
163
- "would have returned.",
164
- "</unavailable_datastores>",
165
- ].join("\n");
166
- }
167
- return [
168
- "<available_datastores>",
169
- "You have access to the following datastores through the record tools",
170
- `(${EXPECTED_RECORD_TOOLS.join(", ")}).`,
171
- "Before your first operation against a datastore, call describe_datastore to",
172
- "learn its collections, field encodings, and which operations you are allowed",
173
- "to perform.",
174
- "",
175
- ...entries,
176
- "",
177
- "If any record tool is missing from your toolset or a record tool call",
178
- "fails, the datastore is unreachable: tell the user plainly that you cannot",
179
- "reach it right now, and do not answer from memory, guess, or improvise a",
180
- "substitute for what it would have returned.",
181
- "</available_datastores>",
182
- ].join("\n");
183
- }
184
- //# sourceMappingURL=datastore-attachment.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"datastore-attachment.js","sourceRoot":"","sources":["../../src/shared/datastore-attachment.ts"],"names":[],"mappings":"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"}