@gajae-code/coding-agent 0.17.4 → 0.17.6
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 +82 -0
- package/dist/types/config/autorouting-tier-map.d.ts +1031 -3
- package/dist/types/config/file-lock.d.ts +4 -2
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
- package/dist/types/harness-control-plane/finalize.d.ts +14 -3
- package/dist/types/main.d.ts +1 -1
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +13 -6
- package/dist/types/sdk/broker/broker.d.ts +4 -2
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/identity.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +11 -3
- package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
- package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +8 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -1
- package/dist/types/sdk/prompt-deadline-flush.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +7 -2
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tools/bash-interactive.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +10 -1
- package/dist/types/tools/subagent.d.ts +8 -1
- package/package.json +7 -7
- package/src/cli/public-command-registry.ts +8 -2
- package/src/cli/setup-cli.ts +1 -0
- package/src/cli/stats-cli.ts +11 -1
- package/src/config/autorouting-tier-map.ts +529 -3
- package/src/config/file-lock.ts +59 -14
- package/src/config/model-discovery-manager.ts +4 -4
- package/src/config/model-profile-activation.ts +83 -23
- package/src/config/model-registry.ts +72 -12
- package/src/config/model-resolver.ts +6 -1
- package/src/coordinator-mcp/question-state.ts +4 -1
- package/src/coordinator-mcp/server.ts +14 -6
- package/src/exec/bash-executor.ts +3 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
- package/src/gjc-runtime/state-writer.ts +317 -6
- package/src/harness-control-plane/control-endpoint.ts +24 -6
- package/src/harness-control-plane/finalize.ts +312 -25
- package/src/harness-control-plane/owner.ts +323 -89
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/main.ts +69 -19
- package/src/modes/acp/acp-agent.ts +581 -37
- package/src/modes/components/gjc-bundle-settings.ts +21 -1
- package/src/modes/components/tool-status-header.ts +72 -6
- package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
- package/src/runtime-mcp/loader.ts +6 -3
- package/src/runtime-mcp/manager.ts +177 -75
- package/src/sdk/broker/broker.ts +1058 -150
- package/src/sdk/broker/ensure.ts +65 -8
- package/src/sdk/broker/identity.ts +13 -0
- package/src/sdk/broker/lifecycle-ledger.ts +16 -4
- package/src/sdk/broker/managed-task-dag.ts +1927 -0
- package/src/sdk/broker/managed-task-verification.ts +642 -0
- package/src/sdk/broker/spawn-authority.ts +140 -7
- package/src/sdk/broker/startup-failure.ts +80 -1
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +22 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +5 -1
- package/src/sdk/cli/session-cli.ts +18 -0
- package/src/sdk/host/session-runtime.test.ts +46 -0
- package/src/sdk/host/session-runtime.ts +13 -6
- package/src/sdk/lifecycle/service.ts +2 -0
- package/src/sdk/prompt-deadline-flush.ts +26 -4
- package/src/sdk/session.ts +587 -115
- package/src/session/agent-session.ts +60 -14
- package/src/session/internal/managed-session-scope.ts +10 -0
- package/src/session/internal/managed-session-storage.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/executor.ts +46 -17
- package/src/task/types.ts +2 -0
- package/src/tools/bash-interactive.ts +2 -0
- package/src/tools/bash.ts +20 -0
- package/src/tools/fetch.ts +21 -5
- package/src/tools/subagent-render.ts +24 -1
- package/src/tools/subagent.ts +27 -1
- package/src/utils/mupdf.ts +11 -4
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { StatePublicationUncertainError, StateWriteConflictError } from "../../gjc-runtime/state-writer";
|
|
3
|
+
import {
|
|
4
|
+
defaultFinalizeChecks,
|
|
5
|
+
type FinalizeChecks,
|
|
6
|
+
ValidationObservationUncertainError,
|
|
7
|
+
type ValidationRun,
|
|
8
|
+
} from "../../harness-control-plane/finalize";
|
|
9
|
+
import {
|
|
10
|
+
buildReceipt,
|
|
11
|
+
type ReceiptEnvelope,
|
|
12
|
+
type ValidationEvidence,
|
|
13
|
+
validateReceipt,
|
|
14
|
+
} from "../../harness-control-plane/receipts";
|
|
15
|
+
import {
|
|
16
|
+
assertManagedManifestCurrent,
|
|
17
|
+
captureManagedManifest,
|
|
18
|
+
type ManagedDomainBinding,
|
|
19
|
+
type ManagedManifest,
|
|
20
|
+
type ManagedTaskAttempt,
|
|
21
|
+
type ManagedTaskDefinition,
|
|
22
|
+
type ManagedVerificationObservation,
|
|
23
|
+
resourceCoversPath,
|
|
24
|
+
transactManagedTaskDomain,
|
|
25
|
+
within,
|
|
26
|
+
} from "./managed-task-dag";
|
|
27
|
+
|
|
28
|
+
const VERIFIER_SOURCE = "managed-task-verification";
|
|
29
|
+
|
|
30
|
+
export type ManagedVerificationStatus =
|
|
31
|
+
| { status: "running"; executionId: string; started: false }
|
|
32
|
+
| { status: "unknown"; executionId: string; started: boolean; reason?: string }
|
|
33
|
+
| { status: "finished"; executionId: string; started: false; accepted: boolean }
|
|
34
|
+
| { status: "accepted"; executionId: string; started: boolean; acceptedId: string; acceptedHash: string }
|
|
35
|
+
| { status: "failed"; executionId: string; started: boolean; reason: string };
|
|
36
|
+
|
|
37
|
+
function requirePolicy(condition: unknown, message: string): asserts condition {
|
|
38
|
+
if (!condition) throw new Error(message);
|
|
39
|
+
}
|
|
40
|
+
function isCorruptAuthority(error: unknown): boolean {
|
|
41
|
+
return error instanceof Error && /corrupt authority|established authority missing/.test(error.message);
|
|
42
|
+
}
|
|
43
|
+
function isStaleAcceptedEvidence(error: unknown): boolean {
|
|
44
|
+
return error instanceof Error && error.message === "managed-task: content drift";
|
|
45
|
+
}
|
|
46
|
+
function isPublicationUncertainty(error: unknown): boolean {
|
|
47
|
+
return (
|
|
48
|
+
error instanceof ValidationObservationUncertainError ||
|
|
49
|
+
error instanceof StatePublicationUncertainError ||
|
|
50
|
+
error instanceof StateWriteConflictError ||
|
|
51
|
+
isCorruptAuthority(error)
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
function isKnownPolicyRefusal(error: unknown): boolean {
|
|
55
|
+
if (!(error instanceof Error) || isPublicationUncertainty(error)) return false;
|
|
56
|
+
return (
|
|
57
|
+
isStaleAcceptedEvidence(error) ||
|
|
58
|
+
error.message === "validation output escaped declared Q" ||
|
|
59
|
+
error.message === "attempt is fenced" ||
|
|
60
|
+
error.message === "attempt definition is not current" ||
|
|
61
|
+
error.message === "missing frozen verification manifests" ||
|
|
62
|
+
error.message === "missing terminal observations" ||
|
|
63
|
+
error.message === "validation command failed" ||
|
|
64
|
+
error.message === "receipt count mismatch" ||
|
|
65
|
+
error.message === "receipt evidence is not trusted" ||
|
|
66
|
+
error.message === "receipt binding mismatch"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function unknownStatus(executionId: string, _error: unknown, started = true): ManagedVerificationStatus {
|
|
71
|
+
return {
|
|
72
|
+
status: "unknown",
|
|
73
|
+
executionId,
|
|
74
|
+
started,
|
|
75
|
+
reason: "managed-task: verification unknown",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function publicFailureReason(error: unknown): string {
|
|
79
|
+
if (!(error instanceof Error)) return "verification failed";
|
|
80
|
+
if (
|
|
81
|
+
/corrupt authority|established authority missing|path is outside enrolled worktrees|content drift|resource conflict|untrusted acceptance|domain binding mismatch/.test(
|
|
82
|
+
error.message,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
return "managed-task: verification refused";
|
|
86
|
+
if (error.message.startsWith("managed-task:")) return "managed-task: verification refused";
|
|
87
|
+
return "verification failed";
|
|
88
|
+
}
|
|
89
|
+
function canonicalArtifactPath(
|
|
90
|
+
attempt: ManagedTaskAttempt,
|
|
91
|
+
artifact: ManagedTaskDefinition["artifacts"][number],
|
|
92
|
+
): string {
|
|
93
|
+
return (
|
|
94
|
+
attempt.resources.find(
|
|
95
|
+
resource => resource.declaration.kind === "path" && resource.declaration.path === artifact.path,
|
|
96
|
+
)?.path ?? artifact.path
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
function declaredRoleRoots(
|
|
100
|
+
attempt: ManagedTaskAttempt,
|
|
101
|
+
roles: ReadonlyArray<ManagedTaskDefinition["artifacts"][number]["role"]>,
|
|
102
|
+
): string[] {
|
|
103
|
+
return [
|
|
104
|
+
...new Set(
|
|
105
|
+
attempt.definition.artifacts
|
|
106
|
+
.filter(artifact => roles.includes(artifact.role))
|
|
107
|
+
.map(artifact => canonicalArtifactPath(attempt, artifact)),
|
|
108
|
+
),
|
|
109
|
+
].sort();
|
|
110
|
+
}
|
|
111
|
+
function pathInRoleTree(roots: string[], targetPath: string): boolean {
|
|
112
|
+
return roots.some(root => targetPath === root || within(root, targetPath));
|
|
113
|
+
}
|
|
114
|
+
function artifactAuthorized(attempt: ManagedTaskAttempt, targetPath: string, needed: "read" | "write"): boolean {
|
|
115
|
+
return attempt.resources.some(resource => resourceCoversPath(resource, targetPath, needed));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function producedArtifacts(definition: ManagedTaskDefinition) {
|
|
119
|
+
return definition.artifacts.filter(artifact => artifact.role === "output" || artifact.role === "read-write");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function readOnlyInputManifest(attempt: ManagedTaskAttempt): ManagedManifest {
|
|
123
|
+
const inputRoots = declaredRoleRoots(attempt, ["input"]);
|
|
124
|
+
const predecessor = new Set(attempt.predecessors.flatMap(item => item.produced.map(entry => entry.path)));
|
|
125
|
+
return attempt.inputs.filter(
|
|
126
|
+
entry =>
|
|
127
|
+
pathInRoleTree(inputRoots, entry.path) ||
|
|
128
|
+
predecessor.has(entry.path) ||
|
|
129
|
+
[...predecessor].some(parent => within(parent, entry.path)),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
function producedPaths(attempt: ManagedTaskAttempt): string[] {
|
|
133
|
+
const roots = declaredRoleRoots(attempt, ["output", "read-write"]);
|
|
134
|
+
for (const root of roots)
|
|
135
|
+
requirePolicy(artifactAuthorized(attempt, root, "write"), "produced artifact is not reserved");
|
|
136
|
+
return roots;
|
|
137
|
+
}
|
|
138
|
+
function validationOutputRoots(attempt: ManagedTaskAttempt): string[] {
|
|
139
|
+
const roots = declaredRoleRoots(attempt, ["validation-output"]);
|
|
140
|
+
for (const root of roots)
|
|
141
|
+
requirePolicy(artifactAuthorized(attempt, root, "write"), "validation output is not reserved");
|
|
142
|
+
return roots;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function capturePaths(binding: ManagedDomainBinding, paths: string[]): Promise<ManagedManifest> {
|
|
146
|
+
if (paths.length === 0) return [];
|
|
147
|
+
return captureManagedManifest(binding, paths);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function assertPresence(
|
|
151
|
+
definition: ManagedTaskDefinition,
|
|
152
|
+
produced: ManagedManifest,
|
|
153
|
+
attempt: ManagedTaskAttempt,
|
|
154
|
+
): void {
|
|
155
|
+
for (const artifact of producedArtifacts(definition)) {
|
|
156
|
+
const canonical = canonicalArtifactPath(attempt, artifact);
|
|
157
|
+
const entry = produced.find(item => item.path === canonical);
|
|
158
|
+
if (artifact.presence === "required") requirePolicy(entry && entry.kind !== "absent", "absent required output");
|
|
159
|
+
if (artifact.presence === "absent")
|
|
160
|
+
requirePolicy(!entry || entry.kind === "absent", "output presence contract failed");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function currentNodeDefinition(
|
|
165
|
+
graphs: Array<{
|
|
166
|
+
id: string;
|
|
167
|
+
nodes?: Array<{ definition: { id: string }; hash: string; revision: number }>;
|
|
168
|
+
}>,
|
|
169
|
+
graphId: string,
|
|
170
|
+
nodeId: string,
|
|
171
|
+
): { hash: string; revision: number } | undefined {
|
|
172
|
+
return graphs.find(item => item.id === graphId)?.nodes?.find(node => node.definition.id === nodeId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function findAttempt(
|
|
176
|
+
stateGraphs: Array<{
|
|
177
|
+
id: string;
|
|
178
|
+
owner: string;
|
|
179
|
+
attempts: ManagedTaskAttempt[];
|
|
180
|
+
nodes?: Array<{ definition: { id: string }; hash: string; revision: number }>;
|
|
181
|
+
}>,
|
|
182
|
+
graphId: string,
|
|
183
|
+
nodeId: string,
|
|
184
|
+
owner: string,
|
|
185
|
+
): { graph: { id: string; owner: string; attempts: ManagedTaskAttempt[] }; attempt: ManagedTaskAttempt } {
|
|
186
|
+
const graph = stateGraphs.find(item => item.id === graphId);
|
|
187
|
+
requirePolicy(graph && graph.owner === owner, "graph owner mismatch");
|
|
188
|
+
const current = currentNodeDefinition(stateGraphs, graphId, nodeId);
|
|
189
|
+
requirePolicy(current, "node missing");
|
|
190
|
+
const attempt = [...graph.attempts]
|
|
191
|
+
.reverse()
|
|
192
|
+
.find(
|
|
193
|
+
item =>
|
|
194
|
+
item.nodeId === nodeId &&
|
|
195
|
+
item.taskRevision === current.revision &&
|
|
196
|
+
item.definitionHash === current.hash &&
|
|
197
|
+
item.fence === "current",
|
|
198
|
+
);
|
|
199
|
+
requirePolicy(attempt, "attempt missing");
|
|
200
|
+
return { graph, attempt };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function markManagedVerificationUnknown(
|
|
204
|
+
binding: ManagedDomainBinding,
|
|
205
|
+
graphId: string,
|
|
206
|
+
attemptId: string,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
try {
|
|
209
|
+
await transactManagedTaskDomain({ binding, internal: true }, async state => {
|
|
210
|
+
const attempt = state.graphs.find(graph => graph.id === graphId)?.attempts.find(item => item.id === attemptId);
|
|
211
|
+
requirePolicy(attempt, "attempt missing");
|
|
212
|
+
if (attempt.validation === "running") attempt.validation = "unknown";
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error instanceof Error && /corrupt authority|established authority missing/.test(error.message)) {
|
|
216
|
+
throw new Error("managed-task: corrupt authority");
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function reconcileRunningManagedVerification(binding: ManagedDomainBinding): Promise<void> {
|
|
223
|
+
await transactManagedTaskDomain({ binding, internal: true }, async state => {
|
|
224
|
+
for (const graph of state.graphs) {
|
|
225
|
+
for (const attempt of graph.attempts) {
|
|
226
|
+
if (attempt.validation === "running") attempt.validation = "unknown";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}).catch(error => {
|
|
230
|
+
if (error instanceof Error && /established authority missing|corrupt/.test(error.message)) return;
|
|
231
|
+
throw error;
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function startOrObserve(
|
|
236
|
+
binding: ManagedDomainBinding,
|
|
237
|
+
input: { graphId: string; nodeId: string; owner: string },
|
|
238
|
+
): Promise<{ attempt: ManagedTaskAttempt; started: boolean }> {
|
|
239
|
+
const result = await transactManagedTaskDomain({ binding, internal: true }, async state => {
|
|
240
|
+
const { attempt } = findAttempt(state.graphs, input.graphId, input.nodeId, input.owner);
|
|
241
|
+
requirePolicy(attempt.worker === "closed", "verification requires closed worker");
|
|
242
|
+
requirePolicy(attempt.fence === "current", "attempt is fenced");
|
|
243
|
+
requirePolicy(attempt.accepted === null || attempt.validation === "finished", "attempt already accepted");
|
|
244
|
+
if (attempt.accepted !== null && attempt.validation === "finished") {
|
|
245
|
+
await assertManagedManifestCurrent(binding, readOnlyInputManifest(attempt));
|
|
246
|
+
if (attempt.produced) await assertManagedManifestCurrent(binding, attempt.produced);
|
|
247
|
+
if (attempt.validationImmutable) await assertManagedManifestCurrent(binding, attempt.validationImmutable);
|
|
248
|
+
return { attempt: structuredClone(attempt), started: false };
|
|
249
|
+
}
|
|
250
|
+
if (attempt.validation === "running" || attempt.validation === "unknown" || attempt.validation === "finished") {
|
|
251
|
+
return { attempt: structuredClone(attempt), started: false };
|
|
252
|
+
}
|
|
253
|
+
requirePolicy(
|
|
254
|
+
attempt.validation === "not-started" && attempt.verificationExecution === null,
|
|
255
|
+
"verification already started",
|
|
256
|
+
);
|
|
257
|
+
requirePolicy(attempt.definition.validations.length > 0, "empty validation list");
|
|
258
|
+
await assertManagedManifestCurrent(binding, readOnlyInputManifest(attempt));
|
|
259
|
+
for (const predecessor of attempt.predecessors) await assertManagedManifestCurrent(binding, predecessor.produced);
|
|
260
|
+
const produced = await capturePaths(binding, producedPaths(attempt));
|
|
261
|
+
assertPresence(attempt.definition, produced, attempt);
|
|
262
|
+
const validationImmutable = [...readOnlyInputManifest(attempt), ...produced].sort((left, right) =>
|
|
263
|
+
left.path.localeCompare(right.path),
|
|
264
|
+
);
|
|
265
|
+
const validationOutputs = await capturePaths(binding, validationOutputRoots(attempt));
|
|
266
|
+
attempt.produced = produced;
|
|
267
|
+
attempt.validationImmutable = validationImmutable;
|
|
268
|
+
attempt.validationOutputs = validationOutputs;
|
|
269
|
+
attempt.verificationExecution = {
|
|
270
|
+
executionId: `verify-${randomUUID()}`,
|
|
271
|
+
workspace: attempt.definition.workspace,
|
|
272
|
+
commands: attempt.definition.validations.map(command => ({ name: command.name, command: command.command })),
|
|
273
|
+
observations: [],
|
|
274
|
+
receipts: [],
|
|
275
|
+
};
|
|
276
|
+
attempt.validation = "running";
|
|
277
|
+
return { attempt: structuredClone(attempt), started: true };
|
|
278
|
+
});
|
|
279
|
+
return result.result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function reconcileUncertainStart(
|
|
283
|
+
binding: ManagedDomainBinding,
|
|
284
|
+
input: { graphId: string; nodeId: string; owner: string },
|
|
285
|
+
): Promise<ManagedTaskAttempt> {
|
|
286
|
+
const result = await transactManagedTaskDomain({ binding, internal: true }, async state => {
|
|
287
|
+
const { attempt } = findAttempt(state.graphs, input.graphId, input.nodeId, input.owner);
|
|
288
|
+
if (attempt.validation === "running") attempt.validation = "unknown";
|
|
289
|
+
return structuredClone(attempt);
|
|
290
|
+
});
|
|
291
|
+
return result.result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function durableAttemptStatus(attempt: ManagedTaskAttempt, started: boolean): ManagedVerificationStatus {
|
|
295
|
+
const execution = attempt.verificationExecution;
|
|
296
|
+
requirePolicy(execution, "verification execution missing");
|
|
297
|
+
if (attempt.validation === "unknown") return { status: "unknown", executionId: execution.executionId, started };
|
|
298
|
+
if (attempt.validation === "running")
|
|
299
|
+
return { status: "running", executionId: execution.executionId, started: false };
|
|
300
|
+
if (attempt.accepted)
|
|
301
|
+
return {
|
|
302
|
+
status: "accepted",
|
|
303
|
+
executionId: execution.executionId,
|
|
304
|
+
started,
|
|
305
|
+
acceptedId: attempt.accepted.id,
|
|
306
|
+
acceptedHash: attempt.accepted.hash,
|
|
307
|
+
};
|
|
308
|
+
if (attempt.validation === "finished" && !attempt.accepted) {
|
|
309
|
+
const failedObservation = execution.observations.some(
|
|
310
|
+
observation => !observation.pass || observation.exitStatus !== 0,
|
|
311
|
+
);
|
|
312
|
+
return {
|
|
313
|
+
status: "failed",
|
|
314
|
+
executionId: execution.executionId,
|
|
315
|
+
started,
|
|
316
|
+
reason: failedObservation ? "validation command failed" : "verification failed",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
status: "finished",
|
|
321
|
+
executionId: execution.executionId,
|
|
322
|
+
started: false,
|
|
323
|
+
accepted: false,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function reconcileAttemptStatus(
|
|
328
|
+
binding: ManagedDomainBinding,
|
|
329
|
+
input: { graphId: string; attemptId: string; executionId: string; started: boolean },
|
|
330
|
+
): Promise<ManagedVerificationStatus> {
|
|
331
|
+
const result = await transactManagedTaskDomain({ binding, internal: true }, async state => {
|
|
332
|
+
const attempt = state.graphs
|
|
333
|
+
.find(graph => graph.id === input.graphId)
|
|
334
|
+
?.attempts.find(item => item.id === input.attemptId);
|
|
335
|
+
requirePolicy(attempt, "attempt missing");
|
|
336
|
+
if (attempt.validation === "running") attempt.validation = "unknown";
|
|
337
|
+
return structuredClone(attempt);
|
|
338
|
+
});
|
|
339
|
+
const attempt = result.result;
|
|
340
|
+
if (!attempt.verificationExecution)
|
|
341
|
+
return unknownStatus(input.executionId, new Error("verification execution missing"), input.started);
|
|
342
|
+
return durableAttemptStatus(attempt, input.started);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function appendObservation(
|
|
346
|
+
binding: ManagedDomainBinding,
|
|
347
|
+
input: { graphId: string; attemptId: string; observation: ManagedVerificationObservation },
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
await transactManagedTaskDomain({ binding, internal: true }, async state => {
|
|
350
|
+
const attempt = state.graphs
|
|
351
|
+
.find(graph => graph.id === input.graphId)
|
|
352
|
+
?.attempts.find(item => item.id === input.attemptId);
|
|
353
|
+
requirePolicy(attempt?.verificationExecution, "verification execution missing");
|
|
354
|
+
requirePolicy(attempt.validation === "running", "verification is not running");
|
|
355
|
+
requirePolicy(
|
|
356
|
+
attempt.verificationExecution.observations.length < attempt.verificationExecution.commands.length,
|
|
357
|
+
"unexpected extra observation",
|
|
358
|
+
);
|
|
359
|
+
const expected = attempt.verificationExecution.commands[attempt.verificationExecution.observations.length]!;
|
|
360
|
+
requirePolicy(
|
|
361
|
+
input.observation.name === expected.name && input.observation.command === expected.command,
|
|
362
|
+
"observation command mismatch",
|
|
363
|
+
);
|
|
364
|
+
attempt.verificationExecution.observations.push(input.observation);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function trustedObservation(
|
|
369
|
+
spec: { name: string; command: string },
|
|
370
|
+
workspace: string,
|
|
371
|
+
run: ValidationRun,
|
|
372
|
+
): ManagedVerificationObservation {
|
|
373
|
+
requirePolicy(run.exactCommand === spec.command, "exact command mismatch");
|
|
374
|
+
requirePolicy(run.cwd === workspace, "validation cwd mismatch");
|
|
375
|
+
return { name: spec.name, command: spec.command, cwd: run.cwd, exitStatus: run.exitStatus, pass: run.pass };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function trustedReceipt(
|
|
379
|
+
owner: string,
|
|
380
|
+
workspace: string,
|
|
381
|
+
executionId: string,
|
|
382
|
+
index: number,
|
|
383
|
+
spec: { name: string; command: string },
|
|
384
|
+
observation: ManagedVerificationObservation,
|
|
385
|
+
): ReceiptEnvelope<ValidationEvidence> {
|
|
386
|
+
requirePolicy(observation.exitStatus === 0 && observation.pass === true, "validation command failed");
|
|
387
|
+
const envelope = buildReceipt<ValidationEvidence>({
|
|
388
|
+
receiptId: `${executionId}:${index}`,
|
|
389
|
+
sessionId: owner,
|
|
390
|
+
family: "validation",
|
|
391
|
+
source: VERIFIER_SOURCE,
|
|
392
|
+
subject: { workspace, branch: null, head: null, commit: null },
|
|
393
|
+
evidence: {
|
|
394
|
+
command: spec.name,
|
|
395
|
+
exactCommand: observation.command,
|
|
396
|
+
cwd: observation.cwd,
|
|
397
|
+
exitStatus: observation.exitStatus,
|
|
398
|
+
pass: observation.pass,
|
|
399
|
+
commitUnderTest: null,
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
const outcome = validateReceipt(envelope);
|
|
403
|
+
requirePolicy(outcome.valid, "receipt envelope invalid");
|
|
404
|
+
requirePolicy(
|
|
405
|
+
envelope.sessionId === owner &&
|
|
406
|
+
envelope.subject.workspace === workspace &&
|
|
407
|
+
envelope.source === VERIFIER_SOURCE &&
|
|
408
|
+
envelope.family === "validation",
|
|
409
|
+
"receipt binding mismatch",
|
|
410
|
+
);
|
|
411
|
+
requirePolicy(
|
|
412
|
+
envelope.evidence.exactCommand === spec.command &&
|
|
413
|
+
envelope.evidence.cwd === workspace &&
|
|
414
|
+
envelope.evidence.exitStatus === 0 &&
|
|
415
|
+
envelope.evidence.pass === true,
|
|
416
|
+
"receipt evidence is not trusted",
|
|
417
|
+
);
|
|
418
|
+
return envelope;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function acceptExecution(
|
|
422
|
+
binding: ManagedDomainBinding,
|
|
423
|
+
input: { graphId: string; attemptId: string; owner: string; envelopes: ReceiptEnvelope<ValidationEvidence>[] },
|
|
424
|
+
): Promise<{ id: string; hash: string }> {
|
|
425
|
+
const result = await transactManagedTaskDomain({ binding, internal: true }, async state => {
|
|
426
|
+
const attempt = state.graphs
|
|
427
|
+
.find(graph => graph.id === input.graphId)
|
|
428
|
+
?.attempts.find(item => item.id === input.attemptId);
|
|
429
|
+
requirePolicy(attempt?.verificationExecution, "verification execution missing");
|
|
430
|
+
requirePolicy(attempt.fence === "current", "attempt is fenced");
|
|
431
|
+
const graph = state.graphs.find(item => item.id === input.graphId);
|
|
432
|
+
requirePolicy(
|
|
433
|
+
graph?.nodes.some(
|
|
434
|
+
node =>
|
|
435
|
+
node.definition.id === attempt.nodeId &&
|
|
436
|
+
node.hash === attempt.definitionHash &&
|
|
437
|
+
node.revision === attempt.taskRevision,
|
|
438
|
+
),
|
|
439
|
+
"attempt definition is not current",
|
|
440
|
+
);
|
|
441
|
+
requirePolicy(attempt.worker === "closed", "verification requires closed worker");
|
|
442
|
+
requirePolicy(attempt.validation === "running", "verification is not running");
|
|
443
|
+
requirePolicy(
|
|
444
|
+
attempt.produced && attempt.validationImmutable && attempt.validationOutputs,
|
|
445
|
+
"missing frozen verification manifests",
|
|
446
|
+
);
|
|
447
|
+
await assertManagedManifestCurrent(binding, attempt.validationImmutable);
|
|
448
|
+
await assertManagedManifestCurrent(binding, attempt.produced);
|
|
449
|
+
await assertManagedManifestCurrent(binding, readOnlyInputManifest(attempt));
|
|
450
|
+
for (const predecessor of attempt.predecessors) await assertManagedManifestCurrent(binding, predecessor.produced);
|
|
451
|
+
const qRoots = validationOutputRoots(attempt);
|
|
452
|
+
const finalQ = await capturePaths(binding, qRoots);
|
|
453
|
+
for (const entry of finalQ)
|
|
454
|
+
requirePolicy(
|
|
455
|
+
qRoots.some(root => entry.path === root || within(root, entry.path)),
|
|
456
|
+
"validation output escaped declared Q",
|
|
457
|
+
);
|
|
458
|
+
const execution = attempt.verificationExecution;
|
|
459
|
+
requirePolicy(execution.observations.length === execution.commands.length, "missing terminal observations");
|
|
460
|
+
requirePolicy(
|
|
461
|
+
execution.observations.every(observation => observation.pass && observation.exitStatus === 0),
|
|
462
|
+
"validation command failed",
|
|
463
|
+
);
|
|
464
|
+
requirePolicy(input.envelopes.length === execution.commands.length, "receipt count mismatch");
|
|
465
|
+
execution.receipts = input.envelopes.map(envelope => ({
|
|
466
|
+
id: envelope.receiptId,
|
|
467
|
+
hash: envelope.sha256,
|
|
468
|
+
envelope: { ...envelope },
|
|
469
|
+
}));
|
|
470
|
+
attempt.validationOutputs = finalQ;
|
|
471
|
+
attempt.validation = "finished";
|
|
472
|
+
const accepted = { id: input.envelopes[0]!.receiptId, hash: input.envelopes[0]!.sha256 };
|
|
473
|
+
attempt.accepted = accepted;
|
|
474
|
+
return accepted;
|
|
475
|
+
});
|
|
476
|
+
return result.result;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function failExecution(
|
|
480
|
+
binding: ManagedDomainBinding,
|
|
481
|
+
graphId: string,
|
|
482
|
+
attemptId: string,
|
|
483
|
+
reason: string,
|
|
484
|
+
): Promise<void> {
|
|
485
|
+
await transactManagedTaskDomain({ binding, internal: true }, async state => {
|
|
486
|
+
const attempt = state.graphs.find(graph => graph.id === graphId)?.attempts.find(item => item.id === attemptId);
|
|
487
|
+
requirePolicy(attempt, "attempt missing");
|
|
488
|
+
if (attempt.validation === "running") {
|
|
489
|
+
attempt.validation = "finished";
|
|
490
|
+
attempt.fence = attempt.fence === "current" ? "failed" : attempt.fence;
|
|
491
|
+
}
|
|
492
|
+
void reason;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export async function verifyManagedTaskAttempt(input: {
|
|
497
|
+
binding: ManagedDomainBinding;
|
|
498
|
+
graphId: string;
|
|
499
|
+
nodeId: string;
|
|
500
|
+
owner: string;
|
|
501
|
+
runner?: FinalizeChecks;
|
|
502
|
+
}): Promise<ManagedVerificationStatus> {
|
|
503
|
+
let started: { attempt: ManagedTaskAttempt; started: boolean };
|
|
504
|
+
try {
|
|
505
|
+
started = await startOrObserve(input.binding, {
|
|
506
|
+
graphId: input.graphId,
|
|
507
|
+
nodeId: input.nodeId,
|
|
508
|
+
owner: input.owner,
|
|
509
|
+
});
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (error instanceof StatePublicationUncertainError) {
|
|
512
|
+
try {
|
|
513
|
+
const attempt = await reconcileUncertainStart(input.binding, {
|
|
514
|
+
graphId: input.graphId,
|
|
515
|
+
nodeId: input.nodeId,
|
|
516
|
+
owner: input.owner,
|
|
517
|
+
});
|
|
518
|
+
if (attempt.validation !== "not-started") return durableAttemptStatus(attempt, false);
|
|
519
|
+
started = await startOrObserve(input.binding, {
|
|
520
|
+
graphId: input.graphId,
|
|
521
|
+
nodeId: input.nodeId,
|
|
522
|
+
owner: input.owner,
|
|
523
|
+
});
|
|
524
|
+
} catch (reconcileError) {
|
|
525
|
+
if (isPublicationUncertainty(reconcileError) || isCorruptAuthority(reconcileError))
|
|
526
|
+
return unknownStatus("unreadable", reconcileError, false);
|
|
527
|
+
throw reconcileError;
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
if (isCorruptAuthority(error)) return unknownStatus("unreadable", error, false);
|
|
531
|
+
if (isStaleAcceptedEvidence(error))
|
|
532
|
+
return {
|
|
533
|
+
status: "failed",
|
|
534
|
+
executionId: "accepted",
|
|
535
|
+
started: false,
|
|
536
|
+
reason: "managed-task: verification refused",
|
|
537
|
+
};
|
|
538
|
+
throw error;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
const execution = started.attempt.verificationExecution;
|
|
542
|
+
requirePolicy(execution, "verification execution missing");
|
|
543
|
+
if (!started.started) {
|
|
544
|
+
return durableAttemptStatus(started.attempt, false);
|
|
545
|
+
}
|
|
546
|
+
const workspace = execution.workspace;
|
|
547
|
+
const runner = input.runner ?? defaultFinalizeChecks(workspace);
|
|
548
|
+
const envelopes: ReceiptEnvelope<ValidationEvidence>[] = [];
|
|
549
|
+
try {
|
|
550
|
+
for (const [index, spec] of execution.commands.entries()) {
|
|
551
|
+
const run = await runner.runValidation(spec);
|
|
552
|
+
const observation = trustedObservation(spec, workspace, run);
|
|
553
|
+
await appendObservation(input.binding, { graphId: input.graphId, attemptId: started.attempt.id, observation });
|
|
554
|
+
if (!observation.pass || observation.exitStatus !== 0) {
|
|
555
|
+
await failExecution(input.binding, input.graphId, started.attempt.id, "validation command failed");
|
|
556
|
+
return {
|
|
557
|
+
status: "failed",
|
|
558
|
+
executionId: execution.executionId,
|
|
559
|
+
started: true,
|
|
560
|
+
reason: "validation command failed",
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
envelopes.push(trustedReceipt(input.owner, workspace, execution.executionId, index, spec, observation));
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const accepted = await acceptExecution(input.binding, {
|
|
567
|
+
graphId: input.graphId,
|
|
568
|
+
attemptId: started.attempt.id,
|
|
569
|
+
owner: input.owner,
|
|
570
|
+
envelopes,
|
|
571
|
+
});
|
|
572
|
+
return {
|
|
573
|
+
status: "accepted",
|
|
574
|
+
executionId: execution.executionId,
|
|
575
|
+
started: true,
|
|
576
|
+
acceptedId: accepted.id,
|
|
577
|
+
acceptedHash: accepted.hash,
|
|
578
|
+
};
|
|
579
|
+
} catch (error) {
|
|
580
|
+
if (isPublicationUncertainty(error) || isCorruptAuthority(error)) {
|
|
581
|
+
if (error instanceof StatePublicationUncertainError) {
|
|
582
|
+
try {
|
|
583
|
+
return await reconcileAttemptStatus(input.binding, {
|
|
584
|
+
graphId: input.graphId,
|
|
585
|
+
attemptId: started.attempt.id,
|
|
586
|
+
executionId: execution.executionId,
|
|
587
|
+
started: true,
|
|
588
|
+
});
|
|
589
|
+
} catch (reconcileError) {
|
|
590
|
+
return unknownStatus(execution.executionId, reconcileError);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
await markManagedVerificationUnknown(input.binding, input.graphId, started.attempt.id);
|
|
595
|
+
} catch (markError) {
|
|
596
|
+
if (isPublicationUncertainty(markError) || isCorruptAuthority(markError) || isCorruptAuthority(error))
|
|
597
|
+
return unknownStatus(execution.executionId, isCorruptAuthority(markError) ? markError : error);
|
|
598
|
+
throw markError;
|
|
599
|
+
}
|
|
600
|
+
return unknownStatus(execution.executionId, error);
|
|
601
|
+
}
|
|
602
|
+
if (isKnownPolicyRefusal(error) || isStaleAcceptedEvidence(error)) {
|
|
603
|
+
await failExecution(input.binding, input.graphId, started.attempt.id, publicFailureReason(error));
|
|
604
|
+
return {
|
|
605
|
+
status: "failed",
|
|
606
|
+
executionId: execution.executionId,
|
|
607
|
+
started: true,
|
|
608
|
+
reason: publicFailureReason(error),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
await markManagedVerificationUnknown(input.binding, input.graphId, started.attempt.id);
|
|
613
|
+
} catch (markError) {
|
|
614
|
+
if (isPublicationUncertainty(markError) || isCorruptAuthority(markError) || isCorruptAuthority(error))
|
|
615
|
+
return unknownStatus(execution.executionId, isCorruptAuthority(markError) ? markError : error);
|
|
616
|
+
throw markError;
|
|
617
|
+
}
|
|
618
|
+
return unknownStatus(execution.executionId, error);
|
|
619
|
+
}
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (error instanceof StatePublicationUncertainError) {
|
|
622
|
+
try {
|
|
623
|
+
return await reconcileAttemptStatus(input.binding, {
|
|
624
|
+
graphId: input.graphId,
|
|
625
|
+
attemptId: started.attempt.id,
|
|
626
|
+
executionId: execution.executionId,
|
|
627
|
+
started: true,
|
|
628
|
+
});
|
|
629
|
+
} catch (reconcileError) {
|
|
630
|
+
return unknownStatus(execution.executionId, reconcileError);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
await markManagedVerificationUnknown(input.binding, input.graphId, started.attempt.id);
|
|
635
|
+
} catch (markError) {
|
|
636
|
+
if (isPublicationUncertainty(markError) || isCorruptAuthority(markError) || isCorruptAuthority(error))
|
|
637
|
+
return unknownStatus(execution.executionId, isCorruptAuthority(markError) ? markError : error);
|
|
638
|
+
throw markError;
|
|
639
|
+
}
|
|
640
|
+
return unknownStatus(execution.executionId, error);
|
|
641
|
+
}
|
|
642
|
+
}
|