@tiangong-ai/cli 0.0.60 → 0.0.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/README.md +167 -26
- package/dist/research/orchestration.js +108 -18
- package/dist/research/orchestration.js.map +1 -1
- package/dist/research/workspace/acquisition-routes.d.ts +1 -1
- package/dist/research/workspace/acquisition-routes.js +2 -11
- package/dist/research/workspace/acquisition-routes.js.map +1 -1
- package/dist/research/workspace/artifact-read-audit.d.ts +16 -0
- package/dist/research/workspace/artifact-read-audit.js +176 -0
- package/dist/research/workspace/artifact-read-audit.js.map +1 -0
- package/dist/research/workspace/artifact-trace.d.ts +12 -0
- package/dist/research/workspace/artifact-trace.js +109 -0
- package/dist/research/workspace/artifact-trace.js.map +1 -0
- package/dist/research/workspace/artifact-view-mcp.d.ts +75 -0
- package/dist/research/workspace/artifact-view-mcp.js +214 -0
- package/dist/research/workspace/artifact-view-mcp.js.map +1 -0
- package/dist/research/workspace/artifact-views.d.ts +100 -0
- package/dist/research/workspace/artifact-views.js +395 -0
- package/dist/research/workspace/artifact-views.js.map +1 -0
- package/dist/research/workspace/audit-bundle.d.ts +4 -0
- package/dist/research/workspace/audit-bundle.js +107 -8
- package/dist/research/workspace/audit-bundle.js.map +1 -1
- package/dist/research/workspace/executor.d.ts +5 -1
- package/dist/research/workspace/executor.js +106 -22
- package/dist/research/workspace/executor.js.map +1 -1
- package/dist/research/workspace/native-run.d.ts +77 -0
- package/dist/research/workspace/native-run.js +637 -0
- package/dist/research/workspace/native-run.js.map +1 -0
- package/dist/research/workspace/preflight.d.ts +8 -2
- package/dist/research/workspace/preflight.js +17 -14
- package/dist/research/workspace/preflight.js.map +1 -1
- package/dist/research/workspace/project-mutations.d.ts +1 -1
- package/dist/research/workspace/project-mutations.js +5 -3
- package/dist/research/workspace/project-mutations.js.map +1 -1
- package/dist/research/workspace/projects.js +1 -0
- package/dist/research/workspace/projects.js.map +1 -1
- package/dist/research/workspace/request-provenance.d.ts +67 -0
- package/dist/research/workspace/request-provenance.js +115 -0
- package/dist/research/workspace/request-provenance.js.map +1 -0
- package/dist/research/workspace/review-executor.js +22 -11
- package/dist/research/workspace/review-executor.js.map +1 -1
- package/dist/research/workspace/runtime.d.ts +52 -0
- package/dist/research/workspace/runtime.js +221 -98
- package/dist/research/workspace/runtime.js.map +1 -1
- package/dist/research/workspace/sanitization.js +1 -1
- package/dist/research/workspace/sanitization.js.map +1 -1
- package/dist/research/workspace/schema-compatibility.d.ts +5 -0
- package/dist/research/workspace/schema-compatibility.js +55 -0
- package/dist/research/workspace/schema-compatibility.js.map +1 -0
- package/dist/research/workspace/scientific-design.js +8 -17
- package/dist/research/workspace/scientific-design.js.map +1 -1
- package/dist/research/workspace/scientific-fulfillment-audit.d.ts +3 -0
- package/dist/research/workspace/scientific-fulfillment-audit.js +137 -0
- package/dist/research/workspace/scientific-fulfillment-audit.js.map +1 -0
- package/dist/research/workspace/scientific-fulfillment.d.ts +121 -0
- package/dist/research/workspace/scientific-fulfillment.js +539 -0
- package/dist/research/workspace/scientific-fulfillment.js.map +1 -0
- package/dist/research/workspace/scientific-review-execution.js +82 -61
- package/dist/research/workspace/scientific-review-execution.js.map +1 -1
- package/dist/research/workspace/scientific-review.d.ts +5 -1
- package/dist/research/workspace/scientific-review.js +197 -52
- package/dist/research/workspace/scientific-review.js.map +1 -1
- package/dist/research/workspace/setup-catalog.js +2 -2
- package/dist/research/workspace/task-acceptance.d.ts +10 -2
- package/dist/research/workspace/task-acceptance.js +72 -8
- package/dist/research/workspace/task-acceptance.js.map +1 -1
- package/dist/research/workspace/task-audit.js +52 -2
- package/dist/research/workspace/task-audit.js.map +1 -1
- package/dist/research/workspace/task-contract.d.ts +4 -1
- package/dist/research/workspace/task-contract.js +26 -5
- package/dist/research/workspace/task-contract.js.map +1 -1
- package/dist/research/workspace/types.d.ts +6 -3
- package/package.json +1 -1
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { constants } from "node:fs";
|
|
5
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, readFile, realpath } from "node:fs/promises";
|
|
6
|
+
import { arch, platform } from "node:os";
|
|
7
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
8
|
+
import { CliError } from "../../errors.js";
|
|
9
|
+
import { loadCurrentEvidenceSnapshot } from "./acquisition.js";
|
|
10
|
+
import { appendJournalEvent, readVerifiedJournal } from "./journal.js";
|
|
11
|
+
import { assertProjectAuthority, projectAuthorityIndex } from "./project-authority.js";
|
|
12
|
+
import { loadProject } from "./projects.js";
|
|
13
|
+
import { configuredResearchSecrets, sanitizeResearchText, sanitizeResearchValue, } from "./sanitization.js";
|
|
14
|
+
import { isContainedRelativePath, registerScientificObject } from "./scientific-objects.js";
|
|
15
|
+
import { canonicalJson, fileRecord, isObject, pathExists, resolveContained, sha256File, sha256Text, workspacePaths, } from "./storage.js";
|
|
16
|
+
import { loadProjectTask, readTaskObject, taskDirectory, taskRequirementSha256, writeTaskObject, } from "./task-contract.js";
|
|
17
|
+
import { loadWorkspaceConfig, withWorkspaceLock } from "./workspace.js";
|
|
18
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
19
|
+
const ID = "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$";
|
|
20
|
+
const statuses = [
|
|
21
|
+
"succeeded",
|
|
22
|
+
"failed",
|
|
23
|
+
"timed-out",
|
|
24
|
+
"cancelled",
|
|
25
|
+
"invalid-output",
|
|
26
|
+
"inputs-changed",
|
|
27
|
+
"stale",
|
|
28
|
+
];
|
|
29
|
+
const inputSchema = {
|
|
30
|
+
type: "object",
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
required: [
|
|
33
|
+
"schemaVersion",
|
|
34
|
+
"runId",
|
|
35
|
+
"requirementId",
|
|
36
|
+
"requirementSha256",
|
|
37
|
+
"nativeSessionId",
|
|
38
|
+
"workingDirectory",
|
|
39
|
+
"runtime",
|
|
40
|
+
"scriptPath",
|
|
41
|
+
"environmentLockPath",
|
|
42
|
+
"inputs",
|
|
43
|
+
"outputs",
|
|
44
|
+
"arguments",
|
|
45
|
+
"timeoutSeconds",
|
|
46
|
+
],
|
|
47
|
+
properties: {
|
|
48
|
+
schemaVersion: { const: 1 },
|
|
49
|
+
runId: { type: "string", pattern: ID },
|
|
50
|
+
requirementId: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$" },
|
|
51
|
+
requirementSha256: { type: "string", pattern: HASH.source },
|
|
52
|
+
nativeSessionId: { type: ["string", "null"], minLength: 1 },
|
|
53
|
+
workingDirectory: { type: "string", minLength: 1 },
|
|
54
|
+
scriptPath: { type: "string", minLength: 1 },
|
|
55
|
+
environmentLockPath: { type: "string", minLength: 1 },
|
|
56
|
+
runtime: {
|
|
57
|
+
type: "object",
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
required: ["kind", "path"],
|
|
60
|
+
properties: { kind: { enum: ["node", "python"] }, path: { type: "string", minLength: 1 } },
|
|
61
|
+
},
|
|
62
|
+
inputs: {
|
|
63
|
+
type: "array",
|
|
64
|
+
minItems: 1,
|
|
65
|
+
maxItems: 256,
|
|
66
|
+
items: {
|
|
67
|
+
type: "object",
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
required: ["id", "artifactId", "sha256"],
|
|
70
|
+
properties: {
|
|
71
|
+
id: { type: "string", pattern: ID },
|
|
72
|
+
artifactId: { type: "string", pattern: ID },
|
|
73
|
+
sha256: { type: "string", pattern: HASH.source },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
outputs: {
|
|
78
|
+
type: "array",
|
|
79
|
+
minItems: 1,
|
|
80
|
+
maxItems: 32,
|
|
81
|
+
items: {
|
|
82
|
+
type: "object",
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
required: ["id", "fileName", "mediaType"],
|
|
85
|
+
properties: {
|
|
86
|
+
id: { type: "string", pattern: ID },
|
|
87
|
+
fileName: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$" },
|
|
88
|
+
mediaType: { type: "string", pattern: "^(?:text|application|image)/[a-z0-9.+-]+$" },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
arguments: { type: "array", maxItems: 256, items: { type: "string", maxLength: 4000 } },
|
|
93
|
+
timeoutSeconds: { type: "integer", minimum: 1, maximum: 172800 },
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const validateInput = new Ajv2020({ strict: false, allErrors: true }).compile(inputSchema);
|
|
97
|
+
export function nativeRunInputSchema() {
|
|
98
|
+
return structuredClone(inputSchema);
|
|
99
|
+
}
|
|
100
|
+
function error(message, code = "RESEARCH_NATIVE_RUN_INVALID", exitCode = 3) {
|
|
101
|
+
return new CliError(message, { code, exitCode });
|
|
102
|
+
}
|
|
103
|
+
function parseInput(value) {
|
|
104
|
+
if (!validateInput(value) ||
|
|
105
|
+
canonicalJson(sanitizeResearchValue(value, configuredResearchSecrets(process.env))) !==
|
|
106
|
+
canonicalJson(value))
|
|
107
|
+
throw error("Native run must match the closed secret-free schema; supply an ordinary calculation, not an agent launcher.", undefined, 2);
|
|
108
|
+
const input = value;
|
|
109
|
+
for (const names of [
|
|
110
|
+
input.inputs.map((item) => item.id),
|
|
111
|
+
input.outputs.map((item) => item.id),
|
|
112
|
+
input.outputs.map((item) => item.fileName),
|
|
113
|
+
]) {
|
|
114
|
+
if (new Set(names).size !== names.length)
|
|
115
|
+
throw error("Native run inputs and outputs need distinct exact names.", undefined, 2);
|
|
116
|
+
}
|
|
117
|
+
for (const argument of input.arguments) {
|
|
118
|
+
if (/[\r\n\0]/u.test(argument) ||
|
|
119
|
+
/(?:^|=)(?:\/|[A-Za-z]:[\\/])/u.test(argument) ||
|
|
120
|
+
/--(?:api-key|auth-token|password|cookie)(?:=|$)/iu.test(argument))
|
|
121
|
+
throw error("Use exact input/output placeholders and non-secret arguments, not host paths or credentials.", undefined, 2);
|
|
122
|
+
for (const match of argument.matchAll(/\{(input|output):([^}]+)\}/gu)) {
|
|
123
|
+
const collection = match[1] === "input" ? input.inputs : input.outputs;
|
|
124
|
+
if (!collection.some((item) => item.id === match[2]))
|
|
125
|
+
throw error("Native run argument refers to an undeclared file.", undefined, 2);
|
|
126
|
+
}
|
|
127
|
+
if (/[{}]/u.test(argument.replace(/\{(?:input|output):[^}]+\}/gu, "")))
|
|
128
|
+
throw error("Native run contains an unsupported argument placeholder.", undefined, 2);
|
|
129
|
+
}
|
|
130
|
+
return input;
|
|
131
|
+
}
|
|
132
|
+
/** One explicitly requested non-AI calculation. It inherits the native host's OS
|
|
133
|
+
* restrictions; this observer is neither an agent scheduler nor a new sandbox. */
|
|
134
|
+
export async function observeNativeRun(root, projectId, value, confirm) {
|
|
135
|
+
if (!confirm)
|
|
136
|
+
throw error("Review the exact calculation and pass --confirm-execution; no program was started.", "RESEARCH_NATIVE_RUN_CONFIRMATION_REQUIRED", 2);
|
|
137
|
+
const input = parseInput(value);
|
|
138
|
+
const requestSha256 = sha256Text(canonicalJson(input));
|
|
139
|
+
try {
|
|
140
|
+
const prepared = await withWorkspaceLock(root, "research.task.run.prepare", async () => {
|
|
141
|
+
const project = await loadProject(root, projectId);
|
|
142
|
+
const events = await readVerifiedJournal(workspacePaths(root).journal);
|
|
143
|
+
assertProjectAuthority(project, projectAuthorityIndex(events));
|
|
144
|
+
const started = events.find((event) => event.scope === projectId &&
|
|
145
|
+
event.type === "project.task.run.started" &&
|
|
146
|
+
event.payload.runId === input.runId);
|
|
147
|
+
if (started) {
|
|
148
|
+
if (started.payload.requestSha256 !== requestSha256)
|
|
149
|
+
throw error("This run ID already names a different exact request. Use a new run ID for a new calculation.", "RESEARCH_NATIVE_RUN_CONFLICT");
|
|
150
|
+
const completed = completedRunEvent(events, projectId, input.runId);
|
|
151
|
+
if (!completed)
|
|
152
|
+
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");
|
|
153
|
+
return {
|
|
154
|
+
replay: await readNativeRun(root, projectId, String(completed.payload.recordSha256)),
|
|
155
|
+
stagingDirectoryName: String(started.payload.stagingDirectoryName ?? ""),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
await assertRunWindow(root, project, input);
|
|
159
|
+
const config = await loadWorkspaceConfig(root);
|
|
160
|
+
const timeoutSeconds = Math.min(input.timeoutSeconds, config.budget.packageMaxWallSeconds.analyze, config.budget.maxWallSeconds - project.usage.wallSeconds);
|
|
161
|
+
if (timeoutSeconds < input.timeoutSeconds)
|
|
162
|
+
throw error("The declared calculation timeout exceeds the remaining finite workspace time budget.");
|
|
163
|
+
const working = await exactDirectory(input.workingDirectory);
|
|
164
|
+
if (isContainedRelativePath(relative(await realpath(workspacePaths(root).control), working)))
|
|
165
|
+
throw error("Calculation staging must use an explicit directory outside the control store.");
|
|
166
|
+
if (!isAbsolute(input.runtime.path) || input.runtime.path !== resolve(input.runtime.path))
|
|
167
|
+
throw error("Select an absolute Node or Python interpreter path.");
|
|
168
|
+
const runtimePath = await realpath(input.runtime.path);
|
|
169
|
+
if (!(await lstat(runtimePath)).isFile())
|
|
170
|
+
throw error("The selected interpreter is not a regular executable file.");
|
|
171
|
+
const binarySha256 = await sha256File(runtimePath);
|
|
172
|
+
const version = await captureProcess(input.runtime.path, ["--version"], working, { PATH: dirname(input.runtime.path) }, 5);
|
|
173
|
+
const reportedVersion = version.stdout.trim() || version.stderr.trim();
|
|
174
|
+
if (version.exitCode !== 0 ||
|
|
175
|
+
!(input.runtime.kind === "node" ? /^v\d+\.\d+\.\d+$/u : /^Python \d+\.\d+\.\d+$/u).test(reportedVersion))
|
|
176
|
+
throw error("The selected executable is not the declared ordinary Node/Python runtime. Agent CLI launchers are not supported.");
|
|
177
|
+
const acquisition = await loadCurrentEvidenceSnapshot(root, projectId);
|
|
178
|
+
const selected = input.inputs.map((item) => {
|
|
179
|
+
const artifact = acquisition.artifacts.find((artifact) => artifact.artifactId === item.artifactId && artifact.sha256 === item.sha256);
|
|
180
|
+
if (!artifact)
|
|
181
|
+
throw error("Calculation input must name an exact artifact in the current frozen acquisition.");
|
|
182
|
+
return { ...item, artifact };
|
|
183
|
+
});
|
|
184
|
+
const script = await registerScientificObject({
|
|
185
|
+
root,
|
|
186
|
+
objectKind: "model-implementation",
|
|
187
|
+
path: input.scriptPath,
|
|
188
|
+
mediaType: input.runtime.kind === "node" ? "text/javascript" : "text/x-python",
|
|
189
|
+
});
|
|
190
|
+
const environment = await registerScientificObject({
|
|
191
|
+
root,
|
|
192
|
+
objectKind: "environment-lock",
|
|
193
|
+
path: input.environmentLockPath,
|
|
194
|
+
mediaType: input.environmentLockPath.endsWith(".json") ? "application/json" : "text/plain",
|
|
195
|
+
});
|
|
196
|
+
const staging = await mkdtemp(join(working, `.tiangong-run-${input.runId}-`));
|
|
197
|
+
const privateHome = join(staging, "home");
|
|
198
|
+
await mkdir(privateHome, { mode: 0o700 });
|
|
199
|
+
const scriptFile = join(staging, basename(input.scriptPath));
|
|
200
|
+
await copyExact(resolveContained(workspacePaths(root).control, script.objectLocator), scriptFile, script.sha256);
|
|
201
|
+
const runInputs = [];
|
|
202
|
+
for (const item of selected) {
|
|
203
|
+
const source = resolveContained(workspacePaths(root).control, item.artifact.locator);
|
|
204
|
+
const executionPath = join(staging, `input-${item.id}`);
|
|
205
|
+
await copyExact(source, executionPath, item.sha256);
|
|
206
|
+
runInputs.push({
|
|
207
|
+
id: item.id,
|
|
208
|
+
artifactId: item.artifactId,
|
|
209
|
+
...(await storeRunObject(root, projectId, executionPath)),
|
|
210
|
+
executionPath,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const outputPaths = new Map(input.outputs.map((item) => [item.id, join(staging, item.fileName)]));
|
|
214
|
+
if (new Set([
|
|
215
|
+
...runInputs.map((item) => item.executionPath),
|
|
216
|
+
scriptFile,
|
|
217
|
+
privateHome,
|
|
218
|
+
...outputPaths.values(),
|
|
219
|
+
]).size !==
|
|
220
|
+
runInputs.length + outputPaths.size + 2)
|
|
221
|
+
throw error("Declared outputs collide with protected calculation inputs.");
|
|
222
|
+
const args = input.arguments.map((argument) => argument.replace(/\{(input|output):([^}]+)\}/gu, (_match, kind, id) => kind === "input"
|
|
223
|
+
? runInputs.find((item) => item.id === id).executionPath
|
|
224
|
+
: outputPaths.get(id)));
|
|
225
|
+
const nativePacketSha256 = await nativePacketBinding(root, project, input.nativeSessionId);
|
|
226
|
+
const sourceScript = await storeRunObject(root, projectId, scriptFile);
|
|
227
|
+
const environmentLock = await storeRunObject(root, projectId, resolveContained(workspacePaths(root).control, environment.objectLocator));
|
|
228
|
+
await appendJournalEvent(workspacePaths(root).journal, "project.task.run.started", projectId, {
|
|
229
|
+
runId: input.runId,
|
|
230
|
+
requestSha256,
|
|
231
|
+
requirementId: input.requirementId,
|
|
232
|
+
requirementSha256: input.requirementSha256,
|
|
233
|
+
scriptSha256: sourceScript.sha256,
|
|
234
|
+
environmentLockSha256: environmentLock.sha256,
|
|
235
|
+
runtimeBinarySha256: binarySha256,
|
|
236
|
+
nativePacketSha256,
|
|
237
|
+
stagingDirectoryName: basename(staging),
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
project,
|
|
241
|
+
runtimePath,
|
|
242
|
+
binarySha256,
|
|
243
|
+
reportedVersion,
|
|
244
|
+
staging,
|
|
245
|
+
privateHome,
|
|
246
|
+
scriptFile,
|
|
247
|
+
runInputs,
|
|
248
|
+
outputPaths,
|
|
249
|
+
args,
|
|
250
|
+
nativePacketSha256,
|
|
251
|
+
sourceScript,
|
|
252
|
+
environmentLock,
|
|
253
|
+
timeoutSeconds,
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
if ("replay" in prepared)
|
|
257
|
+
return {
|
|
258
|
+
record: prepared.replay,
|
|
259
|
+
replayed: true,
|
|
260
|
+
stagingDirectoryName: prepared.stagingDirectoryName,
|
|
261
|
+
};
|
|
262
|
+
// No workspace lease is held during the native calculation.
|
|
263
|
+
const observed = await captureProcess(input.runtime.path, [prepared.scriptFile, ...prepared.args], prepared.staging, {
|
|
264
|
+
PATH: dirname(input.runtime.path),
|
|
265
|
+
HOME: prepared.privateHome,
|
|
266
|
+
TMPDIR: prepared.staging,
|
|
267
|
+
LANG: "C.UTF-8",
|
|
268
|
+
TZ: "UTC",
|
|
269
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
270
|
+
PYTHONNOUSERSITE: "1",
|
|
271
|
+
}, prepared.timeoutSeconds);
|
|
272
|
+
return await withWorkspaceLock(root, "research.task.run.commit", async () => {
|
|
273
|
+
const project = await loadProject(root, projectId);
|
|
274
|
+
let status = observed.cancelled
|
|
275
|
+
? "cancelled"
|
|
276
|
+
: observed.timedOut
|
|
277
|
+
? "timed-out"
|
|
278
|
+
: observed.exitCode === 0
|
|
279
|
+
? "succeeded"
|
|
280
|
+
: "failed";
|
|
281
|
+
try {
|
|
282
|
+
await assertRunWindow(root, project, input);
|
|
283
|
+
if ((await nativePacketBinding(root, project, input.nativeSessionId)) !==
|
|
284
|
+
prepared.nativePacketSha256 ||
|
|
285
|
+
project.scientificDesign?.designSha256 !==
|
|
286
|
+
prepared.project.scientificDesign?.designSha256 ||
|
|
287
|
+
project.publicationPolicy?.resolvedPolicySha256 !==
|
|
288
|
+
prepared.project.publicationPolicy?.resolvedPolicySha256)
|
|
289
|
+
status = "stale";
|
|
290
|
+
const current = await loadCurrentEvidenceSnapshot(root, projectId);
|
|
291
|
+
if (prepared.runInputs.some((item) => !current.artifacts.some((artifact) => artifact.artifactId === item.artifactId && artifact.sha256 === item.sha256)))
|
|
292
|
+
status = "stale";
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
status = "stale";
|
|
296
|
+
}
|
|
297
|
+
const stable = await filesStillMatch([
|
|
298
|
+
{ path: prepared.runtimePath, sha256: prepared.binarySha256 },
|
|
299
|
+
{ path: prepared.scriptFile, sha256: prepared.sourceScript.sha256 },
|
|
300
|
+
...prepared.runInputs.map((item) => ({ path: item.executionPath, sha256: item.sha256 })),
|
|
301
|
+
]);
|
|
302
|
+
if (!stable || (await realpath(input.runtime.path)) !== prepared.runtimePath)
|
|
303
|
+
status = "inputs-changed";
|
|
304
|
+
const outputs = [];
|
|
305
|
+
for (const output of input.outputs) {
|
|
306
|
+
const path = prepared.outputPaths.get(output.id);
|
|
307
|
+
try {
|
|
308
|
+
const info = await lstat(path);
|
|
309
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
310
|
+
throw error("Calculation output is not a regular file.");
|
|
311
|
+
if (output.mediaType === "application/json")
|
|
312
|
+
JSON.parse(await readFile(path, "utf8"));
|
|
313
|
+
outputs.push({
|
|
314
|
+
id: output.id,
|
|
315
|
+
mediaType: output.mediaType,
|
|
316
|
+
...(await storeRunObject(root, projectId, path)),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
if (status === "succeeded")
|
|
321
|
+
status = "invalid-output";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const core = {
|
|
325
|
+
schemaVersion: 1,
|
|
326
|
+
kind: "tiangong-native-run",
|
|
327
|
+
projectId,
|
|
328
|
+
runId: input.runId,
|
|
329
|
+
requestSha256,
|
|
330
|
+
requirementId: input.requirementId,
|
|
331
|
+
requirementSha256: input.requirementSha256,
|
|
332
|
+
designSha256: prepared.project.scientificDesign?.designSha256 ?? null,
|
|
333
|
+
policySha256: prepared.project.publicationPolicy?.resolvedPolicySha256 ?? null,
|
|
334
|
+
nativePacketSha256: prepared.nativePacketSha256,
|
|
335
|
+
status,
|
|
336
|
+
observation: "cli-observed-native-process",
|
|
337
|
+
executionCertified: false,
|
|
338
|
+
environmentVerification: "declared-lock-not-attested",
|
|
339
|
+
runtime: {
|
|
340
|
+
kind: input.runtime.kind,
|
|
341
|
+
version: prepared.reportedVersion,
|
|
342
|
+
binarySha256: prepared.binarySha256,
|
|
343
|
+
platform: platform(),
|
|
344
|
+
architecture: arch(),
|
|
345
|
+
},
|
|
346
|
+
script: prepared.sourceScript,
|
|
347
|
+
environmentLock: prepared.environmentLock,
|
|
348
|
+
inputs: prepared.runInputs.map(({ executionPath: _path, ...item }) => item),
|
|
349
|
+
outputs,
|
|
350
|
+
expectedOutputIds: input.outputs.map((item) => item.id),
|
|
351
|
+
arguments: input.arguments,
|
|
352
|
+
process: {
|
|
353
|
+
exitCode: observed.exitCode,
|
|
354
|
+
signal: observed.signal,
|
|
355
|
+
startedAt: observed.startedAt,
|
|
356
|
+
finishedAt: observed.finishedAt,
|
|
357
|
+
wallSeconds: observed.wallSeconds,
|
|
358
|
+
stdoutSha256: observed.stdoutSha256,
|
|
359
|
+
stderrSha256: observed.stderrSha256,
|
|
360
|
+
stdoutBytes: observed.stdoutBytes,
|
|
361
|
+
stderrBytes: observed.stderrBytes,
|
|
362
|
+
diagnostic: safeDiagnostic(observed.stderr, observed.truncated),
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
const record = { ...core, recordSha256: sha256Text(canonicalJson(core)) };
|
|
366
|
+
await writeTaskObject(root, projectId, "runs", record.recordSha256, record);
|
|
367
|
+
await appendJournalEvent(workspacePaths(root).journal, "project.task.run.completed", projectId, { runId: input.runId, requestSha256, recordSha256: record.recordSha256, status });
|
|
368
|
+
return { record, replayed: false, stagingDirectoryName: basename(prepared.staging) };
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
catch (caught) {
|
|
372
|
+
if (caught instanceof CliError)
|
|
373
|
+
throw caught;
|
|
374
|
+
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");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function assertRunWindow(root, project, input) {
|
|
378
|
+
const events = await readVerifiedJournal(workspacePaths(root).journal);
|
|
379
|
+
assertProjectAuthority(project, projectAuthorityIndex(events));
|
|
380
|
+
const task = await loadProjectTask(root, project.id, events);
|
|
381
|
+
const requirement = task?.current.requirements.find((item) => item.id === input.requirementId && taskRequirementSha256(item) === input.requirementSha256);
|
|
382
|
+
if (!requirement ||
|
|
383
|
+
requirement.checkKind !== "computation" ||
|
|
384
|
+
project.packages.find((item) => item.stage === "acquire")?.status !== "complete" ||
|
|
385
|
+
project.packages.find((item) => item.stage === "review")?.status === "complete" ||
|
|
386
|
+
project.handoff.state !== "agent-actionable")
|
|
387
|
+
throw error("An observed calculation needs an active computational requirement, frozen acquisition, and an actionable project before completed review.");
|
|
388
|
+
await nativePacketBinding(root, project, input.nativeSessionId);
|
|
389
|
+
}
|
|
390
|
+
async function nativePacketBinding(root, project, sessionId) {
|
|
391
|
+
const active = await pathExists(join(workspacePaths(root).projects, project.id, "native/active.json"));
|
|
392
|
+
if (!active) {
|
|
393
|
+
if (sessionId !== null)
|
|
394
|
+
throw error("The selected native stage session is no longer active.");
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
if (!sessionId)
|
|
398
|
+
throw error("An active native calculation must name its exact stage session.");
|
|
399
|
+
const { inspectNativeCalculationScope } = await import("./runtime.js");
|
|
400
|
+
return inspectNativeCalculationScope(root, project, sessionId);
|
|
401
|
+
}
|
|
402
|
+
async function exactDirectory(path) {
|
|
403
|
+
if (!isAbsolute(path) || path !== resolve(path))
|
|
404
|
+
throw error("Native calculation directory must be explicit, absolute and canonical.");
|
|
405
|
+
const info = await lstat(path);
|
|
406
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
407
|
+
throw error("Native calculation directory cannot be a symbolic link.");
|
|
408
|
+
// Canonicalize parent aliases such as macOS /var before containment checks.
|
|
409
|
+
return realpath(path);
|
|
410
|
+
}
|
|
411
|
+
async function copyExact(source, target, sha256) {
|
|
412
|
+
const info = await lstat(source);
|
|
413
|
+
if (!info.isFile() || info.isSymbolicLink() || (await sha256File(source)) !== sha256)
|
|
414
|
+
throw error("A bound calculation file is missing, linked or changed.");
|
|
415
|
+
await copyFile(source, target, constants.COPYFILE_EXCL);
|
|
416
|
+
await chmod(target, 0o444);
|
|
417
|
+
if ((await sha256File(target)) !== sha256)
|
|
418
|
+
throw error("Calculation bytes changed during staging.");
|
|
419
|
+
}
|
|
420
|
+
async function storeRunObject(root, projectId, source) {
|
|
421
|
+
const info = await lstat(source);
|
|
422
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
423
|
+
throw error("Run objects must be exact regular files.");
|
|
424
|
+
const bytes = await readFile(source);
|
|
425
|
+
if (sanitizeResearchText(bytes.toString("utf8"), configuredResearchSecrets(process.env)) !==
|
|
426
|
+
bytes.toString("utf8"))
|
|
427
|
+
throw error("Calculation files contain sensitive material; provide an explicitly safe artifact.");
|
|
428
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
429
|
+
const directory = await taskDirectory(root, projectId, "run-objects", true);
|
|
430
|
+
const target = join(directory, sha256);
|
|
431
|
+
if (!(await pathExists(target)))
|
|
432
|
+
await copyExact(source, target, sha256);
|
|
433
|
+
else if (!(await filesStillMatch([{ path: target, sha256 }])))
|
|
434
|
+
throw error("An immutable native run object changed.");
|
|
435
|
+
return { path: `task/run-objects/${sha256}`, sha256, bytes: bytes.length };
|
|
436
|
+
}
|
|
437
|
+
async function filesStillMatch(files) {
|
|
438
|
+
for (const file of files) {
|
|
439
|
+
const info = await lstat(file.path).catch(() => null);
|
|
440
|
+
if (!info?.isFile() || info.isSymbolicLink() || (await sha256File(file.path)) !== file.sha256)
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
function completedRunEvent(events, projectId, runId) {
|
|
446
|
+
return events.findLast((event) => event.scope === projectId &&
|
|
447
|
+
event.type === "project.task.run.completed" &&
|
|
448
|
+
event.payload.runId === runId);
|
|
449
|
+
}
|
|
450
|
+
export async function inspectNativeRun(root, projectId, runId) {
|
|
451
|
+
const events = await readVerifiedJournal(workspacePaths(root).journal);
|
|
452
|
+
const completed = completedRunEvent(events, projectId, runId);
|
|
453
|
+
if (completed)
|
|
454
|
+
return { record: await readNativeRun(root, projectId, String(completed.payload.recordSha256)) };
|
|
455
|
+
return {
|
|
456
|
+
projectId,
|
|
457
|
+
runId,
|
|
458
|
+
status: events.some((event) => event.scope === projectId &&
|
|
459
|
+
event.type === "project.task.run.started" &&
|
|
460
|
+
event.payload.runId === runId)
|
|
461
|
+
? "incomplete"
|
|
462
|
+
: "not-found",
|
|
463
|
+
automaticRetry: false,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
export async function readNativeRun(root, projectId, hash, knownEvents) {
|
|
467
|
+
const record = validateNativeRunRecord(await readTaskObject(root, projectId, "runs", hash, "recordSha256"), projectId);
|
|
468
|
+
const events = knownEvents ?? (await readVerifiedJournal(workspacePaths(root).journal));
|
|
469
|
+
const completed = completedRunEvent(events, projectId, record.runId);
|
|
470
|
+
const started = events.find((event) => event.scope === projectId &&
|
|
471
|
+
event.type === "project.task.run.started" &&
|
|
472
|
+
event.payload.runId === record.runId);
|
|
473
|
+
if (completed?.payload.recordSha256 !== hash ||
|
|
474
|
+
completed.payload.requestSha256 !== record.requestSha256 ||
|
|
475
|
+
started?.payload.requestSha256 !== record.requestSha256 ||
|
|
476
|
+
started.payload.scriptSha256 !== record.script.sha256 ||
|
|
477
|
+
started.payload.runtimeBinarySha256 !== record.runtime.binarySha256 ||
|
|
478
|
+
started.payload.environmentLockSha256 !== record.environmentLock.sha256 ||
|
|
479
|
+
started.payload.nativePacketSha256 !== record.nativePacketSha256 ||
|
|
480
|
+
completed.payload.status !== record.status ||
|
|
481
|
+
started.sequence >= completed.sequence)
|
|
482
|
+
throw error("Native run does not match its committed start and completion.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
|
|
483
|
+
await taskDirectory(root, projectId, "run-objects", false);
|
|
484
|
+
for (const object of [
|
|
485
|
+
record.script,
|
|
486
|
+
record.environmentLock,
|
|
487
|
+
...record.inputs,
|
|
488
|
+
...record.outputs,
|
|
489
|
+
]) {
|
|
490
|
+
const path = join(workspacePaths(root).projects, projectId, object.path);
|
|
491
|
+
if (!(await filesStillMatch([{ path, sha256: object.sha256 }])) ||
|
|
492
|
+
(await lstat(path)).size !== object.bytes)
|
|
493
|
+
throw error("Native run object bytes changed.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
|
|
494
|
+
}
|
|
495
|
+
return record;
|
|
496
|
+
}
|
|
497
|
+
/** Explicit journal references only; no directory scan or newest-result selection. */
|
|
498
|
+
export async function nativeRunArtifactRecords(root, projectId) {
|
|
499
|
+
const events = await readVerifiedJournal(workspacePaths(root).journal);
|
|
500
|
+
const hashes = [
|
|
501
|
+
...new Set(events
|
|
502
|
+
.filter((event) => event.scope === projectId && event.type === "project.task.run.completed")
|
|
503
|
+
.map((event) => String(event.payload.recordSha256))),
|
|
504
|
+
];
|
|
505
|
+
const objects = new Map();
|
|
506
|
+
for (const hash of hashes) {
|
|
507
|
+
const run = await readNativeRun(root, projectId, hash, events);
|
|
508
|
+
const path = `task/runs/${hash}.json`;
|
|
509
|
+
objects.set(path, await fileRecord(join(workspacePaths(root).projects, projectId, path), path));
|
|
510
|
+
for (const object of [run.script, run.environmentLock, ...run.inputs, ...run.outputs]) {
|
|
511
|
+
objects.set(object.path, { path: object.path, sha256: object.sha256, bytes: object.bytes });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return [...objects.values()];
|
|
515
|
+
}
|
|
516
|
+
export function validateNativeRunRecord(value, projectId) {
|
|
517
|
+
if (!isObject(value) ||
|
|
518
|
+
value.schemaVersion !== 1 ||
|
|
519
|
+
value.kind !== "tiangong-native-run" ||
|
|
520
|
+
value.projectId !== projectId ||
|
|
521
|
+
value.observation !== "cli-observed-native-process" ||
|
|
522
|
+
value.executionCertified !== false ||
|
|
523
|
+
value.environmentVerification !== "declared-lock-not-attested" ||
|
|
524
|
+
!statuses.includes(value.status) ||
|
|
525
|
+
!isObject(value.runtime) ||
|
|
526
|
+
!isObject(value.process) ||
|
|
527
|
+
!Array.isArray(value.inputs) ||
|
|
528
|
+
!Array.isArray(value.outputs) ||
|
|
529
|
+
!Array.isArray(value.expectedOutputIds))
|
|
530
|
+
throw error("Native run record is malformed.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
|
|
531
|
+
const { recordSha256, ...core } = value;
|
|
532
|
+
if (recordSha256 !== sha256Text(canonicalJson(core)))
|
|
533
|
+
throw error("Native run record hash changed.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
|
|
534
|
+
for (const object of [value.script, value.environmentLock, ...value.inputs, ...value.outputs]) {
|
|
535
|
+
if (!isObject(object) ||
|
|
536
|
+
typeof object.sha256 !== "string" ||
|
|
537
|
+
!HASH.test(object.sha256) ||
|
|
538
|
+
object.path !== `task/run-objects/${object.sha256}` ||
|
|
539
|
+
!Number.isSafeInteger(object.bytes) ||
|
|
540
|
+
Number(object.bytes) < 0)
|
|
541
|
+
throw error("Native run object address is invalid.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
|
|
542
|
+
}
|
|
543
|
+
if (value.status === "succeeded" &&
|
|
544
|
+
(value.process.exitCode !== 0 ||
|
|
545
|
+
value.process.signal !== null ||
|
|
546
|
+
value.outputs.length !== value.expectedOutputIds.length))
|
|
547
|
+
throw error("A successful run needs an observed zero exit and every declared output.", "RESEARCH_NATIVE_RUN_BINDING_INVALID");
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
function safeDiagnostic(text, truncated) {
|
|
551
|
+
if (truncated)
|
|
552
|
+
return "Calculation diagnostics exceeded the bounded log capture; no partial secret-bearing text is reported.";
|
|
553
|
+
return sanitizeResearchText(text, configuredResearchSecrets(process.env))
|
|
554
|
+
.replace(/\/(?:Users|home|private\/tmp|private\/var|tmp)\/[^\s"':)]+/gu, "[host-path]")
|
|
555
|
+
.replace(/[A-Za-z]:\\[^\s"']+/gu, "[host-path]")
|
|
556
|
+
.slice(0, 2000);
|
|
557
|
+
}
|
|
558
|
+
async function captureProcess(binary, args, cwd, env, timeoutSeconds) {
|
|
559
|
+
const startedAt = new Date().toISOString();
|
|
560
|
+
const start = process.hrtime.bigint();
|
|
561
|
+
return new Promise((resolvePromise) => {
|
|
562
|
+
const child = spawn(binary, args, {
|
|
563
|
+
cwd,
|
|
564
|
+
env,
|
|
565
|
+
shell: false,
|
|
566
|
+
detached: platform() !== "win32",
|
|
567
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
568
|
+
});
|
|
569
|
+
const out = createHash("sha256"), err = createHash("sha256");
|
|
570
|
+
const stdout = [], stderr = [];
|
|
571
|
+
let stdoutBytes = 0, stderrBytes = 0, truncated = false, timedOut = false, cancelled = false, spawnFailed = false;
|
|
572
|
+
const terminate = (signal) => {
|
|
573
|
+
if (!child.pid || child.exitCode !== null || child.signalCode !== null)
|
|
574
|
+
return;
|
|
575
|
+
try {
|
|
576
|
+
if (platform() === "win32")
|
|
577
|
+
child.kill(signal);
|
|
578
|
+
else
|
|
579
|
+
process.kill(-child.pid, signal);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
child.kill(signal);
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
const cancel = () => {
|
|
586
|
+
cancelled = true;
|
|
587
|
+
terminate("SIGKILL");
|
|
588
|
+
};
|
|
589
|
+
process.once("SIGINT", cancel);
|
|
590
|
+
process.once("SIGTERM", cancel);
|
|
591
|
+
const timer = setTimeout(() => {
|
|
592
|
+
timedOut = true;
|
|
593
|
+
terminate("SIGKILL");
|
|
594
|
+
}, timeoutSeconds * 1000);
|
|
595
|
+
child.stdout.on("data", (chunk) => {
|
|
596
|
+
out.update(chunk);
|
|
597
|
+
stdoutBytes += chunk.length;
|
|
598
|
+
if (stdoutBytes <= 1024 * 1024)
|
|
599
|
+
stdout.push(chunk);
|
|
600
|
+
else
|
|
601
|
+
truncated = true;
|
|
602
|
+
});
|
|
603
|
+
child.stderr.on("data", (chunk) => {
|
|
604
|
+
err.update(chunk);
|
|
605
|
+
stderrBytes += chunk.length;
|
|
606
|
+
if (stderrBytes <= 1024 * 1024)
|
|
607
|
+
stderr.push(chunk);
|
|
608
|
+
else
|
|
609
|
+
truncated = true;
|
|
610
|
+
});
|
|
611
|
+
child.on("error", () => {
|
|
612
|
+
spawnFailed = true;
|
|
613
|
+
});
|
|
614
|
+
child.on("close", (exitCode, signal) => {
|
|
615
|
+
clearTimeout(timer);
|
|
616
|
+
process.off("SIGINT", cancel);
|
|
617
|
+
process.off("SIGTERM", cancel);
|
|
618
|
+
resolvePromise({
|
|
619
|
+
exitCode: spawnFailed ? null : exitCode,
|
|
620
|
+
signal,
|
|
621
|
+
startedAt,
|
|
622
|
+
finishedAt: new Date().toISOString(),
|
|
623
|
+
wallSeconds: Number(process.hrtime.bigint() - start) / 1e9,
|
|
624
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
625
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
626
|
+
stdoutSha256: out.digest("hex"),
|
|
627
|
+
stderrSha256: err.digest("hex"),
|
|
628
|
+
stdoutBytes,
|
|
629
|
+
stderrBytes,
|
|
630
|
+
truncated,
|
|
631
|
+
timedOut,
|
|
632
|
+
cancelled,
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
//# sourceMappingURL=native-run.js.map
|