agentwrangler 0.1.0

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 (157) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +116 -0
  3. package/dist/apply/jobs.js +429 -0
  4. package/dist/apply/open-terminal-child.mjs +98 -0
  5. package/dist/apply/open-terminal.js +221 -0
  6. package/dist/apply/settings-gen.js +35 -0
  7. package/dist/cli/agentwrangler.js +18 -0
  8. package/dist/daemon/config.js +51 -0
  9. package/dist/daemon/http.js +258 -0
  10. package/dist/daemon/index.js +372 -0
  11. package/dist/daemon/outcomes-pass.js +82 -0
  12. package/dist/daemon/readiness.js +15 -0
  13. package/dist/daemon/router.js +756 -0
  14. package/dist/daemon/static.js +146 -0
  15. package/dist/db/migrate.js +72 -0
  16. package/dist/db/migrations/001_observe.sql +196 -0
  17. package/dist/db/migrations/002_indexes.sql +6 -0
  18. package/dist/db/migrations/003_context_inventory_history.sql +20 -0
  19. package/dist/db/migrations/004_apply_jobs.sql +17 -0
  20. package/dist/db/migrations/005_tool_event_metadata.sql +17 -0
  21. package/dist/db/migrations/006_d7_query_indexes.sql +9 -0
  22. package/dist/db/migrations/007_work_item_branch_keys.sql +11 -0
  23. package/dist/db/migrations/008_thinking_tokens.sql +1 -0
  24. package/dist/db/migrations/009_user_turn_count.sql +1 -0
  25. package/dist/db/migrations/010_workspace_cwd.sql +1 -0
  26. package/dist/db/migrations/011_reports.sql +1 -0
  27. package/dist/db/migrations/012_reconcile_indexes.sql +2 -0
  28. package/dist/db/migrations/013_friction_fields.sql +5 -0
  29. package/dist/db/migrations/014_session_churn.sql +11 -0
  30. package/dist/db/migrations/015_gap_aggregates.sql +6 -0
  31. package/dist/db/open.js +30 -0
  32. package/dist/detector/benchmark-anchors.js +36 -0
  33. package/dist/detector/calibration.js +302 -0
  34. package/dist/detector/context-history-retention.js +312 -0
  35. package/dist/detector/context-probe.js +574 -0
  36. package/dist/detector/d1-source-identity.js +25 -0
  37. package/dist/detector/detectors/d10_catalog_footprint.js +146 -0
  38. package/dist/detector/detectors/d1_ctx_always_loaded.js +203 -0
  39. package/dist/detector/detectors/d2_session_long_full_context.js +119 -0
  40. package/dist/detector/detectors/d4_model_mismatch.js +258 -0
  41. package/dist/detector/detectors/d5_limit_burn_forecast.js +138 -0
  42. package/dist/detector/detectors/d6_tool_result_bloat.js +301 -0
  43. package/dist/detector/detectors/d7_loop_retry_waste.js +345 -0
  44. package/dist/detector/detectors/d8_cache_write_churn.js +201 -0
  45. package/dist/detector/detectors/d9_idle_background_session.js +101 -0
  46. package/dist/detector/engine.js +88 -0
  47. package/dist/detector/index.js +17 -0
  48. package/dist/detector/measurement.js +426 -0
  49. package/dist/detector/practice-registry.js +259 -0
  50. package/dist/detector/registry.js +32 -0
  51. package/dist/detector/savings.js +249 -0
  52. package/dist/detector/types.js +14 -0
  53. package/dist/evidence/common/approved-input.js +632 -0
  54. package/dist/evidence/common/boundary.js +84 -0
  55. package/dist/evidence/common/canonical.js +55 -0
  56. package/dist/evidence/common/redaction.js +321 -0
  57. package/dist/evidence/common/sqlite.js +25 -0
  58. package/dist/evidence/common/state.js +29 -0
  59. package/dist/evidence/cond1/cli.js +289 -0
  60. package/dist/evidence/cond1/packet.js +407 -0
  61. package/dist/evidence/cond1/prepare.js +295 -0
  62. package/dist/evidence/cond1/score.js +349 -0
  63. package/dist/evidence/cond1/types.js +1 -0
  64. package/dist/evidence/create-approval.js +365 -0
  65. package/dist/evidence/create-scratch.js +542 -0
  66. package/dist/evidence/d7/cli.js +113 -0
  67. package/dist/evidence/d7/measure.js +193 -0
  68. package/dist/evidence/d7/types.js +1 -0
  69. package/dist/evidence/discover-approval.js +492 -0
  70. package/dist/evidence/g2/adjudicate.js +20 -0
  71. package/dist/evidence/g2/cli.js +207 -0
  72. package/dist/evidence/g2/kappa.js +39 -0
  73. package/dist/evidence/g2/pipeline.js +92 -0
  74. package/dist/evidence/g2/store.js +14 -0
  75. package/dist/evidence/github/client.js +1 -0
  76. package/dist/evidence/github/gh-cli-client.js +301 -0
  77. package/dist/evidence/r3/cli.js +209 -0
  78. package/dist/evidence/r3/evaluate.js +417 -0
  79. package/dist/evidence/r3/packet.js +162 -0
  80. package/dist/evidence/r3/prepare.js +405 -0
  81. package/dist/evidence/r3/score.js +341 -0
  82. package/dist/evidence/r3/transcript.js +155 -0
  83. package/dist/evidence/r3/types.js +4 -0
  84. package/dist/hook/context-budget-hook.mjs +138 -0
  85. package/dist/hook/danger-guard-denylist.json +27 -0
  86. package/dist/hook/danger-guard-hook.mjs +167 -0
  87. package/dist/hook/install.js +0 -0
  88. package/dist/hook/limit-burn-hook.mjs +127 -0
  89. package/dist/hook/loop-guard-hook.mjs +104 -0
  90. package/dist/hook/precompact-checkpoint-hook.mjs +123 -0
  91. package/dist/ingest/churn-collector.js +122 -0
  92. package/dist/ingest/detector-hook.js +52 -0
  93. package/dist/ingest/discovery.js +207 -0
  94. package/dist/ingest/health.js +43 -0
  95. package/dist/ingest/index.js +28 -0
  96. package/dist/ingest/ingestor.js +509 -0
  97. package/dist/ingest/parser.js +344 -0
  98. package/dist/ingest/pricing.js +153 -0
  99. package/dist/ingest/reconcile.js +52 -0
  100. package/dist/ingest/tail.js +152 -0
  101. package/dist/ingest/types.js +24 -0
  102. package/dist/ingest/workspace-mapping.js +114 -0
  103. package/dist/oauth/anthropic-api-key.js +88 -0
  104. package/dist/oauth/count-tokens.js +86 -0
  105. package/dist/oauth/credentials.js +171 -0
  106. package/dist/oauth/judge-g2-client.js +154 -0
  107. package/dist/oauth/usage.js +167 -0
  108. package/dist/outcomes/branch-key.js +49 -0
  109. package/dist/outcomes/conclusions.js +45 -0
  110. package/dist/outcomes/derive.js +94 -0
  111. package/dist/outcomes/finding-extractors.js +131 -0
  112. package/dist/outcomes/findings.js +237 -0
  113. package/dist/outcomes/github/client.js +367 -0
  114. package/dist/outcomes/github/credential.js +195 -0
  115. package/dist/outcomes/github/gh-cli-client.js +340 -0
  116. package/dist/outcomes/linker.js +486 -0
  117. package/dist/outcomes/pool.js +24 -0
  118. package/dist/outcomes/sync.js +276 -0
  119. package/dist/query/api/agents-liveness.js +182 -0
  120. package/dist/query/api/burn-status.js +50 -0
  121. package/dist/query/api/context-budget.js +114 -0
  122. package/dist/query/api/context-composition.js +67 -0
  123. package/dist/query/api/cost-per-success.js +104 -0
  124. package/dist/query/api/delivery.js +92 -0
  125. package/dist/query/api/effectiveness.js +254 -0
  126. package/dist/query/api/efficiency-headroom.js +74 -0
  127. package/dist/query/api/headroom-trend.js +105 -0
  128. package/dist/query/api/hook-config.js +75 -0
  129. package/dist/query/api/hook-install.js +8 -0
  130. package/dist/query/api/hot-sessions.js +17 -0
  131. package/dist/query/api/idle-sessions.js +52 -0
  132. package/dist/query/api/index.js +40 -0
  133. package/dist/query/api/loop-guard.js +90 -0
  134. package/dist/query/api/offload-share.js +41 -0
  135. package/dist/query/api/outcomes.js +218 -0
  136. package/dist/query/api/overview.js +535 -0
  137. package/dist/query/api/rec-prompt.js +138 -0
  138. package/dist/query/api/recommendations-ledger.js +111 -0
  139. package/dist/query/api/recommendations.js +514 -0
  140. package/dist/query/api/reports.js +78 -0
  141. package/dist/query/api/self-churn.js +77 -0
  142. package/dist/query/api/self-percentiles.js +109 -0
  143. package/dist/query/api/session-drivers.js +153 -0
  144. package/dist/query/api/settings.js +85 -0
  145. package/dist/query/api/spend-flavor.js +234 -0
  146. package/dist/query/api/trends.js +155 -0
  147. package/dist/query/cap-weighted.js +119 -0
  148. package/dist/query/db-context.js +42 -0
  149. package/dist/query/envelope.js +71 -0
  150. package/dist/query/forecast.js +191 -0
  151. package/dist/query/settings-store.js +441 -0
  152. package/dist/query/spend.js +171 -0
  153. package/dist/query/trends.js +194 -0
  154. package/dist/ui/assets/index-DnRKgc21.css +1 -0
  155. package/dist/ui/assets/index-h1Q1wWq5.js +168 -0
  156. package/dist/ui/index.html +39 -0
  157. package/package.json +59 -0
