@tiangong-ai/cli 0.0.60 → 0.0.62

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 (239) hide show
  1. package/AGENTS.md +17 -6
  2. package/README.md +577 -55
  3. package/dist/data/builtins.js +33 -5
  4. package/dist/data/builtins.js.map +1 -1
  5. package/dist/data/catalog.js +46 -0
  6. package/dist/data/catalog.js.map +1 -1
  7. package/dist/data/commands.js +15 -1
  8. package/dist/data/commands.js.map +1 -1
  9. package/dist/data/connectors/airnow-hourly-observations.js +60 -10
  10. package/dist/data/connectors/airnow-hourly-observations.js.map +1 -1
  11. package/dist/data/connectors/epa-eis-records.js +16 -2
  12. package/dist/data/connectors/epa-eis-records.js.map +1 -1
  13. package/dist/data/connectors/gdelt-doc-search.js +54 -18
  14. package/dist/data/connectors/gdelt-doc-search.js.map +1 -1
  15. package/dist/data/connectors/gdelt-file-feeds.js +20 -11
  16. package/dist/data/connectors/gdelt-file-feeds.js.map +1 -1
  17. package/dist/data/connectors/gdelt-web-ngrams.d.ts +2 -0
  18. package/dist/data/connectors/gdelt-web-ngrams.js +350 -0
  19. package/dist/data/connectors/gdelt-web-ngrams.js.map +1 -0
  20. package/dist/data/connectors/gdelt-web-ngrams.schemas.d.ts +188 -0
  21. package/dist/data/connectors/gdelt-web-ngrams.schemas.js +138 -0
  22. package/dist/data/connectors/gdelt-web-ngrams.schemas.js.map +1 -0
  23. package/dist/data/connectors/nasa-firms-fire.js +4 -0
  24. package/dist/data/connectors/nasa-firms-fire.js.map +1 -1
  25. package/dist/data/connectors/open-meteo-air-quality.js +27 -22
  26. package/dist/data/connectors/open-meteo-air-quality.js.map +1 -1
  27. package/dist/data/connectors/open-meteo-air-quality.schemas.d.ts +4 -1
  28. package/dist/data/connectors/open-meteo-air-quality.schemas.js +21 -1
  29. package/dist/data/connectors/open-meteo-air-quality.schemas.js.map +1 -1
  30. package/dist/data/connectors/open-meteo-flood.js +28 -23
  31. package/dist/data/connectors/open-meteo-flood.js.map +1 -1
  32. package/dist/data/connectors/open-meteo-flood.schemas.d.ts +4 -1
  33. package/dist/data/connectors/open-meteo-flood.schemas.js +21 -1
  34. package/dist/data/connectors/open-meteo-flood.schemas.js.map +1 -1
  35. package/dist/data/connectors/open-meteo-historical-weather.js +26 -21
  36. package/dist/data/connectors/open-meteo-historical-weather.js.map +1 -1
  37. package/dist/data/connectors/open-meteo-historical-weather.schemas.d.ts +4 -1
  38. package/dist/data/connectors/open-meteo-historical-weather.schemas.js +21 -1
  39. package/dist/data/connectors/open-meteo-historical-weather.schemas.js.map +1 -1
  40. package/dist/data/connectors/openaq-air-quality.js +50 -12
  41. package/dist/data/connectors/openaq-air-quality.js.map +1 -1
  42. package/dist/data/connectors/openaq-air-quality.schemas.d.ts +0 -4
  43. package/dist/data/connectors/openaq-air-quality.schemas.js +2 -2
  44. package/dist/data/connectors/openaq-air-quality.schemas.js.map +1 -1
  45. package/dist/data/connectors/youtube-public-content.js +76 -6
  46. package/dist/data/connectors/youtube-public-content.js.map +1 -1
  47. package/dist/data/connectors/youtube-public-content.schemas.d.ts +63 -3
  48. package/dist/data/connectors/youtube-public-content.schemas.js +56 -2
  49. package/dist/data/connectors/youtube-public-content.schemas.js.map +1 -1
  50. package/dist/data/contracts.d.ts +22 -1
  51. package/dist/data/runtime/bounded-http.d.ts +2 -0
  52. package/dist/data/runtime/bounded-http.js +221 -59
  53. package/dist/data/runtime/bounded-http.js.map +1 -1
  54. package/dist/data/runtime/execute.d.ts +2 -0
  55. package/dist/data/runtime/execute.js +82 -60
  56. package/dist/data/runtime/execute.js.map +1 -1
  57. package/dist/data/runtime/http-timing.d.ts +15 -0
  58. package/dist/data/runtime/http-timing.js +59 -0
  59. package/dist/data/runtime/http-timing.js.map +1 -0
  60. package/dist/data/runtime/http-transport.d.ts +1 -0
  61. package/dist/data/runtime/http-transport.js +27 -0
  62. package/dist/data/runtime/http-transport.js.map +1 -0
  63. package/dist/data/schemas/catalog.v1.json +46 -0
  64. package/dist/data/schemas/discovery.v1.json +12 -0
  65. package/dist/data/schemas/error.v1.json +1 -0
  66. package/dist/data/schemas/manifest.v1.json +19 -1
  67. package/dist/research/orchestration.js +341 -28
  68. package/dist/research/orchestration.js.map +1 -1
  69. package/dist/research/setup-command.js +37 -6
  70. package/dist/research/setup-command.js.map +1 -1
  71. package/dist/research/workspace/acquisition-routes.d.ts +1 -1
  72. package/dist/research/workspace/acquisition-routes.js +2 -11
  73. package/dist/research/workspace/acquisition-routes.js.map +1 -1
  74. package/dist/research/workspace/artifact-read-audit.d.ts +16 -0
  75. package/dist/research/workspace/artifact-read-audit.js +176 -0
  76. package/dist/research/workspace/artifact-read-audit.js.map +1 -0
  77. package/dist/research/workspace/artifact-trace.d.ts +12 -0
  78. package/dist/research/workspace/artifact-trace.js +109 -0
  79. package/dist/research/workspace/artifact-trace.js.map +1 -0
  80. package/dist/research/workspace/artifact-view-mcp.d.ts +75 -0
  81. package/dist/research/workspace/artifact-view-mcp.js +214 -0
  82. package/dist/research/workspace/artifact-view-mcp.js.map +1 -0
  83. package/dist/research/workspace/artifact-views.d.ts +100 -0
  84. package/dist/research/workspace/artifact-views.js +395 -0
  85. package/dist/research/workspace/artifact-views.js.map +1 -0
  86. package/dist/research/workspace/audit-bundle.d.ts +4 -0
  87. package/dist/research/workspace/audit-bundle.js +130 -11
  88. package/dist/research/workspace/audit-bundle.js.map +1 -1
  89. package/dist/research/workspace/broker.js +25 -4
  90. package/dist/research/workspace/broker.js.map +1 -1
  91. package/dist/research/workspace/constants.d.ts +1 -0
  92. package/dist/research/workspace/constants.js +1 -0
  93. package/dist/research/workspace/constants.js.map +1 -1
  94. package/dist/research/workspace/content-evidence.d.ts +1 -0
  95. package/dist/research/workspace/content-evidence.js +1 -1
  96. package/dist/research/workspace/content-evidence.js.map +1 -1
  97. package/dist/research/workspace/credentials.js +2 -1
  98. package/dist/research/workspace/credentials.js.map +1 -1
  99. package/dist/research/workspace/data-evidence-adapter.d.ts +71 -0
  100. package/dist/research/workspace/data-evidence-adapter.js +269 -59
  101. package/dist/research/workspace/data-evidence-adapter.js.map +1 -1
  102. package/dist/research/workspace/data-evidence-view.d.ts +58 -0
  103. package/dist/research/workspace/data-evidence-view.js +411 -0
  104. package/dist/research/workspace/data-evidence-view.js.map +1 -0
  105. package/dist/research/workspace/evidence-ledger.js +32 -29
  106. package/dist/research/workspace/evidence-ledger.js.map +1 -1
  107. package/dist/research/workspace/evidence.d.ts +28 -0
  108. package/dist/research/workspace/evidence.js +48 -0
  109. package/dist/research/workspace/evidence.js.map +1 -1
  110. package/dist/research/workspace/executor.d.ts +32 -2
  111. package/dist/research/workspace/executor.js +286 -42
  112. package/dist/research/workspace/executor.js.map +1 -1
  113. package/dist/research/workspace/investigation-attempt.d.ts +159 -0
  114. package/dist/research/workspace/investigation-attempt.js +619 -0
  115. package/dist/research/workspace/investigation-attempt.js.map +1 -0
  116. package/dist/research/workspace/investigation-audit.d.ts +20 -0
  117. package/dist/research/workspace/investigation-audit.js +306 -0
  118. package/dist/research/workspace/investigation-audit.js.map +1 -0
  119. package/dist/research/workspace/investigation-candidate.d.ts +84 -0
  120. package/dist/research/workspace/investigation-candidate.js +156 -0
  121. package/dist/research/workspace/investigation-candidate.js.map +1 -0
  122. package/dist/research/workspace/investigation-certification.d.ts +39 -0
  123. package/dist/research/workspace/investigation-certification.js +152 -0
  124. package/dist/research/workspace/investigation-certification.js.map +1 -0
  125. package/dist/research/workspace/investigation-close.d.ts +47 -0
  126. package/dist/research/workspace/investigation-close.js +153 -0
  127. package/dist/research/workspace/investigation-close.js.map +1 -0
  128. package/dist/research/workspace/investigation-lineage.d.ts +8 -0
  129. package/dist/research/workspace/investigation-lineage.js +10 -0
  130. package/dist/research/workspace/investigation-lineage.js.map +1 -0
  131. package/dist/research/workspace/investigation-observer.d.ts +22 -0
  132. package/dist/research/workspace/investigation-observer.js +122 -0
  133. package/dist/research/workspace/investigation-observer.js.map +1 -0
  134. package/dist/research/workspace/investigation-promotion.d.ts +129 -0
  135. package/dist/research/workspace/investigation-promotion.js +316 -0
  136. package/dist/research/workspace/investigation-promotion.js.map +1 -0
  137. package/dist/research/workspace/investigation-requirements.d.ts +5 -0
  138. package/dist/research/workspace/investigation-requirements.js +32 -0
  139. package/dist/research/workspace/investigation-requirements.js.map +1 -0
  140. package/dist/research/workspace/investigation-resources.d.ts +12 -0
  141. package/dist/research/workspace/investigation-resources.js +78 -0
  142. package/dist/research/workspace/investigation-resources.js.map +1 -0
  143. package/dist/research/workspace/investigation-source-export.d.ts +6 -0
  144. package/dist/research/workspace/investigation-source-export.js +68 -0
  145. package/dist/research/workspace/investigation-source-export.js.map +1 -0
  146. package/dist/research/workspace/investigation-store.d.ts +9 -0
  147. package/dist/research/workspace/investigation-store.js +55 -0
  148. package/dist/research/workspace/investigation-store.js.map +1 -0
  149. package/dist/research/workspace/investigation.d.ts +424 -0
  150. package/dist/research/workspace/investigation.js +716 -0
  151. package/dist/research/workspace/investigation.js.map +1 -0
  152. package/dist/research/workspace/native-process-supervisor.d.ts +1 -0
  153. package/dist/research/workspace/native-process-supervisor.js +97 -0
  154. package/dist/research/workspace/native-process-supervisor.js.map +1 -0
  155. package/dist/research/workspace/native-run.d.ts +150 -0
  156. package/dist/research/workspace/native-run.js +879 -0
  157. package/dist/research/workspace/native-run.js.map +1 -0
  158. package/dist/research/workspace/preflight.d.ts +11 -2
  159. package/dist/research/workspace/preflight.js +28 -20
  160. package/dist/research/workspace/preflight.js.map +1 -1
  161. package/dist/research/workspace/project-authority.js +2 -0
  162. package/dist/research/workspace/project-authority.js.map +1 -1
  163. package/dist/research/workspace/project-budget.d.ts +41 -0
  164. package/dist/research/workspace/project-budget.js +265 -0
  165. package/dist/research/workspace/project-budget.js.map +1 -0
  166. package/dist/research/workspace/project-mutations.d.ts +1 -1
  167. package/dist/research/workspace/project-mutations.js +29 -3
  168. package/dist/research/workspace/project-mutations.js.map +1 -1
  169. package/dist/research/workspace/projects.d.ts +66 -0
  170. package/dist/research/workspace/projects.js +182 -3
  171. package/dist/research/workspace/projects.js.map +1 -1
  172. package/dist/research/workspace/provider-budget.d.ts +3 -0
  173. package/dist/research/workspace/provider-budget.js +45 -0
  174. package/dist/research/workspace/provider-budget.js.map +1 -0
  175. package/dist/research/workspace/provider-state.d.ts +1 -0
  176. package/dist/research/workspace/provider-state.js +20 -0
  177. package/dist/research/workspace/provider-state.js.map +1 -0
  178. package/dist/research/workspace/publication-lineage.d.ts +34 -0
  179. package/dist/research/workspace/publication-lineage.js +165 -0
  180. package/dist/research/workspace/publication-lineage.js.map +1 -0
  181. package/dist/research/workspace/publication-workflow.d.ts +24 -0
  182. package/dist/research/workspace/publication-workflow.js +118 -14
  183. package/dist/research/workspace/publication-workflow.js.map +1 -1
  184. package/dist/research/workspace/request-provenance.d.ts +67 -0
  185. package/dist/research/workspace/request-provenance.js +115 -0
  186. package/dist/research/workspace/request-provenance.js.map +1 -0
  187. package/dist/research/workspace/review-executor.d.ts +6 -1
  188. package/dist/research/workspace/review-executor.js +39 -14
  189. package/dist/research/workspace/review-executor.js.map +1 -1
  190. package/dist/research/workspace/runtime.d.ts +64 -1
  191. package/dist/research/workspace/runtime.js +359 -112
  192. package/dist/research/workspace/runtime.js.map +1 -1
  193. package/dist/research/workspace/sanitization.js +1 -1
  194. package/dist/research/workspace/sanitization.js.map +1 -1
  195. package/dist/research/workspace/schema-compatibility.d.ts +5 -0
  196. package/dist/research/workspace/schema-compatibility.js +55 -0
  197. package/dist/research/workspace/schema-compatibility.js.map +1 -0
  198. package/dist/research/workspace/scientific-amendment.d.ts +103 -0
  199. package/dist/research/workspace/scientific-amendment.js +537 -0
  200. package/dist/research/workspace/scientific-amendment.js.map +1 -0
  201. package/dist/research/workspace/scientific-design.js +8 -17
  202. package/dist/research/workspace/scientific-design.js.map +1 -1
  203. package/dist/research/workspace/scientific-fulfillment-audit.d.ts +21 -0
  204. package/dist/research/workspace/scientific-fulfillment-audit.js +217 -0
  205. package/dist/research/workspace/scientific-fulfillment-audit.js.map +1 -0
  206. package/dist/research/workspace/scientific-fulfillment.d.ts +130 -0
  207. package/dist/research/workspace/scientific-fulfillment.js +553 -0
  208. package/dist/research/workspace/scientific-fulfillment.js.map +1 -0
  209. package/dist/research/workspace/scientific-review-execution.js +236 -75
  210. package/dist/research/workspace/scientific-review-execution.js.map +1 -1
  211. package/dist/research/workspace/scientific-review.d.ts +6 -1
  212. package/dist/research/workspace/scientific-review.js +228 -52
  213. package/dist/research/workspace/scientific-review.js.map +1 -1
  214. package/dist/research/workspace/setup-catalog.js +3 -3
  215. package/dist/research/workspace/setup-release.d.ts +22 -0
  216. package/dist/research/workspace/setup-release.js +426 -0
  217. package/dist/research/workspace/setup-release.js.map +1 -0
  218. package/dist/research/workspace/setup-runtime-integrity.d.ts +2 -0
  219. package/dist/research/workspace/setup-runtime-integrity.js +31 -0
  220. package/dist/research/workspace/setup-runtime-integrity.js.map +1 -0
  221. package/dist/research/workspace/setup-upgrade.d.ts +17 -0
  222. package/dist/research/workspace/setup-upgrade.js +784 -0
  223. package/dist/research/workspace/setup-upgrade.js.map +1 -0
  224. package/dist/research/workspace/setup.d.ts +31 -39
  225. package/dist/research/workspace/setup.js +220 -17
  226. package/dist/research/workspace/setup.js.map +1 -1
  227. package/dist/research/workspace/task-acceptance.d.ts +17 -2
  228. package/dist/research/workspace/task-acceptance.js +106 -10
  229. package/dist/research/workspace/task-acceptance.js.map +1 -1
  230. package/dist/research/workspace/task-audit.d.ts +5 -1
  231. package/dist/research/workspace/task-audit.js +70 -5
  232. package/dist/research/workspace/task-audit.js.map +1 -1
  233. package/dist/research/workspace/task-contract.d.ts +4 -1
  234. package/dist/research/workspace/task-contract.js +26 -5
  235. package/dist/research/workspace/task-contract.js.map +1 -1
  236. package/dist/research/workspace/types.d.ts +50 -3
  237. package/dist/research/workspace/workspace.js +34 -4
  238. package/dist/research/workspace/workspace.js.map +1 -1
  239. package/package.json +2 -1
