@tea-agent/loop-agent 0.24.5 → 0.24.7
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/CHANGELOG.md +32 -0
- package/dist/executors/shell-executor.js +46 -3
- package/dist/worker/delivery/verification-bundle.js +26 -5
- package/dist/worker/observe/dag-run-artifacts.js +90 -0
- package/dist/worker/observe/node-input.js +444 -0
- package/dist/worker/observe/routes.js +17 -0
- package/dist/worker/observe/static/api.js +9 -0
- package/dist/worker/observe/static/constants.js +9 -0
- package/dist/worker/observe/static/state.js +14 -0
- package/dist/worker/observe/static/styles.css +74 -0
- package/dist/worker/observe/static/views/dag-inspector.js +371 -15
- package/dist/worker/outcomes/projector.js +5 -1
- package/dist/workflows/dag/backend-test-result-contract.js +103 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
- package/dist/workflows/dag/init-hybrid.js +4 -6
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/types.js +2 -0
- package/dist/workflows/dag/validate.js +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { redactSecrets, truncateUtf8Preview, } from "../../shared/preview.js";
|
|
4
|
+
import { resolveDagNodeSkills } from "../../workflows/dag/skills.js";
|
|
5
|
+
import { resolveDagNodePromptRedacted, resolveDagRunJson, } from "./dag-run-artifacts.js";
|
|
6
|
+
export const NODE_INPUT_SCHEMA_VERSION = 1;
|
|
7
|
+
export const NODE_INPUT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
8
|
+
const SHELL_GATE_KEYS = [
|
|
9
|
+
"verdictGate",
|
|
10
|
+
"projectGovernanceGate",
|
|
11
|
+
"requirementCoverageGate",
|
|
12
|
+
"jsonArtifactGate",
|
|
13
|
+
"frontendPrewriteGate",
|
|
14
|
+
"frontendLintBaseline",
|
|
15
|
+
"frontendVerificationBundle",
|
|
16
|
+
"frontendReviewContext",
|
|
17
|
+
"backendTestPipeline",
|
|
18
|
+
"verifyEvidence",
|
|
19
|
+
"repairArtifactGate",
|
|
20
|
+
];
|
|
21
|
+
const PROMPT_FINGERPRINT_RE = /<prompt\s+sha256=([a-f0-9]{12})\s+len=(\d+)\s*>/i;
|
|
22
|
+
function unavailableBody(dagRunId, nodeId, reason, warnings = []) {
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: NODE_INPUT_SCHEMA_VERSION,
|
|
25
|
+
dagRunId,
|
|
26
|
+
nodeId,
|
|
27
|
+
available: false,
|
|
28
|
+
availabilityReason: reason,
|
|
29
|
+
source: null,
|
|
30
|
+
summary: null,
|
|
31
|
+
dependency: null,
|
|
32
|
+
prompt: null,
|
|
33
|
+
executorInput: null,
|
|
34
|
+
boundaries: null,
|
|
35
|
+
promptFingerprint: null,
|
|
36
|
+
warnings,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function redactString(value) {
|
|
40
|
+
if (typeof value !== "string")
|
|
41
|
+
return null;
|
|
42
|
+
return redactSecrets(value);
|
|
43
|
+
}
|
|
44
|
+
function redactStringList(values) {
|
|
45
|
+
if (!Array.isArray(values))
|
|
46
|
+
return [];
|
|
47
|
+
return values
|
|
48
|
+
.filter((item) => typeof item === "string")
|
|
49
|
+
.map((item) => redactSecrets(item));
|
|
50
|
+
}
|
|
51
|
+
function truncatedField(text, maxBytes = NODE_INPUT_PREVIEW_MAX_BYTES) {
|
|
52
|
+
const redacted = redactSecrets(text);
|
|
53
|
+
const truncated = truncateUtf8Preview(redacted, maxBytes);
|
|
54
|
+
return {
|
|
55
|
+
text: truncated,
|
|
56
|
+
truncated: truncated !== redacted,
|
|
57
|
+
maxBytes,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function parsePromptRedactedFingerprint(markdown) {
|
|
61
|
+
const match = PROMPT_FINGERPRINT_RE.exec(markdown);
|
|
62
|
+
if (!match) {
|
|
63
|
+
return { ok: false, error: "fingerprint-format-mismatch" };
|
|
64
|
+
}
|
|
65
|
+
const sha256Prefix = match[1];
|
|
66
|
+
const lengthChars = Number(match[2]);
|
|
67
|
+
if (!Number.isSafeInteger(lengthChars) || lengthChars < 0) {
|
|
68
|
+
return { ok: false, error: "fingerprint-length-invalid" };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, sha256Prefix, lengthChars };
|
|
71
|
+
}
|
|
72
|
+
function isLikelyDynamicExpandedNode(nodeId, tasks) {
|
|
73
|
+
for (const parent of tasks) {
|
|
74
|
+
const parentId = typeof parent.id === "string" ? parent.id : "";
|
|
75
|
+
const expansion = parent.dynamicExpansion;
|
|
76
|
+
if (expansion && typeof expansion === "object") {
|
|
77
|
+
const childIdPrefix = typeof expansion.childIdPrefix ===
|
|
78
|
+
"string"
|
|
79
|
+
? expansion.childIdPrefix
|
|
80
|
+
: "";
|
|
81
|
+
if (childIdPrefix) {
|
|
82
|
+
const prefix = `${childIdPrefix}-`;
|
|
83
|
+
if (nodeId.startsWith(prefix)) {
|
|
84
|
+
const suffix = nodeId.slice(prefix.length);
|
|
85
|
+
if (/^\d{4}$/.test(suffix) && Number(suffix) >= 1) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const loop = parent.dynamicLoopUntil;
|
|
92
|
+
if (loop && typeof loop === "object" && parentId) {
|
|
93
|
+
const prefix = `${parentId}-r`;
|
|
94
|
+
if (nodeId.startsWith(prefix)) {
|
|
95
|
+
const rest = nodeId.slice(prefix.length);
|
|
96
|
+
const dash = rest.indexOf("-");
|
|
97
|
+
if (dash > 0) {
|
|
98
|
+
const iterationText = rest.slice(0, dash);
|
|
99
|
+
if (/^\d+$/.test(iterationText) && Number(iterationText) >= 1) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
function shellGateNames(shell) {
|
|
109
|
+
const gates = [];
|
|
110
|
+
for (const key of SHELL_GATE_KEYS) {
|
|
111
|
+
if (shell[key] != null)
|
|
112
|
+
gates.push(key);
|
|
113
|
+
}
|
|
114
|
+
return gates;
|
|
115
|
+
}
|
|
116
|
+
function resolveModelHint(spec, task) {
|
|
117
|
+
const complexity = typeof task.complexity === "string" ? task.complexity : null;
|
|
118
|
+
const executorModels = spec.executorModels;
|
|
119
|
+
if (complexity &&
|
|
120
|
+
executorModels &&
|
|
121
|
+
typeof executorModels === "object" &&
|
|
122
|
+
!Array.isArray(executorModels)) {
|
|
123
|
+
const pi = executorModels.pi;
|
|
124
|
+
if (pi && typeof pi === "object" && !Array.isArray(pi)) {
|
|
125
|
+
const model = pi[complexity];
|
|
126
|
+
if (typeof model === "string" && model.trim()) {
|
|
127
|
+
return redactSecrets(model);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const defaults = spec.defaults;
|
|
132
|
+
if (defaults && typeof defaults === "object" && !Array.isArray(defaults)) {
|
|
133
|
+
const model = defaults.model;
|
|
134
|
+
if (typeof model === "string" && model.trim()) {
|
|
135
|
+
return redactSecrets(model);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warnings) {
|
|
141
|
+
const defaults = spec.defaults && typeof spec.defaults === "object"
|
|
142
|
+
? spec.defaults
|
|
143
|
+
: undefined;
|
|
144
|
+
const executorDeclared = typeof task.executor === "string";
|
|
145
|
+
const executor = executorDeclared
|
|
146
|
+
? String(task.executor)
|
|
147
|
+
: "pi";
|
|
148
|
+
const executorSource = executorDeclared
|
|
149
|
+
? "task"
|
|
150
|
+
: "schema-default";
|
|
151
|
+
const writePolicyDeclared = typeof task.writePolicy === "string";
|
|
152
|
+
let writePolicy = null;
|
|
153
|
+
let writePolicySource = null;
|
|
154
|
+
if (writePolicyDeclared) {
|
|
155
|
+
writePolicy = String(task.writePolicy);
|
|
156
|
+
writePolicySource = "task";
|
|
157
|
+
}
|
|
158
|
+
else if (typeof defaults?.writePolicy === "string") {
|
|
159
|
+
writePolicy = defaults.writePolicy;
|
|
160
|
+
writePolicySource = "defaults";
|
|
161
|
+
}
|
|
162
|
+
const skillSpec = {
|
|
163
|
+
defaults,
|
|
164
|
+
skillsByRole: spec.skillsByRole && typeof spec.skillsByRole === "object"
|
|
165
|
+
? spec.skillsByRole
|
|
166
|
+
: undefined,
|
|
167
|
+
};
|
|
168
|
+
const skills = resolveDagNodeSkills(skillSpec, {
|
|
169
|
+
role: typeof task.role === "string" ? task.role : undefined,
|
|
170
|
+
skills: Array.isArray(task.skills)
|
|
171
|
+
? task.skills.filter((s) => typeof s === "string")
|
|
172
|
+
: undefined,
|
|
173
|
+
}).map((skill) => redactSecrets(skill));
|
|
174
|
+
const promptRaw = typeof task.subtask_prompt === "string" ? task.subtask_prompt : "";
|
|
175
|
+
const promptBody = truncatedField(promptRaw);
|
|
176
|
+
const promptSourceRaw = task.subtask_prompt_source;
|
|
177
|
+
let promptSource = null;
|
|
178
|
+
if (promptSourceRaw && typeof promptSourceRaw === "object") {
|
|
179
|
+
const src = promptSourceRaw;
|
|
180
|
+
if (src.type === "markdown" && typeof src.path === "string") {
|
|
181
|
+
promptSource = {
|
|
182
|
+
type: "markdown",
|
|
183
|
+
path: redactSecrets(src.path),
|
|
184
|
+
sha256Prefix: typeof src.sha256 === "string" && src.sha256.length >= 12
|
|
185
|
+
? src.sha256.slice(0, 12)
|
|
186
|
+
: null,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const shellRaw = task.shell && typeof task.shell === "object" && !Array.isArray(task.shell)
|
|
191
|
+
? task.shell
|
|
192
|
+
: null;
|
|
193
|
+
const staticRaw = task.static && typeof task.static === "object" && !Array.isArray(task.static)
|
|
194
|
+
? task.static
|
|
195
|
+
: null;
|
|
196
|
+
let kind = "unknown";
|
|
197
|
+
if (executor === "shell" || shellRaw)
|
|
198
|
+
kind = "shell";
|
|
199
|
+
else if (executor === "static" || staticRaw)
|
|
200
|
+
kind = "static";
|
|
201
|
+
else if (executor === "pi")
|
|
202
|
+
kind = "pi";
|
|
203
|
+
const shell = shellRaw
|
|
204
|
+
? {
|
|
205
|
+
commands: redactStringList(shellRaw.commands),
|
|
206
|
+
preset: typeof shellRaw.preset === "string"
|
|
207
|
+
? redactSecrets(shellRaw.preset)
|
|
208
|
+
: null,
|
|
209
|
+
cwd: typeof shellRaw.cwd === "string"
|
|
210
|
+
? redactSecrets(shellRaw.cwd)
|
|
211
|
+
: null,
|
|
212
|
+
timeoutMs: typeof shellRaw.timeoutMs === "number" &&
|
|
213
|
+
Number.isFinite(shellRaw.timeoutMs)
|
|
214
|
+
? shellRaw.timeoutMs
|
|
215
|
+
: null,
|
|
216
|
+
nonZeroExitPolicy: typeof shellRaw.nonZeroExitPolicy === "string"
|
|
217
|
+
? shellRaw.nonZeroExitPolicy
|
|
218
|
+
: null,
|
|
219
|
+
gates: shellGateNames(shellRaw),
|
|
220
|
+
}
|
|
221
|
+
: null;
|
|
222
|
+
const staticPreview = staticRaw && typeof staticRaw.resultMarkdown === "string"
|
|
223
|
+
? truncatedField(staticRaw.resultMarkdown)
|
|
224
|
+
: null;
|
|
225
|
+
const staticBlock = staticRaw
|
|
226
|
+
? {
|
|
227
|
+
status: typeof staticRaw.status === "string" ? staticRaw.status : null,
|
|
228
|
+
resultPreview: staticPreview?.text ?? "",
|
|
229
|
+
truncated: staticPreview?.truncated ?? false,
|
|
230
|
+
maxBytes: staticPreview?.maxBytes ?? NODE_INPUT_PREVIEW_MAX_BYTES,
|
|
231
|
+
}
|
|
232
|
+
: null;
|
|
233
|
+
return {
|
|
234
|
+
schemaVersion: NODE_INPUT_SCHEMA_VERSION,
|
|
235
|
+
dagRunId,
|
|
236
|
+
nodeId,
|
|
237
|
+
available: true,
|
|
238
|
+
availabilityReason: "available",
|
|
239
|
+
source: {
|
|
240
|
+
kind: "frozen-run-spec",
|
|
241
|
+
artifact: "run.json#tasks",
|
|
242
|
+
lifecycle,
|
|
243
|
+
},
|
|
244
|
+
summary: {
|
|
245
|
+
executor: redactString(executor),
|
|
246
|
+
executorSource,
|
|
247
|
+
role: typeof task.role === "string" ? redactSecrets(task.role) : null,
|
|
248
|
+
complexity: typeof task.complexity === "string"
|
|
249
|
+
? task.complexity
|
|
250
|
+
: null,
|
|
251
|
+
writePolicy: writePolicy ? redactSecrets(writePolicy) : null,
|
|
252
|
+
writePolicySource,
|
|
253
|
+
toolProfile: typeof task.toolProfile === "string"
|
|
254
|
+
? redactSecrets(task.toolProfile)
|
|
255
|
+
: null,
|
|
256
|
+
},
|
|
257
|
+
dependency: {
|
|
258
|
+
dependsOn: redactStringList(task.depends_on),
|
|
259
|
+
dependsPolicy: typeof task.dependsPolicy === "string" ? task.dependsPolicy : null,
|
|
260
|
+
failureAwareDependsOn: redactStringList(task.failureAwareDependsOn),
|
|
261
|
+
runIf: typeof task.runIf === "string" ? redactSecrets(task.runIf) : null,
|
|
262
|
+
},
|
|
263
|
+
prompt: {
|
|
264
|
+
format: "markdown",
|
|
265
|
+
text: promptBody.text,
|
|
266
|
+
truncated: promptBody.truncated,
|
|
267
|
+
maxBytes: promptBody.maxBytes,
|
|
268
|
+
source: promptSource,
|
|
269
|
+
},
|
|
270
|
+
executorInput: {
|
|
271
|
+
kind,
|
|
272
|
+
skills,
|
|
273
|
+
modelHint: resolveModelHint(spec, task),
|
|
274
|
+
shell,
|
|
275
|
+
static: staticBlock,
|
|
276
|
+
},
|
|
277
|
+
boundaries: {
|
|
278
|
+
allowedPaths: redactStringList(task.allowedPaths),
|
|
279
|
+
forbiddenPaths: redactStringList(task.forbiddenPaths),
|
|
280
|
+
writeSet: redactStringList(task.writeSet),
|
|
281
|
+
outputContract: typeof task.outputContract === "string"
|
|
282
|
+
? truncatedField(task.outputContract).text
|
|
283
|
+
: null,
|
|
284
|
+
outputMode: typeof task.outputMode === "string" ? task.outputMode : null,
|
|
285
|
+
firstProtocolLine: typeof task.firstProtocolLine === "string"
|
|
286
|
+
? redactSecrets(task.firstProtocolLine)
|
|
287
|
+
: null,
|
|
288
|
+
},
|
|
289
|
+
promptFingerprint: fingerprint,
|
|
290
|
+
warnings,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async function loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings) {
|
|
294
|
+
const resolved = resolveDagNodePromptRedacted(repoRoot, dagRunId, nodeId);
|
|
295
|
+
if (!resolved.ok) {
|
|
296
|
+
if (resolved.reason === "ambiguous") {
|
|
297
|
+
warnings.push("Ambiguous prompt.redacted.md lifecycle copies.");
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
exists: false,
|
|
301
|
+
sha256Prefix: null,
|
|
302
|
+
lengthChars: null,
|
|
303
|
+
artifact: null,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const artifact = path.posix.join(nodeId, "prompt.redacted.md");
|
|
307
|
+
try {
|
|
308
|
+
const markdown = await readFile(resolved.result.absolutePath, "utf-8");
|
|
309
|
+
const parsed = parsePromptRedactedFingerprint(markdown);
|
|
310
|
+
if (!parsed.ok) {
|
|
311
|
+
warnings.push(`prompt.redacted.md parse failed: ${parsed.error}`);
|
|
312
|
+
return {
|
|
313
|
+
exists: true,
|
|
314
|
+
sha256Prefix: null,
|
|
315
|
+
lengthChars: null,
|
|
316
|
+
artifact,
|
|
317
|
+
parseError: parsed.error,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
exists: true,
|
|
322
|
+
sha256Prefix: parsed.sha256Prefix,
|
|
323
|
+
lengthChars: parsed.lengthChars,
|
|
324
|
+
artifact,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
warnings.push("prompt.redacted.md could not be read.");
|
|
329
|
+
return {
|
|
330
|
+
exists: true,
|
|
331
|
+
sha256Prefix: null,
|
|
332
|
+
lengthChars: null,
|
|
333
|
+
artifact,
|
|
334
|
+
parseError: "read-failed",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Build schemaVersion 1 node-input projection for Observe / Inspect.
|
|
340
|
+
* Never returns the raw task object.
|
|
341
|
+
*/
|
|
342
|
+
export async function buildDagNodeInput(repoRoot, dagRunId, nodeId) {
|
|
343
|
+
const runResolved = resolveDagRunJson(repoRoot, dagRunId);
|
|
344
|
+
if (!runResolved.ok) {
|
|
345
|
+
if (runResolved.reason === "invalid-id") {
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
status: 400,
|
|
349
|
+
body: { error: "Invalid dag run or node identifier" },
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (runResolved.reason === "ambiguous") {
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
status: 409,
|
|
356
|
+
body: { error: "Ambiguous dag run lifecycle" },
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (runResolved.reason === "not-found") {
|
|
360
|
+
return {
|
|
361
|
+
ok: false,
|
|
362
|
+
status: 404,
|
|
363
|
+
body: { error: "DAG run not found" },
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
status: 400,
|
|
369
|
+
body: { error: "Invalid or unsafe path" },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
let raw;
|
|
373
|
+
try {
|
|
374
|
+
raw = JSON.parse(await readFile(runResolved.result.absolutePath, "utf-8"));
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
status: 422,
|
|
380
|
+
body: { error: "run.json is unreadable or invalid JSON" },
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
status: 422,
|
|
387
|
+
body: { error: "run.json is not a DAG spec object" },
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
const spec = raw;
|
|
391
|
+
const tasks = Array.isArray(spec.tasks) ? spec.tasks : null;
|
|
392
|
+
if (!tasks) {
|
|
393
|
+
return {
|
|
394
|
+
ok: true,
|
|
395
|
+
status: 200,
|
|
396
|
+
body: unavailableBody(dagRunId, nodeId, "run-spec-unavailable", [
|
|
397
|
+
"run.json has no tasks[] array.",
|
|
398
|
+
]),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const taskRecords = tasks.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
|
|
402
|
+
const task = taskRecords.find((item) => typeof item.id === "string" && item.id === nodeId);
|
|
403
|
+
if (!task) {
|
|
404
|
+
if (isLikelyDynamicExpandedNode(nodeId, taskRecords)) {
|
|
405
|
+
return {
|
|
406
|
+
ok: true,
|
|
407
|
+
status: 200,
|
|
408
|
+
body: unavailableBody(dagRunId, nodeId, "unsupported-dynamic-node", [
|
|
409
|
+
"Runtime-expanded node input is not projected in schemaVersion 1.",
|
|
410
|
+
]),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
status: 200,
|
|
416
|
+
body: unavailableBody(dagRunId, nodeId, "task-not-found"),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
const warnings = [];
|
|
420
|
+
const fingerprint = await loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings);
|
|
421
|
+
// Reject unknown nested leakage by projecting only through allowlist helpers.
|
|
422
|
+
const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, warnings);
|
|
423
|
+
// Defense-in-depth: ensure raw task never sneaks in.
|
|
424
|
+
if ("task" in body) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
status: 422,
|
|
428
|
+
body: { error: "projection leaked raw task" },
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return { ok: true, status: 200, body };
|
|
432
|
+
}
|
|
433
|
+
/** Test helper: ensure response JSON never contains raw task keys. */
|
|
434
|
+
export function assertNoRawTaskLeak(body) {
|
|
435
|
+
if (!body || typeof body !== "object")
|
|
436
|
+
return true;
|
|
437
|
+
const record = body;
|
|
438
|
+
if ("task" in record)
|
|
439
|
+
return false;
|
|
440
|
+
if ("subtask_prompt" in record)
|
|
441
|
+
return false;
|
|
442
|
+
const serialized = JSON.stringify(body);
|
|
443
|
+
return !serialized.includes('"subtask_prompt":');
|
|
444
|
+
}
|
|
@@ -13,6 +13,7 @@ import { dagSourceBindingSchema } from "../../workflows/dag/types.js";
|
|
|
13
13
|
import { getTaskPoolRoot } from "../pool/run-store.js";
|
|
14
14
|
import { isAllowedArtifactTextPath, resolveArtifactPath, resolveRepoFilePreview, toRepoRelativeArtifactPath, } from "./paths.js";
|
|
15
15
|
import { extractSpecEvidence, extractSpecReadContent, } from "./spec-evidence.js";
|
|
16
|
+
import { buildDagNodeInput } from "./node-input.js";
|
|
16
17
|
import { buildObserveHealthV1 } from "./health.js";
|
|
17
18
|
const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
18
19
|
export function createObserveSnapshotCache() {
|
|
@@ -86,6 +87,11 @@ export const ROUTES = [
|
|
|
86
87
|
pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/session-events$/,
|
|
87
88
|
handler: handleDagNodeSessionEvents,
|
|
88
89
|
},
|
|
90
|
+
{
|
|
91
|
+
method: "GET",
|
|
92
|
+
pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/input$/,
|
|
93
|
+
handler: handleDagNodeInput,
|
|
94
|
+
},
|
|
89
95
|
{
|
|
90
96
|
method: "GET",
|
|
91
97
|
pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/spec-evidence$/,
|
|
@@ -455,6 +461,17 @@ async function handleDagRunById(_req, res, match, ctx) {
|
|
|
455
461
|
}
|
|
456
462
|
sendJson(res, 200, dagRun);
|
|
457
463
|
}
|
|
464
|
+
async function handleDagNodeInput(_req, res, match, ctx) {
|
|
465
|
+
const dagRunId = match.params.id;
|
|
466
|
+
const nodeId = match.params.sub;
|
|
467
|
+
if (!isSafeObservabilityIdentifier(dagRunId) ||
|
|
468
|
+
!isSafeObservabilityIdentifier(nodeId)) {
|
|
469
|
+
sendJson(res, 400, { error: "Invalid dag run or node identifier" });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const result = await buildDagNodeInput(ctx.repoRoot, dagRunId, nodeId);
|
|
473
|
+
sendJson(res, result.status, result.body);
|
|
474
|
+
}
|
|
458
475
|
async function handleDagNodeSessionEvents(_req, res, match, ctx) {
|
|
459
476
|
const dagRunId = match.params.id;
|
|
460
477
|
const nodeId = match.params.sub;
|
|
@@ -29,6 +29,15 @@ export async function fetchJsonResult(url) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Fetch a dag node input projection. Preserves HTTP status so callers can
|
|
34
|
+
* distinguish missing runs, unavailable nodes, and transport errors.
|
|
35
|
+
*/
|
|
36
|
+
export async function fetchDagNodeInput(runId, nodeId) {
|
|
37
|
+
const url = `/api/dag-runs/${encodeURIComponent(runId)}/nodes/${encodeURIComponent(nodeId)}/input`;
|
|
38
|
+
return fetchJsonResult(url);
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
/**
|
|
33
42
|
* Fetch a spec-evidence file preview (binding source or successful read).
|
|
34
43
|
* Resolves to the parsed JSON body, or throws an Error carrying the HTTP
|
|
@@ -33,6 +33,15 @@ export const UI_TEXT = {
|
|
|
33
33
|
noBatches: "暂无 Worker 批次,运行后将出现在这里。",
|
|
34
34
|
noNodes: "暂无节点。",
|
|
35
35
|
noOutput: "暂无节点输出。",
|
|
36
|
+
noInput: "暂无节点输入。",
|
|
37
|
+
inputLoading: "正在加载节点输入…",
|
|
38
|
+
inputUnavailable: "未在冻结 DAG tasks[] 中找到该节点输入。",
|
|
39
|
+
inputDynamicUnsupported:
|
|
40
|
+
"该节点为运行时动态展开节点,Phase A 暂不提供完整输入。",
|
|
41
|
+
inputRunSpecUnavailable: "无法读取冻结 DAG 规格(run.json)。",
|
|
42
|
+
inputError: "加载节点输入失败。",
|
|
43
|
+
inputFrozenLabel: "冻结 DAG 节点定义",
|
|
44
|
+
inputFingerprintNote: "仅保存 assembled prompt 指纹,全文未持久化",
|
|
36
45
|
noSessionEvents: "该节点暂无过程日志",
|
|
37
46
|
noArtifacts: "暂无产物记录。",
|
|
38
47
|
noActiveCommand: "当前没有正在执行的命令。",
|
|
@@ -34,6 +34,13 @@ export const uiState = {
|
|
|
34
34
|
// previous node can never mutate the current node's event cache.
|
|
35
35
|
sessionEventIdentity: null,
|
|
36
36
|
sessionEventIdentitySeq: 0,
|
|
37
|
+
/** Node-input request identity ("<dagRunId>:<nodeId>#<seq>"). */
|
|
38
|
+
dagNodeInputIdentity: null,
|
|
39
|
+
dagNodeInputRequestSeq: 0,
|
|
40
|
+
/** Last successful / in-flight node-input payload for the active identity. */
|
|
41
|
+
dagNodeInputData: null,
|
|
42
|
+
dagNodeInputError: null,
|
|
43
|
+
dagNodeInputLoading: false,
|
|
37
44
|
/** When true,「执行过程」展开 message_start/end 原始协议事件。 */
|
|
38
45
|
sessionTimelineShowProtocol: false,
|
|
39
46
|
/** Open <details> keys restored across timeline re-renders. */
|
|
@@ -45,6 +52,7 @@ export const uiState = {
|
|
|
45
52
|
dagGraphViewportState: null,
|
|
46
53
|
dagTimelineViewportState: null,
|
|
47
54
|
dagNodeOutputViewportState: null,
|
|
55
|
+
dagNodeInputViewportState: null,
|
|
48
56
|
runOutputViewportState: null,
|
|
49
57
|
/**
|
|
50
58
|
* Spec-evidence file preview drill-down. null = list view; otherwise
|
|
@@ -65,6 +73,12 @@ export function makeSessionEventIdentity(dagRunId, nodeId) {
|
|
|
65
73
|
return `${dagRunId}:${nodeId}#${uiState.sessionEventIdentitySeq}`;
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
/** Build a fresh node-input identity token and bump its sequence. */
|
|
77
|
+
export function makeDagNodeInputIdentity(dagRunId, nodeId) {
|
|
78
|
+
uiState.dagNodeInputRequestSeq += 1;
|
|
79
|
+
return `${dagRunId}:${nodeId}#${uiState.dagNodeInputRequestSeq}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
export function isPageVisible() {
|
|
69
83
|
return (
|
|
70
84
|
typeof document === "undefined" || document.visibilityState !== "hidden"
|
|
@@ -2010,6 +2010,80 @@ body.is-resizing-dag-graph {
|
|
|
2010
2010
|
.spec-evidence-section { padding-bottom: var(--space-4); border-bottom: 1px solid var(--hairline); }
|
|
2011
2011
|
.spec-evidence-section + .spec-evidence-section { margin-top: var(--space-4); }
|
|
2012
2012
|
.spec-evidence-section h4 { margin: 0 0 var(--space-2); color: var(--ink); font-size: 12px; font-weight: 700; }
|
|
2013
|
+
|
|
2014
|
+
.node-input-root { display: flex; flex-direction: column; gap: var(--space-3); }
|
|
2015
|
+
.node-input-semantics {
|
|
2016
|
+
margin: 0;
|
|
2017
|
+
font-size: 12px;
|
|
2018
|
+
font-weight: 600;
|
|
2019
|
+
color: var(--muted);
|
|
2020
|
+
}
|
|
2021
|
+
.node-input-section {
|
|
2022
|
+
padding-bottom: var(--space-3);
|
|
2023
|
+
border-bottom: 1px solid var(--hairline);
|
|
2024
|
+
}
|
|
2025
|
+
.node-input-section h4 {
|
|
2026
|
+
margin: 0 0 var(--space-2);
|
|
2027
|
+
color: var(--ink);
|
|
2028
|
+
font-size: 12px;
|
|
2029
|
+
font-weight: 700;
|
|
2030
|
+
}
|
|
2031
|
+
.node-input-section h5 {
|
|
2032
|
+
margin: var(--space-2) 0;
|
|
2033
|
+
font-size: 12px;
|
|
2034
|
+
font-weight: 600;
|
|
2035
|
+
}
|
|
2036
|
+
.node-input-summary {
|
|
2037
|
+
display: grid;
|
|
2038
|
+
grid-template-columns: max-content 1fr;
|
|
2039
|
+
gap: 4px 12px;
|
|
2040
|
+
margin: 0;
|
|
2041
|
+
font-size: 12px;
|
|
2042
|
+
}
|
|
2043
|
+
.node-input-summary dt {
|
|
2044
|
+
margin: 0;
|
|
2045
|
+
color: var(--muted);
|
|
2046
|
+
font-weight: 600;
|
|
2047
|
+
}
|
|
2048
|
+
.node-input-summary dd {
|
|
2049
|
+
margin: 0;
|
|
2050
|
+
color: var(--ink);
|
|
2051
|
+
word-break: break-word;
|
|
2052
|
+
}
|
|
2053
|
+
.node-input-inherited {
|
|
2054
|
+
display: inline-block;
|
|
2055
|
+
margin-left: 4px;
|
|
2056
|
+
padding: 0 6px;
|
|
2057
|
+
border-radius: 999px;
|
|
2058
|
+
background: var(--surface-2, #f3f1ec);
|
|
2059
|
+
color: var(--muted);
|
|
2060
|
+
font-size: 11px;
|
|
2061
|
+
font-weight: 600;
|
|
2062
|
+
}
|
|
2063
|
+
.node-input-code-list,
|
|
2064
|
+
.node-input-command-list {
|
|
2065
|
+
margin: var(--space-2) 0 0;
|
|
2066
|
+
padding-left: 1.2rem;
|
|
2067
|
+
}
|
|
2068
|
+
.node-input-command-list pre {
|
|
2069
|
+
margin: 4px 0;
|
|
2070
|
+
padding: 8px;
|
|
2071
|
+
overflow: auto;
|
|
2072
|
+
background: var(--surface-2, #f3f1ec);
|
|
2073
|
+
border-radius: 6px;
|
|
2074
|
+
font-size: 12px;
|
|
2075
|
+
}
|
|
2076
|
+
.node-input-truncated,
|
|
2077
|
+
.node-input-warning {
|
|
2078
|
+
margin: var(--space-2) 0 0;
|
|
2079
|
+
font-size: 12px;
|
|
2080
|
+
color: var(--muted);
|
|
2081
|
+
}
|
|
2082
|
+
.node-input-error {
|
|
2083
|
+
margin: 0;
|
|
2084
|
+
color: var(--danger, #b42318);
|
|
2085
|
+
font-size: 13px;
|
|
2086
|
+
}
|
|
2013
2087
|
.spec-evidence-summary { margin: var(--space-2) 0 0; color: var(--body); font-size: 12px; line-height: 1.6; }
|
|
2014
2088
|
.spec-evidence-list { display: grid; gap: 6px; margin: var(--space-2) 0 0; padding: 0; list-style: none; }
|
|
2015
2089
|
.spec-evidence-list li { display: flex; align-items: flex-start; gap: 6px; color: var(--body); font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
|