@@ -0,0 +1,632 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import Database from "better-sqlite3";
5
+ import { SCRATCH_APPROVAL_VERSION, SCRATCH_PREPARATION_VERSION, } from "../create-scratch.js";
6
+ import { EvidenceBoundaryError, readFileIdentity, sameFileIdentity, } from "./boundary.js";
7
+ import { sha256Bytes } from "./canonical.js";
8
+ import { assertDatabaseIntegrity } from "./sqlite.js";
9
+ const SHA256_RE = /^[0-9a-f]{64}$/u;
10
+ function refuse(code) {
11
+ throw new EvidenceBoundaryError(code);
12
+ }
13
+ function record(value, code) {
14
+ if (value === null || typeof value !== "object" || Array.isArray(value))
15
+ refuse(code);
16
+ return value;
17
+ }
18
+ function exactKeys(value, expected, code) {
19
+ const actual = Object.keys(value).sort();
20
+ const wanted = [...expected].sort();
21
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
22
+ refuse(code);
23
+ }
24
+ }
25
+ function nonempty(value, code) {
26
+ if (typeof value !== "string" || value.length === 0)
27
+ refuse(code);
28
+ return value;
29
+ }
30
+ function sha256(value, code) {
31
+ const parsed = nonempty(value, code);
32
+ if (!SHA256_RE.test(parsed))
33
+ refuse(code);
34
+ return parsed;
35
+ }
36
+ function nonnegativeInteger(value, code) {
37
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
38
+ refuse(code);
39
+ return value;
40
+ }
41
+ function fileIdentity(value, code) {
42
+ const parsed = record(value, code);
43
+ exactKeys(parsed, ["device", "inode", "size"], code);
44
+ return {
45
+ device: nonempty(parsed.device, code),
46
+ inode: nonempty(parsed.inode, code),
47
+ size: nonempty(parsed.size, code),
48
+ };
49
+ }
50
+ function repositoryEntry(value) {
51
+ const parsed = record(value, "repository_map_entry_invalid");
52
+ exactKeys(parsed, ["workspaceId", "owner", "repo", "reportAlias"], "repository_map_entry_unknown_or_missing_key");
53
+ return {
54
+ workspaceId: nonempty(parsed.workspaceId, "repository_workspace_id_invalid"),
55
+ owner: nonempty(parsed.owner, "repository_owner_invalid"),
56
+ repo: nonempty(parsed.repo, "repository_name_invalid"),
57
+ reportAlias: nonempty(parsed.reportAlias, "repository_report_alias_invalid"),
58
+ };
59
+ }
60
+ function parseRepositoryMap(value) {
61
+ if (!Array.isArray(value))
62
+ refuse("repository_map_invalid");
63
+ const workspaceIds = new Set();
64
+ const aliases = new Set();
65
+ return value.map((item) => {
66
+ const entry = repositoryEntry(item);
67
+ if (workspaceIds.has(entry.workspaceId))
68
+ refuse("repository_workspace_duplicate");
69
+ if (aliases.has(entry.reportAlias))
70
+ refuse("repository_report_alias_duplicate");
71
+ workspaceIds.add(entry.workspaceId);
72
+ aliases.add(entry.reportAlias);
73
+ return entry;
74
+ });
75
+ }
76
+ function parseApproval(value) {
77
+ const parsed = record(value, "approval_manifest_invalid");
78
+ exactKeys(parsed, [
79
+ "version",
80
+ "sourceDbPath",
81
+ "scratchDbPath",
82
+ "scratchStatePath",
83
+ "repositories",
84
+ "privateArtifactParentAcknowledged",
85
+ ], "approval_manifest_unknown_or_missing_key");
86
+ if (parsed.version !== SCRATCH_APPROVAL_VERSION)
87
+ refuse("approval_manifest_version_mismatch");
88
+ if (parsed.privateArtifactParentAcknowledged !== true) {
89
+ refuse("private_artifact_parent_not_acknowledged");
90
+ }
91
+ return {
92
+ version: SCRATCH_APPROVAL_VERSION,
93
+ sourceDbPath: nonempty(parsed.sourceDbPath, "approval_source_path_invalid"),
94
+ scratchDbPath: nonempty(parsed.scratchDbPath, "approval_scratch_path_invalid"),
95
+ scratchStatePath: nonempty(parsed.scratchStatePath, "approval_state_path_invalid"),
96
+ repositories: parseRepositoryMap(parsed.repositories),
97
+ privateArtifactParentAcknowledged: true,
98
+ };
99
+ }
100
+ function parsePreparation(value) {
101
+ const parsed = record(value, "preparation_manifest_invalid");
102
+ exactKeys(parsed, [
103
+ "version",
104
+ "createdAt",
105
+ "approval",
106
+ "source",
107
+ "scratch",
108
+ "state",
109
+ "repositoryMap",
110
+ "backup",
111
+ "prohibitedOperations",
112
+ ], "preparation_manifest_unknown_or_missing_key");
113
+ if (parsed.version !== SCRATCH_PREPARATION_VERSION) {
114
+ refuse("preparation_manifest_version_mismatch");
115
+ }
116
+ const createdAt = nonempty(parsed.createdAt, "preparation_created_at_invalid");
117
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(createdAt)) {
118
+ refuse("preparation_created_at_invalid");
119
+ }
120
+ const approval = record(parsed.approval, "preparation_approval_invalid");
121
+ exactKeys(approval, ["version", "sha256", "privateArtifactParentAcknowledged"], "preparation_approval_unknown_or_missing_key");
122
+ if (approval.version !== SCRATCH_APPROVAL_VERSION ||
123
+ approval.privateArtifactParentAcknowledged !== true) {
124
+ refuse("preparation_approval_invalid");
125
+ }
126
+ const source = record(parsed.source, "preparation_source_invalid");
127
+ exactKeys(source, ["path", "identity", "access"], "preparation_source_unknown_or_missing_key");
128
+ if (source.access !== "explicit-readonly-online-backup")
129
+ refuse("preparation_source_invalid");
130
+ const scratch = record(parsed.scratch, "preparation_scratch_invalid");
131
+ exactKeys(scratch, ["path", "identity", "sha256", "integrity", "publication"], "preparation_scratch_unknown_or_missing_key");
132
+ if (scratch.integrity !== "ok" || scratch.publication !== "atomic-hard-link-no-replace") {
133
+ refuse("preparation_scratch_invalid");
134
+ }
135
+ const state = record(parsed.state, "preparation_state_invalid");
136
+ exactKeys(state, ["path", "cleanup", "privateArtifactBoundary"], "preparation_state_unknown_or_missing_key");
137
+ if (state.cleanup !== "manual-only")
138
+ refuse("preparation_state_invalid");
139
+ if (state.privateArtifactBoundary !== "WINDOWS_INHERITED_PARENT_ACL_ACKNOWLEDGED" &&
140
+ state.privateArtifactBoundary !== "POSIX_OWNER_ONLY_MODES_REQUESTED") {
141
+ refuse("preparation_state_invalid");
142
+ }
143
+ const repositoryMap = record(parsed.repositoryMap, "preparation_repository_map_invalid");
144
+ exactKeys(repositoryMap, ["file", "sha256", "mappedWorkspaceN"], "preparation_repository_map_unknown_or_missing_key");
145
+ if (repositoryMap.file !== "repo-map.json")
146
+ refuse("preparation_repository_map_invalid");
147
+ const backup = record(parsed.backup, "preparation_backup_invalid");
148
+ exactKeys(backup, ["totalPages", "remainingPages"], "preparation_backup_unknown_or_missing_key");
149
+ if (backup.remainingPages !== 0)
150
+ refuse("preparation_backup_incomplete");
151
+ const prohibitedOperations = [
152
+ "migrations",
153
+ "github-reads",
154
+ "transcript-reads",
155
+ "reset-replay-vacuum-prune-delete",
156
+ ];
157
+ if (!Array.isArray(parsed.prohibitedOperations) ||
158
+ parsed.prohibitedOperations.length !== prohibitedOperations.length ||
159
+ parsed.prohibitedOperations.some((item, index) => item !== prohibitedOperations[index])) {
160
+ refuse("preparation_prohibited_operations_invalid");
161
+ }
162
+ return {
163
+ version: SCRATCH_PREPARATION_VERSION,
164
+ createdAt,
165
+ approval: {
166
+ version: SCRATCH_APPROVAL_VERSION,
167
+ sha256: sha256(approval.sha256, "preparation_approval_sha256_invalid"),
168
+ privateArtifactParentAcknowledged: true,
169
+ },
170
+ source: {
171
+ path: nonempty(source.path, "preparation_source_path_invalid"),
172
+ identity: fileIdentity(source.identity, "preparation_source_identity_invalid"),
173
+ access: "explicit-readonly-online-backup",
174
+ },
175
+ scratch: {
176
+ path: nonempty(scratch.path, "preparation_scratch_path_invalid"),
177
+ identity: fileIdentity(scratch.identity, "preparation_scratch_identity_invalid"),
178
+ sha256: sha256(scratch.sha256, "preparation_scratch_sha256_invalid"),
179
+ integrity: "ok",
180
+ publication: "atomic-hard-link-no-replace",
181
+ },
182
+ state: {
183
+ path: nonempty(state.path, "preparation_state_path_invalid"),
184
+ cleanup: "manual-only",
185
+ privateArtifactBoundary: state.privateArtifactBoundary,
186
+ },
187
+ repositoryMap: {
188
+ file: "repo-map.json",
189
+ sha256: sha256(repositoryMap.sha256, "preparation_repository_map_sha256_invalid"),
190
+ mappedWorkspaceN: nonnegativeInteger(repositoryMap.mappedWorkspaceN, "preparation_mapped_workspace_count_invalid"),
191
+ },
192
+ backup: {
193
+ totalPages: nonnegativeInteger(backup.totalPages, "preparation_total_pages_invalid"),
194
+ remainingPages: 0,
195
+ },
196
+ prohibitedOperations,
197
+ };
198
+ }
199
+ function comparisonPath(value) {
200
+ const normalized = path.normalize(value).replace(/[\\/]+$/u, "");
201
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
202
+ }
203
+ function samePath(left, right) {
204
+ return comparisonPath(left) === comparisonPath(right);
205
+ }
206
+ function within(parent, child) {
207
+ const relative = path.relative(parent, child);
208
+ return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`));
209
+ }
210
+ function existingFile(inputPath, code) {
211
+ if (inputPath.length === 0)
212
+ refuse(code);
213
+ let resolved;
214
+ try {
215
+ resolved = fs.realpathSync.native(inputPath);
216
+ }
217
+ catch {
218
+ refuse(code);
219
+ }
220
+ if (!fs.statSync(resolved).isFile())
221
+ refuse(code);
222
+ return resolved;
223
+ }
224
+ function existingDirectory(inputPath, code) {
225
+ if (inputPath.length === 0)
226
+ refuse(code);
227
+ let resolved;
228
+ try {
229
+ resolved = fs.realpathSync.native(inputPath);
230
+ }
231
+ catch {
232
+ refuse(code);
233
+ }
234
+ if (!fs.statSync(resolved).isDirectory())
235
+ refuse(code);
236
+ return resolved;
237
+ }
238
+ function descriptorIdentity(descriptor, code) {
239
+ const stat = fs.fstatSync(descriptor, { bigint: true });
240
+ if (!stat.isFile())
241
+ refuse(code);
242
+ return {
243
+ device: stat.dev.toString(),
244
+ inode: stat.ino.toString(),
245
+ size: stat.size.toString(),
246
+ };
247
+ }
248
+ function lstatFileIdentity(filePath, code) {
249
+ const stat = fs.lstatSync(filePath, { bigint: true });
250
+ if (!stat.isFile() || stat.isSymbolicLink())
251
+ refuse(code);
252
+ return {
253
+ device: stat.dev.toString(),
254
+ inode: stat.ino.toString(),
255
+ size: stat.size.toString(),
256
+ };
257
+ }
258
+ function exactIdentity(left, right) {
259
+ return left.device === right.device && left.inode === right.inode && left.size === right.size;
260
+ }
261
+ function aliasesIdentity(left, right) {
262
+ if (left.inode === "0" || right.inode === "0")
263
+ refuse("file_identity_unavailable");
264
+ return sameFileIdentity(left, right);
265
+ }
266
+ function readDescriptorBytes(descriptor, identity, code) {
267
+ const size = Number(identity.size);
268
+ if (!Number.isSafeInteger(size) || size < 0)
269
+ refuse(`${code}_size_invalid`);
270
+ const bytes = Buffer.alloc(size);
271
+ let offset = 0;
272
+ while (offset < size) {
273
+ const read = fs.readSync(descriptor, bytes, offset, size - offset, offset);
274
+ if (read === 0)
275
+ refuse(`${code}_short_read`);
276
+ offset += read;
277
+ }
278
+ if (!exactIdentity(identity, descriptorIdentity(descriptor, `${code}_changed`))) {
279
+ refuse(`${code}_changed`);
280
+ }
281
+ return bytes;
282
+ }
283
+ function queryOnlySnapshotFactory(snapshotBytes) {
284
+ const privateSnapshot = Buffer.from(snapshotBytes);
285
+ return () => {
286
+ const db = new Database(Buffer.from(privateSnapshot));
287
+ try {
288
+ db.pragma("query_only = ON");
289
+ if (db.pragma("query_only", { simple: true }) !== 1) {
290
+ throw new Error("query_only_not_enabled");
291
+ }
292
+ return db;
293
+ }
294
+ catch (error) {
295
+ db.close();
296
+ throw error;
297
+ }
298
+ };
299
+ }
300
+ function readDigestBoundJson(inputPath, expectedSha256, code, liveDbPath, liveIdentity, testHooks) {
301
+ if (!SHA256_RE.test(expectedSha256))
302
+ refuse(`${code}_sha256_invalid`);
303
+ const resolved = existingFile(inputPath, `${code}_missing_or_not_file`);
304
+ const descriptor = fs.openSync(resolved, "r");
305
+ let bytes;
306
+ try {
307
+ const identity = descriptorIdentity(descriptor, `${code}_not_file`);
308
+ if (!exactIdentity(liveIdentity, readFileIdentity(liveDbPath))) {
309
+ refuse("live_db_identity_changed_before_json_read");
310
+ }
311
+ if (aliasesIdentity(liveIdentity, identity))
312
+ refuse(`${code}_aliases_live_db`);
313
+ testHooks.onJsonDescriptorRead?.(code, resolved);
314
+ bytes = readDescriptorBytes(descriptor, identity, code);
315
+ }
316
+ finally {
317
+ fs.closeSync(descriptor);
318
+ }
319
+ if (sha256Bytes(bytes) !== expectedSha256)
320
+ refuse(`${code}_sha256_mismatch`);
321
+ try {
322
+ return { path: resolved, value: JSON.parse(bytes.toString("utf8")) };
323
+ }
324
+ catch {
325
+ refuse(`${code}_invalid_json`);
326
+ }
327
+ }
328
+ function lstatExists(inputPath) {
329
+ try {
330
+ fs.lstatSync(inputPath);
331
+ return true;
332
+ }
333
+ catch (error) {
334
+ if (error.code === "ENOENT")
335
+ return false;
336
+ throw error;
337
+ }
338
+ }
339
+ function safeOutputLeaf(fileName) {
340
+ if (fileName.length === 0 ||
341
+ fileName === "." ||
342
+ fileName === ".." ||
343
+ /[<>:"/\\|?*]/u.test(fileName) ||
344
+ /[. ]$/u.test(fileName)) {
345
+ return false;
346
+ }
347
+ const stem = fileName.split(".", 1)[0] ?? "";
348
+ return !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/iu.test(stem);
349
+ }
350
+ function approvedOutputPath(approvedStatePath, outputPath, existingCode) {
351
+ if (outputPath.length === 0)
352
+ refuse("output_path_invalid");
353
+ const leaf = path.basename(outputPath);
354
+ if (!safeOutputLeaf(leaf))
355
+ refuse("output_leaf_unsafe");
356
+ const parent = path.dirname(path.resolve(outputPath));
357
+ const resolvedParent = existingDirectory(parent, "output_parent_invalid");
358
+ if (!samePath(resolvedParent, approvedStatePath))
359
+ refuse("output_not_direct_state_child");
360
+ const resolvedOutput = path.join(resolvedParent, leaf);
361
+ if (lstatExists(resolvedOutput))
362
+ refuse(existingCode);
363
+ return resolvedOutput;
364
+ }
365
+ function assertExactRepositoryMap(approved, actual) {
366
+ if (approved.length !== actual.length)
367
+ refuse("repository_map_content_mismatch");
368
+ const byWorkspace = new Map(actual.map((entry) => [entry.workspaceId, entry]));
369
+ for (const entry of approved) {
370
+ const found = byWorkspace.get(entry.workspaceId);
371
+ if (found === undefined ||
372
+ found.owner !== entry.owner ||
373
+ found.repo !== entry.repo ||
374
+ found.reportAlias !== entry.reportAlias) {
375
+ refuse("repository_map_content_mismatch");
376
+ }
377
+ }
378
+ }
379
+ /**
380
+ * Load the exact approval/preparation bundle without opening or reading the live database.
381
+ * Only the approved scratch database is opened, read-only and with SQLite query_only enabled.
382
+ */
383
+ export async function loadApprovedEvidenceInput(input, testHooks = {}) {
384
+ const repositoryRoot = existingDirectory(input.repositoryRoot, "repository_root_invalid");
385
+ const liveDbPath = existingFile(input.liveDbPath, "live_db_path_unknown");
386
+ const liveIdentity = readFileIdentity(liveDbPath);
387
+ const scratchDbPath = existingFile(input.scratchDbPath, "scratch_db_missing_or_not_file");
388
+ const scratchStatePath = existingDirectory(input.scratchStatePath, "scratch_state_missing_or_not_directory");
389
+ const repoMapPath = existingFile(input.repoMapPath, "repository_map_missing_or_not_file");
390
+ if (within(repositoryRoot, scratchDbPath) ||
391
+ within(repositoryRoot, scratchStatePath) ||
392
+ within(repositoryRoot, repoMapPath)) {
393
+ refuse("approved_artifact_inside_repository");
394
+ }
395
+ if (!samePath(path.dirname(repoMapPath), scratchStatePath)) {
396
+ refuse("repository_map_not_direct_state_child");
397
+ }
398
+ if (samePath(liveDbPath, scratchDbPath))
399
+ refuse("scratch_aliases_live_db");
400
+ const scratchIdentity = readFileIdentity(scratchDbPath);
401
+ if (aliasesIdentity(liveIdentity, scratchIdentity))
402
+ refuse("scratch_aliases_live_db");
403
+ const approvalRead = readDigestBoundJson(input.approvalManifestPath, input.approvalManifestSha256, "approval_manifest", liveDbPath, liveIdentity, testHooks);
404
+ const preparationRead = readDigestBoundJson(input.preparationManifestPath, input.preparationManifestSha256, "preparation_manifest", liveDbPath, liveIdentity, testHooks);
405
+ const repoMapRead = readDigestBoundJson(repoMapPath, input.repoMapSha256, "repository_map", liveDbPath, liveIdentity, testHooks);
406
+ const approval = parseApproval(approvalRead.value);
407
+ const preparation = parsePreparation(preparationRead.value);
408
+ const repositories = parseRepositoryMap(repoMapRead.value);
409
+ if (within(repositoryRoot, approvalRead.path) || within(repositoryRoot, preparationRead.path)) {
410
+ refuse("approved_artifact_inside_repository");
411
+ }
412
+ if (!samePath(path.dirname(preparationRead.path), scratchStatePath) ||
413
+ path.basename(preparationRead.path) !== "preparation-manifest.json") {
414
+ refuse("preparation_manifest_not_direct_state_child");
415
+ }
416
+ if (path.basename(repoMapPath) !== preparation.repositoryMap.file) {
417
+ refuse("repository_map_file_binding_mismatch");
418
+ }
419
+ if (!samePath(liveDbPath, existingFile(approval.sourceDbPath, "approval_source_path_unresolvable")) ||
420
+ !samePath(liveDbPath, existingFile(preparation.source.path, "preparation_source_path_unresolvable")) ||
421
+ !samePath(scratchDbPath, existingFile(approval.scratchDbPath, "approval_scratch_path_unresolvable")) ||
422
+ !samePath(scratchDbPath, existingFile(preparation.scratch.path, "preparation_scratch_path_unresolvable")) ||
423
+ !samePath(scratchStatePath, existingDirectory(approval.scratchStatePath, "approval_state_path_unresolvable")) ||
424
+ !samePath(scratchStatePath, existingDirectory(preparation.state.path, "preparation_state_path_unresolvable"))) {
425
+ refuse("approved_path_binding_mismatch");
426
+ }
427
+ if (!exactIdentity(liveIdentity, preparation.source.identity)) {
428
+ refuse("live_db_identity_mismatch");
429
+ }
430
+ if (!exactIdentity(scratchIdentity, preparation.scratch.identity)) {
431
+ refuse("scratch_db_identity_mismatch");
432
+ }
433
+ if (preparation.approval.sha256 !== input.approvalManifestSha256 ||
434
+ preparation.scratch.sha256 !== input.scratchDbSha256 ||
435
+ preparation.repositoryMap.sha256 !== input.repoMapSha256) {
436
+ refuse("preparation_digest_binding_mismatch");
437
+ }
438
+ if (preparation.repositoryMap.mappedWorkspaceN !== repositories.length) {
439
+ refuse("repository_map_count_mismatch");
440
+ }
441
+ assertExactRepositoryMap(approval.repositories, repositories);
442
+ if (!SHA256_RE.test(input.scratchDbSha256))
443
+ refuse("scratch_db_sha256_invalid");
444
+ const scratchVerificationPath = approvedOutputPath(scratchStatePath, input.scratchVerificationPath, "scratch_verification_already_exists");
445
+ const scratchDescriptor = fs.openSync(scratchDbPath, "r");
446
+ let verificationDescriptor;
447
+ let verificationIdentity;
448
+ let scratchVerificationPublished = false;
449
+ try {
450
+ const openedScratchIdentity = descriptorIdentity(scratchDescriptor, "scratch_db_missing_or_not_file");
451
+ if (aliasesIdentity(liveIdentity, openedScratchIdentity))
452
+ refuse("scratch_aliases_live_db");
453
+ if (!exactIdentity(scratchIdentity, openedScratchIdentity)) {
454
+ refuse("scratch_db_identity_changed_before_verification");
455
+ }
456
+ testHooks.beforeScratchVerificationLink?.();
457
+ try {
458
+ fs.linkSync(scratchDbPath, scratchVerificationPath);
459
+ }
460
+ catch (error) {
461
+ if (error.code === "EEXIST") {
462
+ refuse("scratch_verification_already_exists");
463
+ }
464
+ refuse("scratch_verification_publish_failed");
465
+ }
466
+ scratchVerificationPublished = true;
467
+ testHooks.afterScratchVerificationLink?.();
468
+ testHooks.beforeScratchVerificationOpen?.();
469
+ try {
470
+ verificationDescriptor = fs.openSync(scratchVerificationPath, "r");
471
+ }
472
+ catch {
473
+ refuse("scratch_verification_open_failed");
474
+ }
475
+ verificationIdentity = descriptorIdentity(verificationDescriptor, "scratch_verification_not_file");
476
+ if (aliasesIdentity(liveIdentity, verificationIdentity) ||
477
+ !exactIdentity(openedScratchIdentity, verificationIdentity) ||
478
+ !exactIdentity(openedScratchIdentity, descriptorIdentity(scratchDescriptor, "scratch_db_changed_before_verification"))) {
479
+ refuse("scratch_verification_identity_mismatch");
480
+ }
481
+ const snapshotBytes = readDescriptorBytes(scratchDescriptor, openedScratchIdentity, "scratch_snapshot");
482
+ const snapshotSha256 = sha256Bytes(snapshotBytes);
483
+ if (snapshotSha256 !== input.scratchDbSha256) {
484
+ refuse("scratch_db_sha256_mismatch");
485
+ }
486
+ const openVerifiedScratchDb = queryOnlySnapshotFactory(snapshotBytes);
487
+ testHooks.beforeSnapshotIntegrity?.();
488
+ let scratch;
489
+ try {
490
+ scratch = openVerifiedScratchDb();
491
+ assertDatabaseIntegrity(scratch);
492
+ testHooks.afterSnapshotIntegrity?.();
493
+ }
494
+ catch (error) {
495
+ if (error instanceof EvidenceBoundaryError)
496
+ throw error;
497
+ refuse("scratch_db_integrity_failed");
498
+ }
499
+ finally {
500
+ scratch?.close();
501
+ }
502
+ if (!exactIdentity(readFileIdentity(liveDbPath), liveIdentity)) {
503
+ refuse("live_db_identity_changed_during_verification");
504
+ }
505
+ if (!exactIdentity(lstatFileIdentity(scratchVerificationPath, "scratch_verification_leaf_invalid"), verificationIdentity)) {
506
+ refuse("scratch_verification_identity_changed");
507
+ }
508
+ return {
509
+ approvalManifestPath: approvalRead.path,
510
+ approvalManifestSha256: input.approvalManifestSha256,
511
+ preparationManifestPath: preparationRead.path,
512
+ preparationManifestSha256: input.preparationManifestSha256,
513
+ approvedScratchDbPath: scratchDbPath,
514
+ scratchDbSha256: input.scratchDbSha256,
515
+ scratchVerificationPath,
516
+ scratchStatePath,
517
+ repoMapPath,
518
+ repoMapSha256: input.repoMapSha256,
519
+ liveDbPath,
520
+ repositoryRoot,
521
+ repositories,
522
+ openVerifiedScratchDb,
523
+ };
524
+ }
525
+ catch (error) {
526
+ if (!scratchVerificationPublished)
527
+ throw error;
528
+ const code = error instanceof EvidenceBoundaryError
529
+ ? error.code
530
+ : "scratch_verification_unexpected_failure";
531
+ throw new EvidenceBoundaryError(`${code}_scratch_verification_retained`);
532
+ }
533
+ finally {
534
+ let closeFailed = false;
535
+ try {
536
+ if (verificationDescriptor !== undefined)
537
+ fs.closeSync(verificationDescriptor);
538
+ }
539
+ catch {
540
+ closeFailed = true;
541
+ }
542
+ try {
543
+ fs.closeSync(scratchDescriptor);
544
+ }
545
+ catch {
546
+ closeFailed = true;
547
+ }
548
+ if (closeFailed) {
549
+ refuse(scratchVerificationPublished
550
+ ? "scratch_descriptor_close_failed_scratch_verification_retained"
551
+ : "scratch_descriptor_close_failed");
552
+ }
553
+ }
554
+ }
555
+ /** Atomically create and verify one new private artifact. A failed partial is retained for manual cleanup. */
556
+ export function publishApprovedOutput(approved, outputPath, value, testHooks = {}) {
557
+ const resolvedOutput = approvedOutputPath(approved.scratchStatePath, outputPath, "output_already_exists");
558
+ const expectedBytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
559
+ const expectedSha256 = sha256Bytes(expectedBytes);
560
+ const temporaryPath = path.join(approved.scratchStatePath, `.evidence-output-${randomUUID()}.tmp`);
561
+ let temporaryDescriptor;
562
+ try {
563
+ temporaryDescriptor = fs.openSync(temporaryPath, "wx+", 0o600);
564
+ }
565
+ catch {
566
+ refuse("output_temporary_create_failed");
567
+ }
568
+ let publishedDescriptor;
569
+ let outputPublished = false;
570
+ try {
571
+ fs.writeFileSync(temporaryDescriptor, expectedBytes);
572
+ fs.fsyncSync(temporaryDescriptor);
573
+ const identity = descriptorIdentity(temporaryDescriptor, "output_temporary_not_file");
574
+ const temporaryBytes = readDescriptorBytes(temporaryDescriptor, identity, "output_temporary");
575
+ if (sha256Bytes(temporaryBytes) !== expectedSha256) {
576
+ refuse("output_temporary_digest_mismatch");
577
+ }
578
+ testHooks.beforePublishLink?.(temporaryPath, resolvedOutput);
579
+ try {
580
+ fs.linkSync(temporaryPath, resolvedOutput);
581
+ }
582
+ catch (error) {
583
+ if (error.code === "EEXIST")
584
+ refuse("output_already_exists");
585
+ refuse("output_publish_failed");
586
+ }
587
+ outputPublished = true;
588
+ testHooks.afterPublishLink?.(temporaryPath, resolvedOutput);
589
+ publishedDescriptor = fs.openSync(resolvedOutput, "r");
590
+ const publishedIdentity = descriptorIdentity(publishedDescriptor, "output_not_file");
591
+ if (!exactIdentity(identity, publishedIdentity)) {
592
+ refuse("output_identity_mismatch");
593
+ }
594
+ const publishedBytes = readDescriptorBytes(publishedDescriptor, publishedIdentity, "output_published");
595
+ if (sha256Bytes(publishedBytes) !== expectedSha256) {
596
+ refuse("output_digest_mismatch");
597
+ }
598
+ if (!exactIdentity(lstatFileIdentity(resolvedOutput, "output_leaf_changed"), publishedIdentity)) {
599
+ refuse("output_identity_changed");
600
+ }
601
+ fs.closeSync(publishedDescriptor);
602
+ publishedDescriptor = undefined;
603
+ fs.closeSync(temporaryDescriptor);
604
+ temporaryDescriptor = undefined;
605
+ fs.unlinkSync(temporaryPath);
606
+ return { path: resolvedOutput, sha256: expectedSha256, identity: publishedIdentity };
607
+ }
608
+ catch (error) {
609
+ const code = error instanceof EvidenceBoundaryError ? error.code : "output_unexpected_failure";
610
+ return refuse(`${code}_${outputPublished ? "temporary_and_output_retained" : "temporary_retained"}`);
611
+ }
612
+ finally {
613
+ let closeFailed = false;
614
+ try {
615
+ if (publishedDescriptor !== undefined)
616
+ fs.closeSync(publishedDescriptor);
617
+ }
618
+ catch {
619
+ closeFailed = true;
620
+ }
621
+ try {
622
+ if (temporaryDescriptor !== undefined)
623
+ fs.closeSync(temporaryDescriptor);
624
+ }
625
+ catch {
626
+ closeFailed = true;
627
+ }
628
+ if (closeFailed) {
629
+ refuse(`output_descriptor_close_failed_${outputPublished ? "temporary_and_output_retained" : "temporary_retained"}`);
630
+ }
631
+ }
632
+ }