@@ -0,0 +1,879 @@
1
+ import { investigationObserverRoute, inspectInvestigationObserver, } from "./investigation-observer.js";
2
+ import { investigationWallReservations } from "./investigation-resources.js";
3
+ import { createCalculationSandboxInvocation } from "./executor.js";
4
+ import { assertInvestigationCertificationRequired, prepareInvestigationCertification, frozenPromotionView, certificationAssessment, } from "./investigation-certification.js";
5
+ import { beginProjectMutation, prepareProjectMutation, projectMutationBinding, settleProjectMutation, } from "./project-mutations.js";
6
+ import { settleProjectCost } from "./project-budget.js";
7
+ import { Ajv2020 } from "ajv/dist/2020.js";
8
+ import { fork, spawn } from "node:child_process";
9
+ import { fileURLToPath } from "node:url";
10
+ import { createHash } from "node:crypto";
11
+ import { constants } from "node:fs";
12
+ import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, realpath } from "node:fs/promises";
13
+ import { arch, platform } from "node:os";
14
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
15
+ import { CliError } from "../../errors.js";
16
+ import { loadCurrentEvidenceSnapshot } from "./acquisition.js";
17
+ import { appendJournalEvent, readVerifiedJournal } from "./journal.js";
18
+ import { assertProjectAuthority, projectAuthorityIndex } from "./project-authority.js";
19
+ import { loadProject } from "./projects.js";
20
+ import { configuredResearchSecrets, sanitizeResearchText, sanitizeResearchValue, } from "./sanitization.js";
21
+ import { isContainedRelativePath, registerScientificObject } from "./scientific-objects.js";
22
+ import { canonicalJson, fileRecord, isObject, pathExists, resolveContained, sha256File, sha256Text, workspacePaths, } from "./storage.js";
23
+ import { loadProjectTask, readTaskObject, taskDirectory, taskRequirementSha256, writeTaskObject, } from "./task-contract.js";
24
+ import { loadWorkspaceConfig, withWorkspaceLock } from "./workspace.js";
25
+ const HASH = /^[a-f0-9]{64}$/;
26
+ const ID = "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$";
27
+ const statuses = [
28
+ "succeeded",
29
+ "failed",
30
+ "timed-out",
31
+ "output-limit-exceeded",
32
+ "cancelled",
33
+ "invalid-output",
34
+ "inputs-changed",
35
+ "stale",
36
+ ];
37
+ const inputSchema = {
38
+ type: "object",
39
+ additionalProperties: false,
40
+ required: [
41
+ "schemaVersion",
42
+ "runId",
43
+ "requirementId",
44
+ "requirementSha256",
45
+ "nativeSessionId",
46
+ "workingDirectory",
47
+ "runtime",
48
+ "scriptPath",
49
+ "environmentLockPath",
50
+ "inputs",
51
+ "outputs",
52
+ "arguments",
53
+ "timeoutSeconds",
54
+ ],
55
+ properties: {
56
+ schemaVersion: { const: 1 },
57
+ runId: { type: "string", pattern: ID },
58
+ requirementId: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" },
59
+ requirementSha256: { type: "string", pattern: HASH.source },
60
+ nativeSessionId: { type: ["string", "null"], minLength: 1 },
61
+ workingDirectory: { type: "string", minLength: 1 },
62
+ scriptPath: { type: "string", minLength: 1 },
63
+ environmentLockPath: { type: "string", minLength: 1 },
64
+ runtime: {
65
+ type: "object",
66
+ additionalProperties: false,
67
+ required: ["kind", "path"],
68
+ properties: { kind: { enum: ["node", "python"] }, path: { type: "string", minLength: 1 } },
69
+ },
70
+ inputs: {
71
+ type: "array",
72
+ minItems: 1,
73
+ maxItems: 256,
74
+ items: {
75
+ type: "object",
76
+ additionalProperties: false,
77
+ required: ["id", "artifactId", "sha256"],
78
+ properties: {
79
+ id: { type: "string", pattern: ID },
80
+ artifactId: { type: "string", pattern: ID },
81
+ sha256: { type: "string", pattern: HASH.source },
82
+ },
83
+ },
84
+ },
85
+ outputs: {
86
+ type: "array",
87
+ minItems: 1,
88
+ maxItems: 32,
89
+ items: {
90
+ type: "object",
91
+ additionalProperties: false,
92
+ required: ["id", "fileName", "mediaType"],
93
+ properties: {
94
+ id: { type: "string", pattern: ID },
95
+ fileName: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" },
96
+ mediaType: { type: "string", pattern: "^(?:text|application|image)/[a-z0-9.+-]+$" },
97
+ },
98
+ },
99
+ },
100
+ arguments: { type: "array", maxItems: 256, items: { type: "string", maxLength: 4000 } },
101
+ timeoutSeconds: { type: "integer", minimum: 1, maximum: 172800 },
102
+ investigationPromotionSha256: { type: "string", pattern: HASH.source },
103
+ },
104
+ };
105
+ const validateInput = new Ajv2020({ strict: false, allErrors: true }).compile(inputSchema);
106
+ export function nativeRunInputSchema() {
107
+ return structuredClone(inputSchema);
108
+ }
109
+ function error(message, code = "RESEARCH_NATIVE_RUN_INVALID", exitCode = 3) {
110
+ return new CliError(message, { code, exitCode });
111
+ }
112
+ function parseInput(value) {
113
+ if (!validateInput(value) ||
114
+ canonicalJson(sanitizeResearchValue(value, configuredResearchSecrets(process.env))) !==
115
+ canonicalJson(value))
116
+ throw error("Native run must match the closed secret-free schema; supply an ordinary calculation, not an agent launcher.", undefined, 2);
117
+ const input = value;
118
+ for (const names of [
119
+ input.inputs.map((item) => item.id),
120
+ input.outputs.map((item) => item.id),
121
+ input.outputs.map((item) => item.fileName),
122
+ ]) {
123
+ if (new Set(names).size !== names.length)
124
+ throw error("Native run inputs and outputs need distinct exact names.", undefined, 2);
125
+ }
126
+ for (const argument of input.arguments) {
127
+ if (/[\r\n\0]/u.test(argument) ||
128
+ /(?:^|=)(?:\/|[A-Za-z]:[\\/])/u.test(argument) ||
129
+ /--(?:api-key|auth-token|password|cookie)(?:=|$)/iu.test(argument))
130
+ throw error("Use exact input/output placeholders and non-secret arguments, not host paths or credentials.", undefined, 2);
131
+ for (const match of argument.matchAll(/\{(input|output):([^}]+)\}/gu)) {
132
+ const collection = match[1] === "input" ? input.inputs : input.outputs;
133
+ if (!collection.some((item) => item.id === match[2]))
134
+ throw error("Native run argument refers to an undeclared file.", undefined, 2);
135
+ }
136
+ if (/[{}]/u.test(argument.replace(/\{(?:input|output):[^}]+\}/gu, "")))
137
+ throw error("Native run contains an unsupported argument placeholder.", undefined, 2);
138
+ }
139
+ return input;
140
+ }
141
+ /** One explicitly requested non-AI calculation. It inherits the native host's OS
142
+ * restrictions; this observer is neither an agent scheduler nor a new sandbox. */
143
+ export async function observeNativeRun(root, projectId, value, confirm) {
144
+ if (!confirm)
145
+ throw error("Review the exact calculation and pass --confirm-execution; no program was started.", "RESEARCH_NATIVE_RUN_CONFIRMATION_REQUIRED", 2);
146
+ const input = parseInput(value);
147
+ const requestSha256 = sha256Text(canonicalJson(input));
148
+ try {
149
+ const prepared = await withWorkspaceLock(root, "research.task.run.prepare", async () => {
150
+ const project = await loadProject(root, projectId);
151
+ const events = await readVerifiedJournal(workspacePaths(root).journal);
152
+ assertProjectAuthority(project, projectAuthorityIndex(events));
153
+ const started = events.find((event) => event.scope === projectId &&
154
+ event.type === "project.task.run.started" &&
155
+ event.payload.runId === input.runId);
156
+ if (started) {
157
+ if (started.payload.requestSha256 !== requestSha256)
158
+ throw error("This run ID already names a different exact request. Use a new run ID for a new calculation.", "RESEARCH_NATIVE_RUN_CONFLICT");
159
+ const completed = completedRunEvent(events, projectId, input.runId);
160
+ if (!completed)
161
+ throw error("This run was started but has no committed result. Inspect the interrupted calculation; it will not be rerun automatically. Use a new run ID only for an explicitly authorized new attempt.", "RESEARCH_NATIVE_RUN_INCOMPLETE");
162
+ return {
163
+ replay: await readNativeRun(root, projectId, String(completed.payload.recordSha256)),
164
+ stagingDirectoryName: String(started.payload.stagingDirectoryName ?? ""),
165
+ };
166
+ }
167
+ await assertInvestigationCertificationRequired(root, projectId, input, events);
168
+ await assertRunWindow(root, project, input);
169
+ const config = await loadWorkspaceConfig(root);
170
+ const { wallSeconds: reservedWall } = await investigationWallReservations(root, projectId, events);
171
+ const timeoutSeconds = Math.min(input.timeoutSeconds, config.budget.packageMaxWallSeconds.analyze, config.budget.maxWallSeconds - project.usage.wallSeconds - reservedWall);
172
+ if (timeoutSeconds < input.timeoutSeconds)
173
+ throw error("The declared calculation timeout exceeds the remaining finite workspace time budget.");
174
+ const working = await exactDirectory(input.workingDirectory);
175
+ if (isContainedRelativePath(relative(await realpath(workspacePaths(root).control), working)))
176
+ throw error("Calculation staging must use an explicit directory outside the control store.");
177
+ if (!isAbsolute(input.runtime.path) || input.runtime.path !== resolve(input.runtime.path))
178
+ throw error("Select an absolute Node or Python interpreter path.");
179
+ const runtimePath = await realpath(input.runtime.path);
180
+ if (!(await lstat(runtimePath)).isFile())
181
+ throw error("The selected interpreter is not a regular executable file.");
182
+ const binarySha256 = await sha256File(runtimePath);
183
+ const certification = await prepareInvestigationCertification(root, project, input, binarySha256, events);
184
+ let reportedVersion = "";
185
+ if (!certification) {
186
+ const version = await captureProcess(input.runtime.path, ["--version"], working, { PATH: dirname(input.runtime.path) }, 5);
187
+ reportedVersion = version.stdout.trim() || version.stderr.trim();
188
+ if (version.exitCode !== 0 ||
189
+ !(input.runtime.kind === "node" ? /^v\d+\.\d+\.\d+$/u : /^Python \d+\.\d+\.\d+$/u).test(reportedVersion))
190
+ throw error("The selected executable is not the declared ordinary Node/Python runtime. Agent CLI launchers are not supported.");
191
+ }
192
+ const acquisition = await loadCurrentEvidenceSnapshot(root, projectId);
193
+ const selected = input.inputs.map((item) => {
194
+ const artifact = acquisition.artifacts.find((artifact) => artifact.artifactId === item.artifactId && artifact.sha256 === item.sha256);
195
+ if (!artifact)
196
+ throw error("Calculation input must name an exact artifact in the current frozen acquisition.");
197
+ return { ...item, artifact };
198
+ });
199
+ const script = await registerScientificObject({
200
+ root,
201
+ objectKind: "model-implementation",
202
+ path: input.scriptPath,
203
+ mediaType: input.runtime.kind === "node" ? "text/javascript" : "text/x-python",
204
+ });
205
+ const environment = await registerScientificObject({
206
+ root,
207
+ objectKind: "environment-lock",
208
+ path: input.environmentLockPath,
209
+ mediaType: input.environmentLockPath.endsWith(".json") ? "application/json" : "text/plain",
210
+ });
211
+ const staging = await mkdtemp(join(working, `.tiangong-run-${input.runId}-`));
212
+ const privateHome = join(staging, "home");
213
+ await mkdir(privateHome, { mode: 0o700 });
214
+ const scriptFile = join(staging, certification
215
+ ? input.runtime.kind === "node"
216
+ ? "program.mjs"
217
+ : "program.py"
218
+ : basename(input.scriptPath));
219
+ await copyExact(resolveContained(workspacePaths(root).control, script.objectLocator), scriptFile, script.sha256);
220
+ const runInputs = [];
221
+ for (const item of selected) {
222
+ const source = resolveContained(workspacePaths(root).control, item.artifact.locator);
223
+ const executionPath = join(staging, `input-${item.id}`);
224
+ await copyExact(source, executionPath, item.sha256);
225
+ runInputs.push({
226
+ id: item.id,
227
+ artifactId: item.artifactId,
228
+ ...(await storeRunObject(root, projectId, executionPath)),
229
+ executionPath,
230
+ });
231
+ }
232
+ const outputPaths = new Map(input.outputs.map((item) => [item.id, join(staging, item.fileName)]));
233
+ if (new Set([
234
+ ...runInputs.map((item) => item.executionPath),
235
+ scriptFile,
236
+ privateHome,
237
+ ...outputPaths.values(),
238
+ ]).size !==
239
+ runInputs.length + outputPaths.size + 2)
240
+ throw error("Declared outputs collide with protected calculation inputs.");
241
+ const args = input.arguments.map((argument) => argument.replace(/\{(input|output):([^}]+)\}/gu, (_match, kind, id) => kind === "input"
242
+ ? runInputs.find((item) => item.id === id).executionPath
243
+ : outputPaths.get(id)));
244
+ const nativePacketSha256 = await nativePacketBinding(root, project, input.nativeSessionId);
245
+ const sourceScript = await storeRunObject(root, projectId, scriptFile);
246
+ const environmentLock = await storeRunObject(root, projectId, resolveContained(workspacePaths(root).control, environment.objectLocator));
247
+ if (certification &&
248
+ (sourceScript.sha256 !== certification.promotion.plan.recipe.script.sha256 ||
249
+ environmentLock.sha256 !== certification.promotion.plan.recipe.environmentLock.sha256))
250
+ throw error("The staged certification bytes changed after promotion scope validation.", "RESEARCH_INVESTIGATION_CERTIFICATION_INVALID");
251
+ const invocation = certification
252
+ ? await createCalculationSandboxInvocation({
253
+ binary: runtimePath,
254
+ args: [scriptFile, ...args],
255
+ capsuleRoot: staging,
256
+ workspaceRoot: root,
257
+ })
258
+ : null;
259
+ const versionInvocation = certification
260
+ ? await createCalculationSandboxInvocation({
261
+ binary: runtimePath,
262
+ args: ["--version"],
263
+ capsuleRoot: staging,
264
+ workspaceRoot: root,
265
+ })
266
+ : null;
267
+ await appendJournalEvent(workspacePaths(root).journal, "project.task.run.started", projectId, {
268
+ runId: input.runId,
269
+ requestSha256,
270
+ requirementId: input.requirementId,
271
+ requirementSha256: input.requirementSha256,
272
+ scriptSha256: sourceScript.sha256,
273
+ environmentLockSha256: environmentLock.sha256,
274
+ runtimeBinarySha256: binarySha256,
275
+ nativePacketSha256,
276
+ stagingDirectoryName: basename(staging),
277
+ ...(certification
278
+ ? {
279
+ investigationPromotionSha256: certification.promotion.recordSha256,
280
+ certificationReservedWallSeconds: timeoutSeconds,
281
+ }
282
+ : {}),
283
+ });
284
+ return {
285
+ project,
286
+ certification,
287
+ invocation,
288
+ versionInvocation,
289
+ runtimePath,
290
+ binarySha256,
291
+ reportedVersion,
292
+ staging,
293
+ privateHome,
294
+ scriptFile,
295
+ runInputs,
296
+ outputPaths,
297
+ args,
298
+ nativePacketSha256,
299
+ sourceScript,
300
+ environmentLock,
301
+ timeoutSeconds,
302
+ };
303
+ });
304
+ if ("replay" in prepared)
305
+ return {
306
+ record: prepared.replay,
307
+ replayed: true,
308
+ stagingDirectoryName: prepared.stagingDirectoryName,
309
+ };
310
+ // No workspace lease is held during either the certification runtime probe
311
+ // or the calculation. Its one-use start is durable before either process.
312
+ const environment = {
313
+ PATH: dirname(input.runtime.path),
314
+ HOME: prepared.privateHome,
315
+ TMPDIR: prepared.staging,
316
+ LANG: "C.UTF-8",
317
+ TZ: "UTC",
318
+ PYTHONDONTWRITEBYTECODE: "1",
319
+ PYTHONNOUSERSITE: "1",
320
+ };
321
+ const probe = prepared.versionInvocation
322
+ ? await captureProcess(prepared.versionInvocation.binary, prepared.versionInvocation.args, prepared.staging, environment, Math.min(5, prepared.timeoutSeconds), { maxBytes: prepared.certification.promotion.plan.certification.maxOutputBytes }, investigationObserverRoute(root, projectId, requestSha256, "runtime-probe", prepared.staging))
323
+ : null;
324
+ const probeVersion = probe
325
+ ? probe.stdout.trim() || probe.stderr.trim()
326
+ : prepared.reportedVersion;
327
+ const runtimeValid = !probe ||
328
+ (probe.exitCode === 0 &&
329
+ !probe.outputLimitExceeded &&
330
+ !probe.timedOut &&
331
+ !probe.cancelled &&
332
+ probeVersion === prepared.certification.promotion.plan.recipe.runtime.version);
333
+ const availableSeconds = prepared.timeoutSeconds - (probe?.wallSeconds ?? 0);
334
+ const availableOutput = prepared.certification
335
+ ? prepared.certification.promotion.plan.certification.maxOutputBytes -
336
+ (probe?.observedOutputBytes ?? 0)
337
+ : Number.POSITIVE_INFINITY;
338
+ const observed = probe && (!runtimeValid || availableSeconds <= 0 || availableOutput <= 0)
339
+ ? probe
340
+ : await captureProcess(prepared.invocation?.binary ?? input.runtime.path, prepared.invocation?.args ?? [prepared.scriptFile, ...prepared.args], prepared.staging, environment, availableSeconds, prepared.certification
341
+ ? { maxBytes: availableOutput, paths: [...prepared.outputPaths.values()] }
342
+ : undefined, prepared.certification
343
+ ? investigationObserverRoute(root, projectId, requestSha256, "calculation", prepared.staging)
344
+ : undefined);
345
+ const wallSeconds = observed.wallSeconds + (probe && observed !== probe ? probe.wallSeconds : 0);
346
+ const outputLimitExceeded = Boolean(probe?.outputLimitExceeded || observed.outputLimitExceeded || availableOutput <= 0);
347
+ const observedOutputBytes = (observed.observedOutputBytes ?? 0) +
348
+ (probe && probe !== observed ? (probe.observedOutputBytes ?? 0) : 0);
349
+ return await withWorkspaceLock(root, "research.task.run.commit", async () => {
350
+ const project = await loadProject(root, projectId);
351
+ let status = observed.cancelled
352
+ ? "cancelled"
353
+ : observed.timedOut
354
+ ? "timed-out"
355
+ : observed.exitCode === 0
356
+ ? "succeeded"
357
+ : "failed";
358
+ if (!runtimeValid)
359
+ status = "failed";
360
+ if (outputLimitExceeded)
361
+ status = "output-limit-exceeded";
362
+ try {
363
+ await assertRunWindow(root, project, input);
364
+ if (prepared.certification &&
365
+ (await frozenPromotionView(root, project, prepared.certification.promotion, await readVerifiedJournal(workspacePaths(root).journal))) !== prepared.certification.effectiveDesignSha256)
366
+ status = "stale";
367
+ if ((await nativePacketBinding(root, project, input.nativeSessionId)) !==
368
+ prepared.nativePacketSha256 ||
369
+ project.scientificDesign?.designSha256 !==
370
+ prepared.project.scientificDesign?.designSha256 ||
371
+ project.publicationPolicy?.resolvedPolicySha256 !==
372
+ prepared.project.publicationPolicy?.resolvedPolicySha256)
373
+ status = "stale";
374
+ const current = await loadCurrentEvidenceSnapshot(root, projectId);
375
+ if (prepared.runInputs.some((item) => !current.artifacts.some((artifact) => artifact.artifactId === item.artifactId && artifact.sha256 === item.sha256)))
376
+ status = "stale";
377
+ }
378
+ catch {
379
+ status = "stale";
380
+ }
381
+ const stable = await filesStillMatch([
382
+ { path: prepared.runtimePath, sha256: prepared.binarySha256 },
383
+ { path: prepared.scriptFile, sha256: prepared.sourceScript.sha256 },
384
+ ...prepared.runInputs.map((item) => ({ path: item.executionPath, sha256: item.sha256 })),
385
+ ]);
386
+ if (!stable || (await realpath(input.runtime.path)) !== prepared.runtimePath)
387
+ status = "inputs-changed";
388
+ const outputs = [];
389
+ for (const output of outputLimitExceeded ? [] : input.outputs) {
390
+ const path = prepared.outputPaths.get(output.id);
391
+ try {
392
+ const info = await lstat(path);
393
+ if (!info.isFile() ||
394
+ info.isSymbolicLink() ||
395
+ (prepared.certification &&
396
+ info.size > prepared.certification.promotion.plan.certification.maxOutputBytes))
397
+ throw error("Calculation output is not a regular file within its approved byte bound.");
398
+ if (output.mediaType === "application/json")
399
+ JSON.parse(await readFile(path, "utf8"));
400
+ outputs.push({
401
+ id: output.id,
402
+ mediaType: output.mediaType,
403
+ ...(await storeRunObject(root, projectId, path)),
404
+ });
405
+ }
406
+ catch {
407
+ if (status === "succeeded")
408
+ status = "invalid-output";
409
+ }
410
+ }
411
+ const investigationCertification = prepared.certification
412
+ ? await certificationAssessment(root, projectId, prepared.certification, status, outputs, probe, prepared.invocation.isolation, prepared.versionInvocation.isolation, { observedOutputBytes, outputLimitExceeded })
413
+ : undefined;
414
+ const core = {
415
+ schemaVersion: 1,
416
+ kind: "tiangong-native-run",
417
+ projectId,
418
+ runId: input.runId,
419
+ requestSha256,
420
+ requirementId: input.requirementId,
421
+ requirementSha256: input.requirementSha256,
422
+ designSha256: prepared.project.scientificDesign?.designSha256 ?? null,
423
+ policySha256: prepared.project.publicationPolicy?.resolvedPolicySha256 ?? null,
424
+ nativePacketSha256: prepared.nativePacketSha256,
425
+ status,
426
+ observation: "cli-observed-native-process",
427
+ executionCertified: false,
428
+ environmentVerification: "declared-lock-not-attested",
429
+ runtime: {
430
+ kind: input.runtime.kind,
431
+ version: runtimeValid ? probeVersion : "unverified",
432
+ binarySha256: prepared.binarySha256,
433
+ platform: platform(),
434
+ architecture: arch(),
435
+ },
436
+ script: prepared.sourceScript,
437
+ environmentLock: prepared.environmentLock,
438
+ inputs: prepared.runInputs.map(({ executionPath: _path, ...item }) => item),
439
+ outputs,
440
+ expectedOutputIds: input.outputs.map((item) => item.id),
441
+ arguments: input.arguments,
442
+ process: {
443
+ exitCode: observed.exitCode,
444
+ signal: observed.signal,
445
+ startedAt: probe?.startedAt ?? observed.startedAt,
446
+ finishedAt: observed.finishedAt,
447
+ wallSeconds,
448
+ stdoutSha256: observed.stdoutSha256,
449
+ stderrSha256: observed.stderrSha256,
450
+ stdoutBytes: observed.stdoutBytes,
451
+ stderrBytes: observed.stderrBytes,
452
+ diagnostic: safeDiagnostic(observed.stderr, observed.truncated),
453
+ ...(prepared.certification ? { observedOutputBytes, outputLimitExceeded } : {}),
454
+ },
455
+ ...(investigationCertification ? { investigationCertification } : {}),
456
+ };
457
+ const record = { ...core, recordSha256: sha256Text(canonicalJson(core)) };
458
+ await writeTaskObject(root, projectId, "runs", record.recordSha256, record);
459
+ if (prepared.certification) {
460
+ let mutation = await beginProjectMutation(root, "investigation-certification", project, record.recordSha256);
461
+ try {
462
+ settleProjectCost(project, `investigation-certification-${prepared.certification.promotion.recordSha256}`, prepared.certification.promotion.plan.certification.maxCostUsd, "allocated-upper-bound");
463
+ project.usage.wallSeconds += wallSeconds;
464
+ project.updatedAt = new Date().toISOString();
465
+ mutation = await prepareProjectMutation(root, mutation, project);
466
+ await appendJournalEvent(workspacePaths(root).journal, "project.task.run.completed", projectId, {
467
+ projectId,
468
+ runId: input.runId,
469
+ requestSha256,
470
+ recordSha256: record.recordSha256,
471
+ status,
472
+ mutation: projectMutationBinding(mutation),
473
+ });
474
+ await settleProjectMutation(root, mutation);
475
+ }
476
+ catch (error) {
477
+ await settleProjectMutation(root, mutation);
478
+ throw error;
479
+ }
480
+ }
481
+ else {
482
+ await appendJournalEvent(workspacePaths(root).journal, "project.task.run.completed", projectId, { runId: input.runId, requestSha256, recordSha256: record.recordSha256, status });
483
+ }
484
+ return { record, replayed: false, stagingDirectoryName: basename(prepared.staging) };
485
+ });
486
+ }
487
+ catch (caught) {
488
+ if (caught instanceof CliError)
489
+ throw caught;
490
+ throw error("The exact native calculation could not be observed. Preserve its files and inspect run status before an explicit new attempt.", "RESEARCH_NATIVE_RUN_FAILED");
491
+ }
492
+ }
493
+ async function assertRunWindow(root, project, input) {
494
+ const events = await readVerifiedJournal(workspacePaths(root).journal);
495
+ assertProjectAuthority(project, projectAuthorityIndex(events));
496
+ const task = await loadProjectTask(root, project.id, events);
497
+ const requirement = task?.current.requirements.find((item) => item.id === input.requirementId && taskRequirementSha256(item) === input.requirementSha256);
498
+ if (!requirement ||
499
+ requirement.checkKind !== "computation" ||
500
+ project.packages.find((item) => item.stage === "acquire")?.status !== "complete" ||
501
+ project.packages.find((item) => item.stage === "review")?.status === "complete" ||
502
+ project.handoff.state !== "agent-actionable")
503
+ throw error("An observed calculation needs an active computational requirement, frozen acquisition, and an actionable project before completed review.");
504
+ await nativePacketBinding(root, project, input.nativeSessionId);
505
+ }
506
+ export async function nativePacketBinding(root, project, sessionId) {
507
+ const active = await pathExists(join(workspacePaths(root).projects, project.id, "native/active.json"));
508
+ if (!active) {
509
+ if (sessionId !== null)
510
+ throw error("The selected native stage session is no longer active.");
511
+ return null;
512
+ }
513
+ if (!sessionId)
514
+ throw error("An active native calculation must name its exact stage session.");
515
+ const { inspectNativeCalculationScope } = await import("./runtime.js");
516
+ return inspectNativeCalculationScope(root, project, sessionId);
517
+ }
518
+ async function exactDirectory(path) {
519
+ if (!isAbsolute(path) || path !== resolve(path))
520
+ throw error("Native calculation directory must be explicit, absolute and canonical.");
521
+ const info = await lstat(path);
522
+ if (!info.isDirectory() || info.isSymbolicLink())
523
+ throw error("Native calculation directory cannot be a symbolic link.");
524
+ // Canonicalize parent aliases such as macOS /var before containment checks.
525
+ return realpath(path);
526
+ }
527
+ export async function copyExact(source, target, sha256) {
528
+ const info = await lstat(source);
529
+ if (!info.isFile() || info.isSymbolicLink() || (await sha256File(source)) !== sha256)
530
+ throw error("A bound calculation file is missing, linked or changed.");
531
+ await copyFile(source, target, constants.COPYFILE_EXCL);
532
+ await chmod(target, 0o444);
533
+ if ((await sha256File(target)) !== sha256)
534
+ throw error("Calculation bytes changed during staging.");
535
+ }
536
+ export async function storeRunObject(root, projectId, source) {
537
+ const info = await lstat(source);
538
+ if (!info.isFile() || info.isSymbolicLink())
539
+ throw error("Run objects must be exact regular files.");
540
+ const bytes = await readFile(source);
541
+ if (sanitizeResearchText(bytes.toString("utf8"), configuredResearchSecrets(process.env)) !==
542
+ bytes.toString("utf8"))
543
+ throw error("Calculation files contain sensitive material; provide an explicitly safe artifact.");
544
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
545
+ const directory = await taskDirectory(root, projectId, "run-objects", true);
546
+ const target = join(directory, sha256);
547
+ if (!(await pathExists(target)))
548
+ await copyExact(source, target, sha256);
549
+ else if (!(await filesStillMatch([{ path: target, sha256 }])))
550
+ throw error("An immutable native run object changed.");
551
+ return { path: `task/run-objects/${sha256}`, sha256, bytes: bytes.length };
552
+ }
553
+ async function filesStillMatch(files) {
554
+ for (const file of files) {
555
+ const info = await lstat(file.path).catch(() => null);
556
+ if (!info?.isFile() || info.isSymbolicLink() || (await sha256File(file.path)) !== file.sha256)
557
+ return false;
558
+ }
559
+ return true;
560
+ }
561
+ function completedRunEvent(events, projectId, runId) {
562
+ return events.findLast((event) => event.scope === projectId &&
563
+ event.type === "project.task.run.completed" &&
564
+ event.payload.runId === runId);
565
+ }
566
+ export async function inspectNativeRun(root, projectId, runId) {
567
+ const events = await readVerifiedJournal(workspacePaths(root).journal);
568
+ const completed = completedRunEvent(events, projectId, runId);
569
+ if (completed)
570
+ return { record: await readNativeRun(root, projectId, String(completed.payload.recordSha256)) };
571
+ const started = events.find((event) => event.scope === projectId &&
572
+ event.type === "project.task.run.started" &&
573
+ event.payload.runId === runId);
574
+ return {
575
+ projectId,
576
+ runId,
577
+ status: started ? "incomplete" : "not-found",
578
+ automaticRetry: false,
579
+ ...(started?.payload.investigationPromotionSha256
580
+ ? {
581
+ observer: await inspectInvestigationObserver(root, projectId, String(started.payload.requestSha256)),
582
+ }
583
+ : {}),
584
+ };
585
+ }
586
+ /** Shared by live inspection and portable audit, including ordinary histories
587
+ * that contain no investigation records to trigger a separate verifier. */
588
+ export function assertNativeRunJournalBinding(record, started, completed) {
589
+ const certification = record.investigationCertification;
590
+ if (!started ||
591
+ !completed ||
592
+ started.scope !== record.projectId ||
593
+ completed.scope !== record.projectId ||
594
+ started.type !== "project.task.run.started" ||
595
+ completed.type !== "project.task.run.completed" ||
596
+ started.payload.runId !== record.runId ||
597
+ completed.payload.runId !== record.runId ||
598
+ completed.payload.recordSha256 !== record.recordSha256 ||
599
+ completed.payload.requestSha256 !== record.requestSha256 ||
600
+ started.payload.requestSha256 !== record.requestSha256 ||
601
+ started.payload.requirementId !== record.requirementId ||
602
+ started.payload.requirementSha256 !== record.requirementSha256 ||
603
+ started.payload.scriptSha256 !== record.script.sha256 ||
604
+ started.payload.runtimeBinarySha256 !== record.runtime.binarySha256 ||
605
+ started.payload.environmentLockSha256 !== record.environmentLock.sha256 ||
606
+ started.payload.nativePacketSha256 !== record.nativePacketSha256 ||
607
+ (certification !== undefined &&
608
+ (!isObject(certification) ||
609
+ typeof certification.promotionSha256 !== "string" ||
610
+ !HASH.test(certification.promotionSha256))) ||
611
+ started.payload.investigationPromotionSha256 !== certification?.promotionSha256 ||
612
+ completed.payload.status !== record.status ||
613
+ started.sequence >= completed.sequence)
614
+ throw error("Native run does not match its committed start and completion.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
615
+ }
616
+ export async function readNativeRun(root, projectId, hash, knownEvents) {
617
+ const record = validateNativeRunRecord(await readTaskObject(root, projectId, "runs", hash, "recordSha256"), projectId);
618
+ const events = knownEvents ?? (await readVerifiedJournal(workspacePaths(root).journal));
619
+ const completed = completedRunEvent(events, projectId, record.runId);
620
+ const started = events.find((event) => event.scope === projectId &&
621
+ event.type === "project.task.run.started" &&
622
+ event.payload.runId === record.runId);
623
+ assertNativeRunJournalBinding(record, started, completed);
624
+ await taskDirectory(root, projectId, "run-objects", false);
625
+ for (const object of [
626
+ record.script,
627
+ record.environmentLock,
628
+ ...record.inputs,
629
+ ...record.outputs,
630
+ ]) {
631
+ const path = join(workspacePaths(root).projects, projectId, object.path);
632
+ if (!(await filesStillMatch([{ path, sha256: object.sha256 }])) ||
633
+ (await lstat(path)).size !== object.bytes)
634
+ throw error("Native run object bytes changed.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
635
+ }
636
+ return record;
637
+ }
638
+ /** Explicit journal references only; no directory scan or newest-result selection. */
639
+ export async function nativeRunArtifactRecords(root, projectId) {
640
+ const events = await readVerifiedJournal(workspacePaths(root).journal);
641
+ const hashes = [
642
+ ...new Set(events
643
+ .filter((event) => event.scope === projectId && event.type === "project.task.run.completed")
644
+ .map((event) => String(event.payload.recordSha256))),
645
+ ];
646
+ const objects = new Map();
647
+ for (const hash of hashes) {
648
+ const run = await readNativeRun(root, projectId, hash, events);
649
+ const path = `task/runs/${hash}.json`;
650
+ objects.set(path, await fileRecord(join(workspacePaths(root).projects, projectId, path), path));
651
+ for (const object of [run.script, run.environmentLock, ...run.inputs, ...run.outputs]) {
652
+ objects.set(object.path, { path: object.path, sha256: object.sha256, bytes: object.bytes });
653
+ }
654
+ }
655
+ return [...objects.values()];
656
+ }
657
+ export function validateNativeRunRecord(value, projectId) {
658
+ if (!isObject(value) ||
659
+ value.schemaVersion !== 1 ||
660
+ value.kind !== "tiangong-native-run" ||
661
+ value.projectId !== projectId ||
662
+ value.observation !== "cli-observed-native-process" ||
663
+ value.executionCertified !== false ||
664
+ value.environmentVerification !== "declared-lock-not-attested" ||
665
+ !statuses.includes(value.status) ||
666
+ !isObject(value.runtime) ||
667
+ !isObject(value.process) ||
668
+ !Array.isArray(value.inputs) ||
669
+ !Array.isArray(value.outputs) ||
670
+ !Array.isArray(value.expectedOutputIds))
671
+ throw error("Native run record is malformed.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
672
+ const { recordSha256, ...core } = value;
673
+ if (recordSha256 !== sha256Text(canonicalJson(core)))
674
+ throw error("Native run record hash changed.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
675
+ for (const object of [value.script, value.environmentLock, ...value.inputs, ...value.outputs]) {
676
+ if (!isObject(object) ||
677
+ typeof object.sha256 !== "string" ||
678
+ !HASH.test(object.sha256) ||
679
+ object.path !== `task/run-objects/${object.sha256}` ||
680
+ !Number.isSafeInteger(object.bytes) ||
681
+ Number(object.bytes) < 0)
682
+ throw error("Native run object address is invalid.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
683
+ }
684
+ if (value.status === "succeeded" &&
685
+ (value.process.exitCode !== 0 ||
686
+ value.process.signal !== null ||
687
+ value.outputs.length !== value.expectedOutputIds.length))
688
+ throw error("A successful run needs an observed zero exit and every declared output.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
689
+ return value;
690
+ }
691
+ function safeDiagnostic(text, truncated) {
692
+ if (truncated)
693
+ return "Calculation diagnostics exceeded the bounded log capture; no partial secret-bearing text is reported.";
694
+ return sanitizeResearchText(text, configuredResearchSecrets(process.env))
695
+ .replace(/\/(?:Users|home|private\/tmp|private\/var|tmp)\/[^\s"':)]+/gu, "[host-path]")
696
+ .replace(/[A-Za-z]:\\[^\s"']+/gu, "[host-path]")
697
+ .slice(0, 2000);
698
+ }
699
+ export async function captureProcess(binary, args, cwd, env, timeoutSeconds, outputLimit, observation) {
700
+ if (outputLimit && (!Number.isSafeInteger(outputLimit.maxBytes) || outputLimit.maxBytes < 1))
701
+ throw error("Observed output budget must be a positive finite byte count.");
702
+ const startedAt = new Date().toISOString();
703
+ const start = process.hrtime.bigint();
704
+ return new Promise((resolvePromise) => {
705
+ const supervised = Boolean(outputLimit);
706
+ const sourceRuntime = import.meta.url.endsWith(".ts");
707
+ const supervisorPath = fileURLToPath(new URL(sourceRuntime ? "./native-process-supervisor.ts" : "./native-process-supervisor.js", import.meta.url));
708
+ const child = supervised
709
+ ? fork(supervisorPath, [], {
710
+ cwd,
711
+ env,
712
+ detached: platform() !== "win32",
713
+ execArgv: [
714
+ ...(sourceRuntime ? ["--import", import.meta.resolve("tsx")] : []),
715
+ ...(observation ? [`--title=${observation.title}`] : []),
716
+ ],
717
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
718
+ })
719
+ : spawn(binary, args, {
720
+ cwd,
721
+ env,
722
+ shell: false,
723
+ detached: platform() !== "win32",
724
+ stdio: ["ignore", "pipe", "pipe"],
725
+ });
726
+ let supervisorResult = null;
727
+ if (supervised) {
728
+ child.on("message", (value) => {
729
+ if (isObject(value) &&
730
+ value.kind === "result" &&
731
+ (value.exitCode === null || Number.isInteger(value.exitCode)) &&
732
+ (value.signal === null || typeof value.signal === "string") &&
733
+ typeof value.timedOut === "boolean")
734
+ supervisorResult = {
735
+ exitCode: value.exitCode,
736
+ signal: value.signal,
737
+ timedOut: value.timedOut,
738
+ };
739
+ });
740
+ void (async () => {
741
+ if (observation) {
742
+ if (!child.pid)
743
+ throw new Error("Observer was not spawned");
744
+ await observation.onSpawn(child.pid);
745
+ }
746
+ child.send({
747
+ kind: "start",
748
+ binary,
749
+ args,
750
+ env,
751
+ deadlineNs: (start + BigInt(Math.ceil(timeoutSeconds * 1e9))).toString(),
752
+ }, (error) => {
753
+ if (error)
754
+ child.kill("SIGTERM");
755
+ });
756
+ })().catch(() => {
757
+ spawnFailed = true;
758
+ child.kill("SIGTERM");
759
+ });
760
+ }
761
+ const out = createHash("sha256"), err = createHash("sha256");
762
+ const stdout = [], stderr = [];
763
+ let stdoutBytes = 0, stderrBytes = 0, truncated = false, timedOut = false, cancelled = false, spawnFailed = false;
764
+ const terminate = (signal) => {
765
+ if (!child.pid)
766
+ return;
767
+ if (supervised) {
768
+ if (child.connected)
769
+ child.send({ kind: "stop", reason: timedOut ? "timeout" : cancelled ? "cancel" : "output-limit" }, (error) => {
770
+ if (error && child.exitCode === null)
771
+ child.kill("SIGTERM");
772
+ });
773
+ else if (child.exitCode === null)
774
+ child.kill("SIGTERM");
775
+ return;
776
+ }
777
+ if (!outputLimit && (child.exitCode !== null || child.signalCode !== null))
778
+ return;
779
+ try {
780
+ if (platform() === "win32")
781
+ child.kill(signal);
782
+ else
783
+ process.kill(-child.pid, signal);
784
+ }
785
+ catch {
786
+ if (child.exitCode === null && child.signalCode === null)
787
+ child.kill(signal);
788
+ }
789
+ };
790
+ let outputLimitExceeded = false;
791
+ const filePeaks = new Map();
792
+ let pendingScan = null;
793
+ const outputBytes = () => stdoutBytes + stderrBytes + [...filePeaks.values()].reduce((sum, n) => sum + n, 0);
794
+ const checkOutput = () => {
795
+ if (outputLimit && outputBytes() > outputLimit.maxBytes) {
796
+ outputLimitExceeded = true;
797
+ terminate("SIGKILL");
798
+ }
799
+ };
800
+ const scanOutputFiles = async () => {
801
+ if (!outputLimit)
802
+ return;
803
+ await Promise.all((outputLimit.paths ?? []).map(async (path) => {
804
+ const info = await lstat(path).catch(() => null);
805
+ if (info?.isFile() && !info.isSymbolicLink())
806
+ filePeaks.set(path, Math.max(filePeaks.get(path) ?? 0, info.size));
807
+ }));
808
+ checkOutput();
809
+ };
810
+ const outputTimer = outputLimit?.paths?.length
811
+ ? setInterval(() => {
812
+ if (pendingScan)
813
+ return;
814
+ pendingScan = scanOutputFiles().finally(() => {
815
+ pendingScan = null;
816
+ });
817
+ }, 250)
818
+ : null;
819
+ const cancel = () => {
820
+ cancelled = true;
821
+ terminate("SIGKILL");
822
+ };
823
+ process.once("SIGINT", cancel);
824
+ process.once("SIGTERM", cancel);
825
+ const timer = setTimeout(() => {
826
+ timedOut = true;
827
+ terminate("SIGKILL");
828
+ }, timeoutSeconds * 1000);
829
+ child.stdout.on("data", (chunk) => {
830
+ out.update(chunk);
831
+ stdoutBytes += chunk.length;
832
+ if (outputLimit ? stdoutBytes + stderrBytes <= outputLimit.maxBytes : stdoutBytes <= 1024 * 1024)
833
+ stdout.push(chunk);
834
+ else
835
+ truncated = true;
836
+ checkOutput();
837
+ });
838
+ child.stderr.on("data", (chunk) => {
839
+ err.update(chunk);
840
+ stderrBytes += chunk.length;
841
+ if (outputLimit ? stdoutBytes + stderrBytes <= outputLimit.maxBytes : stderrBytes <= 1024 * 1024)
842
+ stderr.push(chunk);
843
+ else
844
+ truncated = true;
845
+ checkOutput();
846
+ });
847
+ child.on("error", () => {
848
+ spawnFailed = true;
849
+ });
850
+ child.on("close", async (exitCode, signal) => {
851
+ clearTimeout(timer);
852
+ if (outputTimer)
853
+ clearInterval(outputTimer);
854
+ if (pendingScan)
855
+ await pendingScan;
856
+ await scanOutputFiles();
857
+ process.off("SIGINT", cancel);
858
+ process.off("SIGTERM", cancel);
859
+ resolvePromise({
860
+ exitCode: supervised ? (supervisorResult?.exitCode ?? null) : spawnFailed ? null : exitCode,
861
+ signal: supervised ? (supervisorResult?.signal ?? signal) : signal,
862
+ startedAt,
863
+ finishedAt: new Date().toISOString(),
864
+ wallSeconds: Number(process.hrtime.bigint() - start) / 1e9,
865
+ stdout: Buffer.concat(stdout).toString("utf8"),
866
+ stderr: Buffer.concat(stderr).toString("utf8"),
867
+ stdoutSha256: out.digest("hex"),
868
+ stderrSha256: err.digest("hex"),
869
+ stdoutBytes,
870
+ stderrBytes,
871
+ truncated,
872
+ timedOut: timedOut || Boolean(supervisorResult?.timedOut),
873
+ cancelled,
874
+ ...(outputLimit ? { outputLimitExceeded, observedOutputBytes: outputBytes() } : {}),
875
+ });
876
+ });
877
+ });
878
+ }
879
+ //# sourceMappingURL=native-run.js.map