@tangle-network/agent-interface 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-candidate-execution-plan-schema.d.ts +50 -27
- package/dist/agent-candidate-execution-plan-schema.js +4 -16
- package/dist/agent-candidate-profile-schema.d.ts +5 -25
- package/dist/agent-candidate-promotion-schema.d.ts +280 -285
- package/dist/agent-candidate-receipt-schema.d.ts +50 -27
- package/dist/agent-candidate-schema.d.ts +5 -25
- package/dist/agent-candidate.d.ts +2 -8
- package/dist/agent-execution-preparation.d.ts +366 -0
- package/dist/agent-execution-preparation.js +736 -0
- package/dist/agent-profile-activation.d.ts +41 -0
- package/dist/agent-profile-activation.js +24 -0
- package/dist/agent-profile-canonical.d.ts +12 -0
- package/dist/agent-profile-canonical.js +79 -0
- package/dist/agent-profile-improvement-schema.d.ts +3 -6
- package/dist/agent-profile-improvement-schema.js +94 -56
- package/dist/agent-profile-improvement.d.ts +3 -3
- package/dist/agent-profile-materialization.d.ts +34 -0
- package/dist/agent-profile-materialization.js +243 -0
- package/dist/agent-profile.d.ts +44 -7
- package/dist/agent-profile.js +39 -2
- package/dist/agent-workspace-lease.d.ts +138 -0
- package/dist/agent-workspace-lease.js +203 -0
- package/dist/harness-capabilities.d.ts +1 -1
- package/dist/harness-capabilities.js +2 -9
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/profile-diff.d.ts +11 -0
- package/dist/profile-diff.js +123 -3
- package/dist/profile-schema.d.ts +142 -84
- package/dist/profile-schema.js +152 -41
- package/package.json +1 -1
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canonicalCandidateDigest, canonicalCandidateJson, isCanonicalJsonValue, isWellFormedUnicode, looksLikeCredential, omitTopLevelDigest, sha256DigestSchema, } from "./agent-candidate-schema-common.js";
|
|
3
|
+
import { canonicalAgentProfileValue } from "./agent-profile-canonical.js";
|
|
4
|
+
import { REASONING_EFFORTS, } from "./agent-profile.js";
|
|
5
|
+
import { AGENT_PROFILE_MATERIALIZATION_AXES, profileMaterializationRequests, } from "./agent-profile-materialization.js";
|
|
6
|
+
import { harnessTypeSchema } from "./harness.js";
|
|
7
|
+
import { agentProfileSchema, reasoningEffortSchema } from "./profile-schema.js";
|
|
8
|
+
import { agentWorkspaceLeaseRecordSchema, agentWorkspaceSourceSnapshotPolicySchema, } from "./agent-workspace-lease.js";
|
|
9
|
+
const nonBlankStringSchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.refine((value) => value.trim().length > 0, "value cannot be blank");
|
|
12
|
+
const publicMechanismIdentifierSchema = z
|
|
13
|
+
.string()
|
|
14
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/, "mechanism must be a public identifier, not launch arguments")
|
|
15
|
+
.refine((value) => !looksLikeCredential(value), "mechanism cannot carry credential-like material");
|
|
16
|
+
const publicLifecycleIdentifierSchema = z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._~:/+-]{0,499}$/, "lifecycle identity must be a public identifier")
|
|
19
|
+
.refine((value) => !looksLikeCredential(value), "lifecycle identity cannot carry credential-like material");
|
|
20
|
+
const publicReasonSchema = nonBlankStringSchema
|
|
21
|
+
.max(4_000)
|
|
22
|
+
.refine((value) => !looksLikeCredential(value), "reason cannot carry credential-like material");
|
|
23
|
+
const jsonPointerSchema = z
|
|
24
|
+
.string()
|
|
25
|
+
.refine(isCanonicalJsonPointer, "path must be a canonical RFC 6901 JSON Pointer");
|
|
26
|
+
export const agentExecutionPreparationAxisResultSchema = z
|
|
27
|
+
.strictObject({
|
|
28
|
+
axis: z.enum(AGENT_PROFILE_MATERIALIZATION_AXES),
|
|
29
|
+
disposition: z.enum([
|
|
30
|
+
"behavior",
|
|
31
|
+
"control",
|
|
32
|
+
"overridden",
|
|
33
|
+
"unsupported",
|
|
34
|
+
]),
|
|
35
|
+
owner: z.enum(["runtime", "executor"]),
|
|
36
|
+
mechanism: publicMechanismIdentifierSchema,
|
|
37
|
+
evidenceDigest: sha256DigestSchema.optional(),
|
|
38
|
+
path: jsonPointerSchema.optional(),
|
|
39
|
+
reason: publicReasonSchema.optional(),
|
|
40
|
+
})
|
|
41
|
+
.superRefine((result, context) => {
|
|
42
|
+
const requiresReason = result.disposition === "overridden" ||
|
|
43
|
+
result.disposition === "unsupported";
|
|
44
|
+
if (requiresReason && result.reason === undefined) {
|
|
45
|
+
context.addIssue({
|
|
46
|
+
code: "custom",
|
|
47
|
+
path: ["reason"],
|
|
48
|
+
message: `${result.disposition} profile coverage requires a reason`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!requiresReason && result.reason !== undefined) {
|
|
52
|
+
context.addIssue({
|
|
53
|
+
code: "custom",
|
|
54
|
+
path: ["reason"],
|
|
55
|
+
message: `${result.disposition} profile coverage cannot carry an override reason`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
export const agentExecutionPreparationReasoningEffortSchema = z
|
|
60
|
+
.strictObject({
|
|
61
|
+
requested: reasoningEffortSchema,
|
|
62
|
+
resolved: reasoningEffortSchema.optional(),
|
|
63
|
+
fidelity: z.enum(["exact", "clamped", "unsupported"]),
|
|
64
|
+
})
|
|
65
|
+
.superRefine((effort, context) => {
|
|
66
|
+
if (effort.fidelity === "unsupported") {
|
|
67
|
+
if (effort.resolved !== undefined) {
|
|
68
|
+
context.addIssue({
|
|
69
|
+
code: "custom",
|
|
70
|
+
path: ["resolved"],
|
|
71
|
+
message: "unsupported reasoning effort cannot claim a resolved value",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (effort.resolved === undefined) {
|
|
77
|
+
context.addIssue({
|
|
78
|
+
code: "custom",
|
|
79
|
+
path: ["resolved"],
|
|
80
|
+
message: `${effort.fidelity} reasoning fidelity requires a resolved value`,
|
|
81
|
+
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (effort.fidelity === "exact" && effort.resolved !== effort.requested) {
|
|
85
|
+
context.addIssue({
|
|
86
|
+
code: "custom",
|
|
87
|
+
path: ["resolved"],
|
|
88
|
+
message: "exact reasoning fidelity must preserve the requested effort",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (effort.fidelity === "clamped") {
|
|
92
|
+
if (effort.resolved === effort.requested) {
|
|
93
|
+
context.addIssue({
|
|
94
|
+
code: "custom",
|
|
95
|
+
path: ["resolved"],
|
|
96
|
+
message: "clamped reasoning fidelity must change the requested effort",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else if (!isDownwardReasoningClamp(effort.requested, effort.resolved)) {
|
|
100
|
+
context.addIssue({
|
|
101
|
+
code: "custom",
|
|
102
|
+
path: ["resolved"],
|
|
103
|
+
message: "reasoning effort may clamp down but must never increase",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
export const agentExecutionPreparationReceiptSchema = z
|
|
109
|
+
.strictObject({
|
|
110
|
+
kind: z.literal("agent-execution-preparation"),
|
|
111
|
+
schemaVersion: z.literal(1),
|
|
112
|
+
preparationId: nonBlankStringSchema.max(500),
|
|
113
|
+
requestDigest: sha256DigestSchema,
|
|
114
|
+
authoredProfileDigest: sha256DigestSchema,
|
|
115
|
+
effectiveProfileDigest: sha256DigestSchema,
|
|
116
|
+
backend: nonBlankStringSchema.max(200),
|
|
117
|
+
harness: harnessTypeSchema,
|
|
118
|
+
harnessVersion: nonBlankStringSchema.max(200),
|
|
119
|
+
resolvedModel: z.strictObject({
|
|
120
|
+
requested: z.string().max(500),
|
|
121
|
+
resolved: nonBlankStringSchema.max(500),
|
|
122
|
+
provider: z.string().max(200).optional(),
|
|
123
|
+
reasoningEffort: agentExecutionPreparationReasoningEffortSchema.optional(),
|
|
124
|
+
}),
|
|
125
|
+
workspace: z.strictObject({
|
|
126
|
+
leaseId: publicLifecycleIdentifierSchema,
|
|
127
|
+
provider: publicLifecycleIdentifierSchema,
|
|
128
|
+
identityDigest: sha256DigestSchema,
|
|
129
|
+
isolation: z.enum(["per-run", "shared"]),
|
|
130
|
+
sourceSnapshotDigest: sha256DigestSchema,
|
|
131
|
+
sourceSnapshotPolicy: agentWorkspaceSourceSnapshotPolicySchema,
|
|
132
|
+
preparedWorkspaceDigest: sha256DigestSchema,
|
|
133
|
+
profileActivationDigest: sha256DigestSchema,
|
|
134
|
+
}),
|
|
135
|
+
axisResults: z.array(agentExecutionPreparationAxisResultSchema),
|
|
136
|
+
executionPlanDigest: sha256DigestSchema,
|
|
137
|
+
materializer: z.strictObject({
|
|
138
|
+
name: nonBlankStringSchema.max(200),
|
|
139
|
+
version: nonBlankStringSchema.max(200),
|
|
140
|
+
}),
|
|
141
|
+
expiresAtMs: z.number().int().positive().safe(),
|
|
142
|
+
digest: sha256DigestSchema,
|
|
143
|
+
})
|
|
144
|
+
.superRefine((receipt, context) => {
|
|
145
|
+
const seen = new Map();
|
|
146
|
+
for (const [index, result] of receipt.axisResults.entries()) {
|
|
147
|
+
const key = axisResultKey(result.axis, result.path);
|
|
148
|
+
const previous = seen.get(key);
|
|
149
|
+
if (previous !== undefined) {
|
|
150
|
+
context.addIssue({
|
|
151
|
+
code: "custom",
|
|
152
|
+
path: ["axisResults", index],
|
|
153
|
+
message: canonicalRowsEqual(previous, result)
|
|
154
|
+
? "duplicate profile axis/path result"
|
|
155
|
+
: "conflicting profile axis/path results",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
seen.set(key, result);
|
|
160
|
+
}
|
|
161
|
+
if (index > 0 &&
|
|
162
|
+
compareAxisResults(receipt.axisResults[index - 1], result) >= 0) {
|
|
163
|
+
context.addIssue({
|
|
164
|
+
code: "custom",
|
|
165
|
+
path: ["axisResults", index],
|
|
166
|
+
message: "profile axis/path results must be canonically sorted",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!isCanonicalJsonValue(receipt)) {
|
|
171
|
+
context.addIssue({
|
|
172
|
+
code: "custom",
|
|
173
|
+
message: "execution preparation receipt must contain only RFC 8785 JSON values",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
else if (canonicalCandidateDigest(omitTopLevelDigest(receipt)) !== receipt.digest) {
|
|
177
|
+
context.addIssue({
|
|
178
|
+
code: "custom",
|
|
179
|
+
path: ["digest"],
|
|
180
|
+
message: "execution preparation receipt digest is invalid",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Canonical RFC 8785/SHA-256 identity for one validated, public AgentProfile.
|
|
186
|
+
* Secret-capable MCP/hook fields are tagged. Other prompt, resource, metadata,
|
|
187
|
+
* and tool text is ordinary public profile data: it may legitimately discuss
|
|
188
|
+
* credentials, security incidents, or examples that resemble credentials, so
|
|
189
|
+
* pattern matching cannot decide whether it is secret. Resolve opaque secret
|
|
190
|
+
* references only after this public identity is fixed.
|
|
191
|
+
*/
|
|
192
|
+
export function canonicalAgentProfileDigest(profile) {
|
|
193
|
+
const parsed = agentProfileSchema.parse(profile);
|
|
194
|
+
const material = canonicalAgentProfileValue(parsed);
|
|
195
|
+
if (material === undefined || !isCanonicalJsonValue(material)) {
|
|
196
|
+
throw new Error("AgentProfile must contain finite, acyclic RFC 8785 JSON values");
|
|
197
|
+
}
|
|
198
|
+
return canonicalCandidateDigest(material);
|
|
199
|
+
}
|
|
200
|
+
/** Build, self-hash, and cross-check one pre-compute executor acknowledgement. */
|
|
201
|
+
export function buildAgentExecutionPreparationReceipt(input) {
|
|
202
|
+
const authoredProfile = agentProfileSchema.parse(input.authoredProfile);
|
|
203
|
+
const effectiveProfile = agentProfileSchema.parse(input.effectiveProfile);
|
|
204
|
+
const workspaceLease = agentWorkspaceLeaseRecordSchema.parse(input.workspaceLease);
|
|
205
|
+
if (workspaceLease.phase !== "workspace-sealed") {
|
|
206
|
+
throw new Error("execution preparation requires a workspace-sealed lease record");
|
|
207
|
+
}
|
|
208
|
+
const axisResults = normalizeAxisResults(input.axisResults, authoredProfile, effectiveProfile);
|
|
209
|
+
const resolvedModel = cleanResolvedModel(input.resolvedModel);
|
|
210
|
+
const material = {
|
|
211
|
+
kind: "agent-execution-preparation",
|
|
212
|
+
schemaVersion: 1,
|
|
213
|
+
preparationId: input.preparationId,
|
|
214
|
+
requestDigest: input.requestDigest,
|
|
215
|
+
authoredProfileDigest: canonicalAgentProfileDigest(authoredProfile),
|
|
216
|
+
effectiveProfileDigest: canonicalAgentProfileDigest(effectiveProfile),
|
|
217
|
+
backend: input.backend,
|
|
218
|
+
harness: input.harness,
|
|
219
|
+
harnessVersion: input.harnessVersion,
|
|
220
|
+
resolvedModel,
|
|
221
|
+
workspace: {
|
|
222
|
+
leaseId: workspaceLease.leaseId,
|
|
223
|
+
provider: workspaceLease.workspace.provider,
|
|
224
|
+
identityDigest: workspaceLease.workspace.identityDigest,
|
|
225
|
+
isolation: workspaceLease.isolation,
|
|
226
|
+
sourceSnapshotDigest: workspaceLease.sourceSnapshotDigest,
|
|
227
|
+
sourceSnapshotPolicy: { ...workspaceLease.sourceSnapshotPolicy },
|
|
228
|
+
preparedWorkspaceDigest: workspaceLease.preparedWorkspaceDigest,
|
|
229
|
+
profileActivationDigest: workspaceLease.profileActivationDigest,
|
|
230
|
+
},
|
|
231
|
+
axisResults,
|
|
232
|
+
executionPlanDigest: input.executionPlanDigest,
|
|
233
|
+
materializer: { ...input.materializer },
|
|
234
|
+
expiresAtMs: input.expiresAtMs,
|
|
235
|
+
};
|
|
236
|
+
const receipt = {
|
|
237
|
+
...material,
|
|
238
|
+
digest: canonicalCandidateDigest(material),
|
|
239
|
+
};
|
|
240
|
+
const validation = validateAgentExecutionPreparationReceiptInternal({
|
|
241
|
+
receipt,
|
|
242
|
+
requestDigest: input.requestDigest,
|
|
243
|
+
authoredProfile,
|
|
244
|
+
effectiveProfile,
|
|
245
|
+
executionPlanDigest: input.executionPlanDigest,
|
|
246
|
+
profileActivation: input.profileActivation,
|
|
247
|
+
nowMs: input.nowMs,
|
|
248
|
+
preparationId: input.preparationId,
|
|
249
|
+
backend: input.backend,
|
|
250
|
+
harness: input.harness,
|
|
251
|
+
harnessVersion: input.harnessVersion,
|
|
252
|
+
workspaceLease,
|
|
253
|
+
requireExecutionBinding: false,
|
|
254
|
+
});
|
|
255
|
+
if (validation.ok)
|
|
256
|
+
return validation.receipt;
|
|
257
|
+
throw new AgentExecutionPreparationValidationError(validation.issues);
|
|
258
|
+
}
|
|
259
|
+
/** Validate structure, bindings, expiry, model fidelity, and exact path coverage. */
|
|
260
|
+
export function validateAgentExecutionPreparationReceipt(options) {
|
|
261
|
+
return validateAgentExecutionPreparationReceiptInternal({
|
|
262
|
+
...options,
|
|
263
|
+
requireExecutionBinding: true,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function validateAgentExecutionPreparationReceiptInternal(options) {
|
|
267
|
+
const parsedReceipt = agentExecutionPreparationReceiptSchema.safeParse(options.receipt);
|
|
268
|
+
if (!parsedReceipt.success) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
issues: parsedReceipt.error.issues.map((issue) => ({
|
|
272
|
+
code: "invalid-receipt",
|
|
273
|
+
message: `${issue.path.join(".") || "receipt"}: ${issue.message}`,
|
|
274
|
+
})),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const receipt = parsedReceipt.data;
|
|
278
|
+
const issues = [];
|
|
279
|
+
const workspaceLeaseResult = agentWorkspaceLeaseRecordSchema.safeParse(options.workspaceLease);
|
|
280
|
+
if (!workspaceLeaseResult.success) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
issues: workspaceLeaseResult.error.issues.map((issue) => ({
|
|
284
|
+
code: "invalid-workspace-lease",
|
|
285
|
+
message: `${issue.path.join(".") || "workspaceLease"}: ${issue.message}`,
|
|
286
|
+
})),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const workspaceLease = workspaceLeaseResult.data;
|
|
290
|
+
const expectedPhase = options.requireExecutionBinding
|
|
291
|
+
? "execution-bound"
|
|
292
|
+
: "workspace-sealed";
|
|
293
|
+
if (workspaceLease.phase !== expectedPhase) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
issues: [
|
|
297
|
+
{
|
|
298
|
+
code: "workspace-not-execution-bound",
|
|
299
|
+
message: `execution preparation expected workspace phase ${expectedPhase}, ` +
|
|
300
|
+
`received ${workspaceLease.phase}`,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const authoredResult = agentProfileSchema.safeParse(options.authoredProfile);
|
|
306
|
+
const effectiveResult = agentProfileSchema.safeParse(options.effectiveProfile);
|
|
307
|
+
if (!authoredResult.success || !effectiveResult.success) {
|
|
308
|
+
for (const result of [authoredResult, effectiveResult]) {
|
|
309
|
+
if (result.success)
|
|
310
|
+
continue;
|
|
311
|
+
for (const issue of result.error.issues) {
|
|
312
|
+
issues.push({
|
|
313
|
+
code: "invalid-profile",
|
|
314
|
+
message: `${issue.path.join(".") || "profile"}: ${issue.message}`,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { ok: false, issues };
|
|
319
|
+
}
|
|
320
|
+
const authoredProfile = authoredResult.data;
|
|
321
|
+
const effectiveProfile = effectiveResult.data;
|
|
322
|
+
let authoredProfileDigest;
|
|
323
|
+
let effectiveProfileDigest;
|
|
324
|
+
try {
|
|
325
|
+
authoredProfileDigest = canonicalAgentProfileDigest(authoredProfile);
|
|
326
|
+
effectiveProfileDigest = canonicalAgentProfileDigest(effectiveProfile);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
issues: [
|
|
332
|
+
{
|
|
333
|
+
code: "invalid-profile",
|
|
334
|
+
message: error instanceof Error ? error.message : String(error),
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
compareDigest(issues, "authored profile", receipt.authoredProfileDigest, authoredProfileDigest);
|
|
340
|
+
compareDigest(issues, "effective profile", receipt.effectiveProfileDigest, effectiveProfileDigest);
|
|
341
|
+
compareDigest(issues, "request", receipt.requestDigest, options.requestDigest);
|
|
342
|
+
compareDigest(issues, "execution plan", receipt.executionPlanDigest, options.executionPlanDigest);
|
|
343
|
+
compareDigest(issues, "profile activation", receipt.workspace.profileActivationDigest, options.profileActivation.digest);
|
|
344
|
+
compareDigest(issues, "workspace profile activation", receipt.workspace.profileActivationDigest, workspaceLease.profileActivationDigest);
|
|
345
|
+
if (workspaceLease.phase === "execution-bound" &&
|
|
346
|
+
workspaceLease.executionPreparationDigest !== receipt.digest) {
|
|
347
|
+
issues.push({
|
|
348
|
+
code: "execution-binding-mismatch",
|
|
349
|
+
message: "workspace execution binding does not name this preparation receipt",
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
compareExpectation(issues, "preparation id", receipt.preparationId, options.preparationId);
|
|
353
|
+
compareExpectation(issues, "backend", receipt.backend, options.backend);
|
|
354
|
+
compareExpectation(issues, "harness", receipt.harness, options.harness);
|
|
355
|
+
compareExpectation(issues, "harness version", receipt.harnessVersion, options.harnessVersion);
|
|
356
|
+
compareExpectation(issues, "workspace lease", receipt.workspace.leaseId, workspaceLease.leaseId);
|
|
357
|
+
compareExpectation(issues, "workspace provider", receipt.workspace.provider, workspaceLease.workspace.provider);
|
|
358
|
+
compareExpectation(issues, "workspace identity", receipt.workspace.identityDigest, workspaceLease.workspace.identityDigest);
|
|
359
|
+
compareExpectation(issues, "workspace isolation", receipt.workspace.isolation, workspaceLease.isolation);
|
|
360
|
+
compareExpectation(issues, "source workspace snapshot", receipt.workspace.sourceSnapshotDigest, workspaceLease.sourceSnapshotDigest);
|
|
361
|
+
compareExpectation(issues, "source snapshot policy kind", receipt.workspace.sourceSnapshotPolicy.kind, workspaceLease.sourceSnapshotPolicy.kind);
|
|
362
|
+
compareExpectation(issues, "source snapshot policy name", receipt.workspace.sourceSnapshotPolicy.name, workspaceLease.sourceSnapshotPolicy.name);
|
|
363
|
+
compareExpectation(issues, "source snapshot policy version", receipt.workspace.sourceSnapshotPolicy.version, workspaceLease.sourceSnapshotPolicy.version);
|
|
364
|
+
compareDigest(issues, "source snapshot policy", receipt.workspace.sourceSnapshotPolicy.digest, workspaceLease.sourceSnapshotPolicy.digest);
|
|
365
|
+
compareExpectation(issues, "prepared workspace snapshot", receipt.workspace.preparedWorkspaceDigest, workspaceLease.preparedWorkspaceDigest);
|
|
366
|
+
if (receipt.expiresAtMs > workspaceLease.expiresAtMs) {
|
|
367
|
+
issues.push({
|
|
368
|
+
code: "expectation-mismatch",
|
|
369
|
+
message: "execution preparation cannot outlive its workspace lease",
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
373
|
+
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
|
|
374
|
+
issues.push({
|
|
375
|
+
code: "expectation-mismatch",
|
|
376
|
+
message: "validation clock must be a non-negative safe integer",
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
else if (receipt.expiresAtMs <= nowMs) {
|
|
380
|
+
issues.push({
|
|
381
|
+
code: "expired",
|
|
382
|
+
message: `execution preparation expired at ${receipt.expiresAtMs}`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
else if (workspaceLease.expiresAtMs <= nowMs) {
|
|
386
|
+
issues.push({
|
|
387
|
+
code: "expired",
|
|
388
|
+
message: `workspace lease expired at ${workspaceLease.expiresAtMs}`,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
validateHarnessAndModel(receipt, authoredProfile, effectiveProfile, issues);
|
|
392
|
+
validateAxisCoverage(receipt, authoredProfile, effectiveProfile, issues);
|
|
393
|
+
return issues.length === 0
|
|
394
|
+
? { ok: true, receipt, issues: [] }
|
|
395
|
+
: { ok: false, issues };
|
|
396
|
+
}
|
|
397
|
+
/** Throw one structured error when a preparation acknowledgement is invalid. */
|
|
398
|
+
export function assertAgentExecutionPreparationReceipt(options) {
|
|
399
|
+
const validation = validateAgentExecutionPreparationReceipt(options);
|
|
400
|
+
if (validation.ok)
|
|
401
|
+
return validation.receipt;
|
|
402
|
+
throw new AgentExecutionPreparationValidationError(validation.issues);
|
|
403
|
+
}
|
|
404
|
+
export class AgentExecutionPreparationValidationError extends Error {
|
|
405
|
+
issues;
|
|
406
|
+
constructor(issues) {
|
|
407
|
+
super(issues.map((issue) => issue.message).join("\n"));
|
|
408
|
+
this.name = "AgentExecutionPreparationValidationError";
|
|
409
|
+
this.issues = [...issues];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function validateHarnessAndModel(receipt, authoredProfile, effectiveProfile, issues) {
|
|
413
|
+
if (effectiveProfile.harness !== undefined &&
|
|
414
|
+
effectiveProfile.harness !== receipt.harness) {
|
|
415
|
+
issues.push({
|
|
416
|
+
code: "expectation-mismatch",
|
|
417
|
+
message: `effective profile harness ${effectiveProfile.harness} does not match ` +
|
|
418
|
+
`prepared harness ${receipt.harness}`,
|
|
419
|
+
axis: "harness",
|
|
420
|
+
path: "/harness",
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (effectiveProfile.model?.default !== undefined &&
|
|
424
|
+
effectiveProfile.model.default !== receipt.resolvedModel.requested) {
|
|
425
|
+
issues.push({
|
|
426
|
+
code: "model-fidelity",
|
|
427
|
+
message: "prepared model request does not match effective profile.model.default",
|
|
428
|
+
axis: "modelDefault",
|
|
429
|
+
path: "/model/default",
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (effectiveProfile.model?.provider !== undefined &&
|
|
433
|
+
effectiveProfile.model.provider !== receipt.resolvedModel.provider) {
|
|
434
|
+
issues.push({
|
|
435
|
+
code: "model-fidelity",
|
|
436
|
+
message: "prepared model provider does not match effective profile.model.provider",
|
|
437
|
+
axis: "modelProvider",
|
|
438
|
+
path: "/model/provider",
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
const requestedEffort = effectiveProfile.model?.reasoningEffort ??
|
|
442
|
+
authoredProfile.model?.reasoningEffort;
|
|
443
|
+
if (requestedEffort === undefined)
|
|
444
|
+
return;
|
|
445
|
+
const effort = receipt.resolvedModel.reasoningEffort;
|
|
446
|
+
if (effort === undefined) {
|
|
447
|
+
issues.push({
|
|
448
|
+
code: "model-fidelity",
|
|
449
|
+
message: "requested reasoning effort is missing from the prepared model",
|
|
450
|
+
axis: "modelReasoningEffort",
|
|
451
|
+
path: "/model/reasoningEffort",
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (effort.requested !== requestedEffort) {
|
|
456
|
+
issues.push({
|
|
457
|
+
code: "model-fidelity",
|
|
458
|
+
message: `prepared reasoning request ${effort.requested} does not match ` +
|
|
459
|
+
`profile request ${requestedEffort}`,
|
|
460
|
+
axis: "modelReasoningEffort",
|
|
461
|
+
path: "/model/reasoningEffort",
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function validateAxisCoverage(receipt, authoredProfile, effectiveProfile, issues) {
|
|
466
|
+
const authored = requestMap(authoredProfile);
|
|
467
|
+
const effective = requestMap(effectiveProfile);
|
|
468
|
+
const expected = new Map([...authored, ...effective]);
|
|
469
|
+
const expectedPathsByAxis = new Map();
|
|
470
|
+
for (const request of expected.values()) {
|
|
471
|
+
const paths = expectedPathsByAxis.get(request.axis) ?? [];
|
|
472
|
+
paths.push(request.path);
|
|
473
|
+
expectedPathsByAxis.set(request.axis, paths);
|
|
474
|
+
}
|
|
475
|
+
for (const paths of expectedPathsByAxis.values())
|
|
476
|
+
paths.sort();
|
|
477
|
+
const actual = new Map();
|
|
478
|
+
for (const result of receipt.axisResults) {
|
|
479
|
+
const paths = expectedPathsByAxis.get(result.axis) ?? [];
|
|
480
|
+
const path = result.path ?? (paths.length === 1 ? paths[0] : undefined);
|
|
481
|
+
if (path === undefined) {
|
|
482
|
+
issues.push({
|
|
483
|
+
code: paths.length === 0 ? "unrequested-coverage" : "ambiguous-coverage",
|
|
484
|
+
message: paths.length === 0
|
|
485
|
+
? `receipt covers unrequested ${result.axis} without an exact path`
|
|
486
|
+
: `${result.axis} covers ${paths.length} requested paths and requires an exact path`,
|
|
487
|
+
axis: result.axis,
|
|
488
|
+
});
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const key = axisResultKey(result.axis, path);
|
|
492
|
+
const previous = actual.get(key);
|
|
493
|
+
if (previous !== undefined) {
|
|
494
|
+
const duplicate = canonicalRowsEqual({ ...previous.result, path }, { ...result, path });
|
|
495
|
+
issues.push({
|
|
496
|
+
code: duplicate ? "duplicate-coverage" : "conflicting-coverage",
|
|
497
|
+
message: duplicate
|
|
498
|
+
? `duplicate coverage for ${result.axis} at ${path}`
|
|
499
|
+
: `conflicting coverage for ${result.axis} at ${path}`,
|
|
500
|
+
axis: result.axis,
|
|
501
|
+
path,
|
|
502
|
+
});
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
actual.set(key, { result, path });
|
|
506
|
+
if (!expected.has(key)) {
|
|
507
|
+
issues.push({
|
|
508
|
+
code: "unrequested-coverage",
|
|
509
|
+
message: `receipt covers unrequested ${result.axis} path ${path}`,
|
|
510
|
+
axis: result.axis,
|
|
511
|
+
path,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const allowPartial = authoredProfile.resources?.failOnError === false;
|
|
516
|
+
for (const [key, request] of expected) {
|
|
517
|
+
const coverage = actual.get(key);
|
|
518
|
+
if (coverage === undefined) {
|
|
519
|
+
issues.push({
|
|
520
|
+
code: "missing-coverage",
|
|
521
|
+
message: `receipt is missing ${request.axis} coverage at ${request.path}`,
|
|
522
|
+
axis: request.axis,
|
|
523
|
+
path: request.path,
|
|
524
|
+
});
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const authoredRequest = authored.get(key);
|
|
528
|
+
const effectiveRequest = effective.get(key);
|
|
529
|
+
const changed = authoredRequest === undefined ||
|
|
530
|
+
effectiveRequest === undefined ||
|
|
531
|
+
!canonicalValuesEqual(authoredRequest.value, effectiveRequest.value);
|
|
532
|
+
const executionOverride = request.axis === "modelReasoningEffort" &&
|
|
533
|
+
receipt.resolvedModel.reasoningEffort?.fidelity === "clamped";
|
|
534
|
+
if (changed &&
|
|
535
|
+
coverage.result.disposition !== "overridden" &&
|
|
536
|
+
coverage.result.disposition !== "unsupported") {
|
|
537
|
+
issues.push({
|
|
538
|
+
code: "invalid-disposition",
|
|
539
|
+
message: `${request.axis} at ${request.path} changed between authored and ` +
|
|
540
|
+
`effective profiles but is marked ${coverage.result.disposition}`,
|
|
541
|
+
axis: request.axis,
|
|
542
|
+
path: request.path,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (!changed &&
|
|
546
|
+
!executionOverride &&
|
|
547
|
+
coverage.result.disposition === "overridden") {
|
|
548
|
+
issues.push({
|
|
549
|
+
code: "invalid-disposition",
|
|
550
|
+
message: `${request.axis} at ${request.path} is marked overridden without ` +
|
|
551
|
+
"an effective change",
|
|
552
|
+
axis: request.axis,
|
|
553
|
+
path: request.path,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (coverage.result.disposition === "unsupported" && !allowPartial) {
|
|
557
|
+
issues.push({
|
|
558
|
+
code: "strict-unsupported",
|
|
559
|
+
message: `${request.axis} at ${request.path} is unsupported but the authored ` +
|
|
560
|
+
"profile did not explicitly set resources.failOnError=false",
|
|
561
|
+
axis: request.axis,
|
|
562
|
+
path: request.path,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
if (request.axis === "modelReasoningEffort") {
|
|
566
|
+
const fidelity = receipt.resolvedModel.reasoningEffort?.fidelity;
|
|
567
|
+
if (coverage.result.disposition === "unsupported" &&
|
|
568
|
+
fidelity !== "unsupported") {
|
|
569
|
+
issues.push({
|
|
570
|
+
code: "model-fidelity",
|
|
571
|
+
message: "unsupported reasoning coverage requires unsupported model fidelity",
|
|
572
|
+
axis: request.axis,
|
|
573
|
+
path: request.path,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (fidelity === "unsupported" &&
|
|
577
|
+
coverage.result.disposition !== "unsupported") {
|
|
578
|
+
issues.push({
|
|
579
|
+
code: "model-fidelity",
|
|
580
|
+
message: "unsupported reasoning fidelity must be reported as unsupported coverage",
|
|
581
|
+
axis: request.axis,
|
|
582
|
+
path: request.path,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
if (fidelity === "clamped" &&
|
|
586
|
+
coverage.result.disposition !== "overridden") {
|
|
587
|
+
issues.push({
|
|
588
|
+
code: "model-fidelity",
|
|
589
|
+
message: "clamped reasoning fidelity must be reported as overridden coverage",
|
|
590
|
+
axis: request.axis,
|
|
591
|
+
path: request.path,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function requestMap(profile) {
|
|
598
|
+
const requests = new Map();
|
|
599
|
+
for (const request of profileMaterializationRequests(profile)) {
|
|
600
|
+
requests.set(axisResultKey(request.axis, request.path), {
|
|
601
|
+
...request,
|
|
602
|
+
value: readJsonPointer(profile, request.path),
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return requests;
|
|
606
|
+
}
|
|
607
|
+
function normalizeAxisResults(results, authoredProfile, effectiveProfile) {
|
|
608
|
+
const expected = new Map([
|
|
609
|
+
...profileMaterializationRequests(authoredProfile).map((request) => [
|
|
610
|
+
axisResultKey(request.axis, request.path),
|
|
611
|
+
request,
|
|
612
|
+
]),
|
|
613
|
+
...profileMaterializationRequests(effectiveProfile).map((request) => [
|
|
614
|
+
axisResultKey(request.axis, request.path),
|
|
615
|
+
request,
|
|
616
|
+
]),
|
|
617
|
+
]);
|
|
618
|
+
const pathsByAxis = new Map();
|
|
619
|
+
for (const request of expected.values()) {
|
|
620
|
+
const paths = pathsByAxis.get(request.axis) ?? [];
|
|
621
|
+
paths.push(request.path);
|
|
622
|
+
pathsByAxis.set(request.axis, paths);
|
|
623
|
+
}
|
|
624
|
+
return results
|
|
625
|
+
.map((result) => {
|
|
626
|
+
const possiblePaths = pathsByAxis.get(result.axis) ?? [];
|
|
627
|
+
const path = result.path ??
|
|
628
|
+
(possiblePaths.length === 1 ? possiblePaths[0] : undefined);
|
|
629
|
+
return cleanAxisResult(result, path);
|
|
630
|
+
})
|
|
631
|
+
.sort(compareAxisResults);
|
|
632
|
+
}
|
|
633
|
+
function cleanAxisResult(result, path) {
|
|
634
|
+
return {
|
|
635
|
+
axis: result.axis,
|
|
636
|
+
disposition: result.disposition,
|
|
637
|
+
owner: result.owner,
|
|
638
|
+
mechanism: result.mechanism,
|
|
639
|
+
...(result.evidenceDigest === undefined
|
|
640
|
+
? {}
|
|
641
|
+
: { evidenceDigest: result.evidenceDigest }),
|
|
642
|
+
...(path === undefined ? {} : { path }),
|
|
643
|
+
...(result.reason === undefined ? {} : { reason: result.reason }),
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
function cleanResolvedModel(model) {
|
|
647
|
+
return {
|
|
648
|
+
requested: model.requested,
|
|
649
|
+
resolved: model.resolved,
|
|
650
|
+
...(model.provider === undefined ? {} : { provider: model.provider }),
|
|
651
|
+
...(model.reasoningEffort === undefined
|
|
652
|
+
? {}
|
|
653
|
+
: {
|
|
654
|
+
reasoningEffort: {
|
|
655
|
+
requested: model.reasoningEffort.requested,
|
|
656
|
+
...(model.reasoningEffort.resolved === undefined
|
|
657
|
+
? {}
|
|
658
|
+
: { resolved: model.reasoningEffort.resolved }),
|
|
659
|
+
fidelity: model.reasoningEffort.fidelity,
|
|
660
|
+
},
|
|
661
|
+
}),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
function readJsonPointer(root, pointer) {
|
|
665
|
+
let value = root;
|
|
666
|
+
for (const encoded of pointer.slice(1).split("/")) {
|
|
667
|
+
const key = encoded.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
668
|
+
if (value === null || typeof value !== "object")
|
|
669
|
+
return undefined;
|
|
670
|
+
if (!Object.prototype.hasOwnProperty.call(value, key))
|
|
671
|
+
return undefined;
|
|
672
|
+
value = value[key];
|
|
673
|
+
}
|
|
674
|
+
return value;
|
|
675
|
+
}
|
|
676
|
+
function canonicalValuesEqual(left, right) {
|
|
677
|
+
try {
|
|
678
|
+
return canonicalCandidateJson(left) === canonicalCandidateJson(right);
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
return Object.is(left, right);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function canonicalRowsEqual(left, right) {
|
|
685
|
+
try {
|
|
686
|
+
return canonicalCandidateJson(left) === canonicalCandidateJson(right);
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function compareDigest(issues, label, actual, expected) {
|
|
693
|
+
if (actual === expected)
|
|
694
|
+
return;
|
|
695
|
+
issues.push({
|
|
696
|
+
code: "digest-mismatch",
|
|
697
|
+
message: `${label} digest does not match the prepared execution`,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function compareExpectation(issues, label, actual, expected) {
|
|
701
|
+
if (expected === undefined || actual === expected)
|
|
702
|
+
return;
|
|
703
|
+
issues.push({
|
|
704
|
+
code: "expectation-mismatch",
|
|
705
|
+
message: `${label} does not match the prepared execution`,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function isCanonicalJsonPointer(value) {
|
|
709
|
+
if (!value.startsWith("/") || !isWellFormedUnicode(value))
|
|
710
|
+
return false;
|
|
711
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
712
|
+
if (value[index] !== "~")
|
|
713
|
+
continue;
|
|
714
|
+
const escape = value[index + 1];
|
|
715
|
+
if (escape !== "0" && escape !== "1")
|
|
716
|
+
return false;
|
|
717
|
+
index += 1;
|
|
718
|
+
}
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
const AXIS_ORDER = new Map(AGENT_PROFILE_MATERIALIZATION_AXES.map((axis, index) => [axis, index]));
|
|
722
|
+
function compareAxisResults(left, right) {
|
|
723
|
+
const axisDifference = (AXIS_ORDER.get(left.axis) ?? Number.MAX_SAFE_INTEGER) -
|
|
724
|
+
(AXIS_ORDER.get(right.axis) ?? Number.MAX_SAFE_INTEGER);
|
|
725
|
+
if (axisDifference !== 0)
|
|
726
|
+
return axisDifference;
|
|
727
|
+
const leftPath = left.path ?? "";
|
|
728
|
+
const rightPath = right.path ?? "";
|
|
729
|
+
return leftPath < rightPath ? -1 : leftPath > rightPath ? 1 : 0;
|
|
730
|
+
}
|
|
731
|
+
function axisResultKey(axis, path) {
|
|
732
|
+
return `${axis}\u0000${path ?? ""}`;
|
|
733
|
+
}
|
|
734
|
+
function isDownwardReasoningClamp(requested, resolved) {
|
|
735
|
+
return (REASONING_EFFORTS.indexOf(resolved) < REASONING_EFFORTS.indexOf(requested));
|
|
736
|
+
}
|