@blokjs/shared 2.1.0 → 2.2.1
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/AgentSessionContracts.d.ts +316 -0
- package/dist/AgentSessionContracts.js +331 -0
- package/dist/BlokError.d.ts +23 -0
- package/dist/BlokError.js +65 -0
- package/dist/CapabilityContracts.d.ts +91 -0
- package/dist/CapabilityContracts.js +104 -0
- package/dist/CapabilityManifest.d.ts +67 -0
- package/dist/CapabilityManifest.js +172 -0
- package/dist/EnforcementContracts.d.ts +61 -0
- package/dist/EnforcementContracts.js +17 -0
- package/dist/EnforcementProfileContracts.d.ts +36 -0
- package/dist/EnforcementProfileContracts.js +55 -0
- package/dist/EvidenceContracts.d.ts +884 -0
- package/dist/EvidenceContracts.js +237 -0
- package/dist/GitCapabilityContracts.d.ts +103 -0
- package/dist/GitCapabilityContracts.js +222 -0
- package/dist/GlobalLogger.d.ts +2 -0
- package/dist/GlobalLogger.js +4 -0
- package/dist/GraphContracts.d.ts +1643 -0
- package/dist/GraphContracts.js +333 -0
- package/dist/InteractionContracts.d.ts +76 -0
- package/dist/InteractionContracts.js +218 -0
- package/dist/JoinContracts.d.ts +593 -0
- package/dist/JoinContracts.js +329 -0
- package/dist/NodeBase.d.ts +20 -0
- package/dist/NodeBase.js +57 -6
- package/dist/PermissionAlgebra.d.ts +51 -0
- package/dist/PermissionAlgebra.js +125 -0
- package/dist/PolicyContracts.d.ts +184 -0
- package/dist/PolicyContracts.js +1 -0
- package/dist/ProcessCapabilityContracts.d.ts +146 -0
- package/dist/ProcessCapabilityContracts.js +263 -0
- package/dist/RuntimeContracts.d.ts +125 -0
- package/dist/RuntimeContracts.js +108 -0
- package/dist/SecretContracts.d.ts +43 -0
- package/dist/SecretContracts.js +1 -0
- package/dist/WasiComponentContracts.d.ts +582 -0
- package/dist/WasiComponentContracts.js +192 -0
- package/dist/WorkflowBindingContracts.d.ts +1062 -0
- package/dist/WorkflowBindingContracts.js +339 -0
- package/dist/index.d.ts +33 -2
- package/dist/index.js +21 -2
- package/dist/types/LoggerContext.d.ts +7 -0
- package/dist/utils/Mapper.d.ts +14 -0
- package/dist/utils/Mapper.js +32 -0
- package/package.json +3 -2
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CAPABILITY_EFFECTS } from "./CapabilityManifest.js";
|
|
3
|
+
import { EVIDENCE_MAX_REQUIREMENTS, EvidenceRequirementSchema, parseEvidenceRecord, } from "./EvidenceContracts.js";
|
|
4
|
+
import { CapabilityAuthoritySchema } from "./PermissionAlgebra.js";
|
|
5
|
+
/** Version of the language-neutral branch-join contract. */
|
|
6
|
+
export const JOIN_CONTRACT_VERSION = "1";
|
|
7
|
+
export const JOIN_MAX_BRANCHES = 128;
|
|
8
|
+
export const JOIN_MAX_OUTPUTS = 128;
|
|
9
|
+
export const JOIN_MAX_SCHEMA_DEPTH = 8;
|
|
10
|
+
export const JOIN_MAX_CONTRACT_BYTES = 64 * 1024;
|
|
11
|
+
/** Version of the retry/resume idempotency contract. */
|
|
12
|
+
export const RETRY_RESUME_IDEMPOTENCY_CONTRACT_VERSION = "1";
|
|
13
|
+
export const RETRY_RESUME_MAX_ATTEMPTS = 20;
|
|
14
|
+
export const RETRY_RESUME_MAX_RESUMES = 100;
|
|
15
|
+
export const RETRY_RESUME_MAX_EVIDENCE = 64;
|
|
16
|
+
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/;
|
|
17
|
+
const DIGEST = /^(sha256):[0-9a-f]{64}$|^(sha512):[0-9a-f]{128}$/i;
|
|
18
|
+
const identifier = z.string().min(1).max(128).regex(IDENTIFIER, "must be a bounded identifier");
|
|
19
|
+
const pathSegment = z.union([identifier, z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)]);
|
|
20
|
+
const jsonSchema = z.union([z.boolean(), z.record(z.unknown())]);
|
|
21
|
+
export class JoinContractError extends Error {
|
|
22
|
+
reasonCode;
|
|
23
|
+
code = "JOIN_REJECTED";
|
|
24
|
+
constructor(reasonCode, message) {
|
|
25
|
+
super(`Join rejected (${reasonCode}): ${message}`);
|
|
26
|
+
this.reasonCode = reasonCode;
|
|
27
|
+
this.name = "JoinContractError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class RetryResumeContractError extends Error {
|
|
31
|
+
reasonCode;
|
|
32
|
+
code = "RETRY_RESUME_REJECTED";
|
|
33
|
+
constructor(reasonCode, message) {
|
|
34
|
+
super(`Retry/resume rejected (${reasonCode}): ${message}`);
|
|
35
|
+
this.reasonCode = reasonCode;
|
|
36
|
+
this.name = "RetryResumeContractError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const joinBranchSchema = z.object({
|
|
40
|
+
id: identifier,
|
|
41
|
+
required: z.boolean(),
|
|
42
|
+
evidence: z.array(EvidenceRequirementSchema).max(EVIDENCE_MAX_REQUIREMENTS).optional(),
|
|
43
|
+
});
|
|
44
|
+
const joinOutputSchema = z.object({
|
|
45
|
+
id: identifier,
|
|
46
|
+
branchId: identifier,
|
|
47
|
+
path: z.array(pathSegment).max(64),
|
|
48
|
+
schema: jsonSchema,
|
|
49
|
+
});
|
|
50
|
+
export const JoinBranchObligationSchema = joinBranchSchema;
|
|
51
|
+
export const JoinOutputDeclarationSchema = joinOutputSchema;
|
|
52
|
+
export const JoinContractSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
version: z.literal(JOIN_CONTRACT_VERSION),
|
|
55
|
+
id: identifier,
|
|
56
|
+
mode: z.enum(["all", "any"]),
|
|
57
|
+
branches: z.array(joinBranchSchema).min(1).max(JOIN_MAX_BRANCHES),
|
|
58
|
+
outputs: z.array(joinOutputSchema).max(JOIN_MAX_OUTPUTS),
|
|
59
|
+
authority: CapabilityAuthoritySchema.optional(),
|
|
60
|
+
})
|
|
61
|
+
.superRefine((value, context) => {
|
|
62
|
+
const branchIds = new Set();
|
|
63
|
+
for (const [index, branch] of value.branches.entries()) {
|
|
64
|
+
if (branchIds.has(branch.id)) {
|
|
65
|
+
context.addIssue({
|
|
66
|
+
code: z.ZodIssueCode.custom,
|
|
67
|
+
path: ["branches", index, "id"],
|
|
68
|
+
message: "branch ids must be unique",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
branchIds.add(branch.id);
|
|
72
|
+
}
|
|
73
|
+
const outputIds = new Set();
|
|
74
|
+
for (const [index, output] of value.outputs.entries()) {
|
|
75
|
+
if (outputIds.has(output.id)) {
|
|
76
|
+
context.addIssue({
|
|
77
|
+
code: z.ZodIssueCode.custom,
|
|
78
|
+
path: ["outputs", index, "id"],
|
|
79
|
+
message: "output ids must be unique",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
outputIds.add(output.id);
|
|
83
|
+
if (!branchIds.has(output.branchId)) {
|
|
84
|
+
context.addIssue({
|
|
85
|
+
code: z.ZodIssueCode.custom,
|
|
86
|
+
path: ["outputs", index, "branchId"],
|
|
87
|
+
message: `output references unknown branch "${output.branchId}"`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const retryIdempotencySchema = z.object({
|
|
93
|
+
mode: z.enum(["not-required", "keyed", "evidence-required"]),
|
|
94
|
+
keyDeclared: z.boolean().optional(),
|
|
95
|
+
});
|
|
96
|
+
export const RetryResumeIdempotencyContractSchema = z
|
|
97
|
+
.object({
|
|
98
|
+
version: z.literal(RETRY_RESUME_IDEMPOTENCY_CONTRACT_VERSION),
|
|
99
|
+
id: identifier,
|
|
100
|
+
stepId: identifier,
|
|
101
|
+
effect: z.enum(["none", ...CAPABILITY_EFFECTS]),
|
|
102
|
+
maxAttempts: z.number().int().positive().max(RETRY_RESUME_MAX_ATTEMPTS),
|
|
103
|
+
maxResumes: z.number().int().nonnegative().max(RETRY_RESUME_MAX_RESUMES),
|
|
104
|
+
idempotency: retryIdempotencySchema,
|
|
105
|
+
evidence: z.array(EvidenceRequirementSchema).max(RETRY_RESUME_MAX_EVIDENCE).optional(),
|
|
106
|
+
authority: CapabilityAuthoritySchema.optional(),
|
|
107
|
+
})
|
|
108
|
+
.superRefine((value, context) => {
|
|
109
|
+
const effectful = value.effect !== "none";
|
|
110
|
+
const retriesOrResume = value.maxAttempts > 1 || value.maxResumes > 0;
|
|
111
|
+
if (effectful && retriesOrResume && value.idempotency.mode === "not-required") {
|
|
112
|
+
context.addIssue({
|
|
113
|
+
code: z.ZodIssueCode.custom,
|
|
114
|
+
path: ["idempotency", "mode"],
|
|
115
|
+
message: "effectful retry/resume requires keyed idempotency or evidence",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (value.idempotency.mode === "keyed" && value.idempotency.keyDeclared !== true) {
|
|
119
|
+
context.addIssue({
|
|
120
|
+
code: z.ZodIssueCode.custom,
|
|
121
|
+
path: ["idempotency", "keyDeclared"],
|
|
122
|
+
message: "keyed idempotency requires keyDeclared: true",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (value.idempotency.mode === "evidence-required" && (!value.evidence || value.evidence.length === 0)) {
|
|
126
|
+
context.addIssue({
|
|
127
|
+
code: z.ZodIssueCode.custom,
|
|
128
|
+
path: ["evidence"],
|
|
129
|
+
message: "evidence-required idempotency must declare evidence obligations",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
export const EffectRetryEvidenceSchema = z.object({
|
|
134
|
+
version: z.literal(RETRY_RESUME_IDEMPOTENCY_CONTRACT_VERSION),
|
|
135
|
+
id: identifier,
|
|
136
|
+
stepId: identifier,
|
|
137
|
+
runId: identifier,
|
|
138
|
+
attempt: z.number().int().positive().max(RETRY_RESUME_MAX_ATTEMPTS),
|
|
139
|
+
effect: z.enum(CAPABILITY_EFFECTS),
|
|
140
|
+
idempotencyKeyDigest: z.string().max(140).regex(DIGEST, "must be a complete sha256: or sha512: digest"),
|
|
141
|
+
outcome: z.enum(["committed", "deduplicated", "not-committed"]),
|
|
142
|
+
producer: z.object({ kind: z.enum(["capability", "deterministic-step", "runner"]), id: identifier }),
|
|
143
|
+
observedAt: z.string().min(1).max(64),
|
|
144
|
+
});
|
|
145
|
+
function parse(schema, value, label) {
|
|
146
|
+
const result = schema.safeParse(value);
|
|
147
|
+
if (!result.success)
|
|
148
|
+
throw new JoinContractError("INVALID_CONTRACT", `${label}: ${result.error.message}`);
|
|
149
|
+
return result.data;
|
|
150
|
+
}
|
|
151
|
+
function boundedJson(value, label) {
|
|
152
|
+
let serialized;
|
|
153
|
+
try {
|
|
154
|
+
serialized = JSON.stringify(value);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
throw new JoinContractError("INVALID_RESULT", `${label} must be JSON-serializable`);
|
|
158
|
+
}
|
|
159
|
+
if (new TextEncoder().encode(serialized).byteLength > JOIN_MAX_CONTRACT_BYTES) {
|
|
160
|
+
throw new JoinContractError("RESULT_TOO_LARGE", `${label} exceeds ${JOIN_MAX_CONTRACT_BYTES} bytes`);
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
function atPath(value, path) {
|
|
165
|
+
let current = value;
|
|
166
|
+
for (const segment of path) {
|
|
167
|
+
if (current === null || typeof current !== "object" || !(segment in current))
|
|
168
|
+
return undefined;
|
|
169
|
+
current = current[segment];
|
|
170
|
+
}
|
|
171
|
+
return current;
|
|
172
|
+
}
|
|
173
|
+
function schemaMatches(value, schema, depth = 0) {
|
|
174
|
+
if (depth > JOIN_MAX_SCHEMA_DEPTH)
|
|
175
|
+
return false;
|
|
176
|
+
if (typeof schema === "boolean")
|
|
177
|
+
return schema;
|
|
178
|
+
const type = schema.type;
|
|
179
|
+
if (typeof type === "string") {
|
|
180
|
+
const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
181
|
+
if (type === "integer") {
|
|
182
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
else if (actual !== type)
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => Object.is(candidate, value)))
|
|
189
|
+
return false;
|
|
190
|
+
if ("const" in schema && !Object.is(schema.const, value))
|
|
191
|
+
return false;
|
|
192
|
+
if (schema.required &&
|
|
193
|
+
typeof schema.required === "object" &&
|
|
194
|
+
Array.isArray(schema.required) &&
|
|
195
|
+
typeof value === "object" &&
|
|
196
|
+
value !== null) {
|
|
197
|
+
for (const key of schema.required) {
|
|
198
|
+
if (typeof key === "string" && !(key in value))
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (schema.properties &&
|
|
203
|
+
typeof schema.properties === "object" &&
|
|
204
|
+
!Array.isArray(schema.properties) &&
|
|
205
|
+
typeof value === "object" &&
|
|
206
|
+
value !== null) {
|
|
207
|
+
for (const [key, childSchema] of Object.entries(schema.properties)) {
|
|
208
|
+
if (key in value && (typeof childSchema === "boolean" || (childSchema && typeof childSchema === "object"))) {
|
|
209
|
+
if (!schemaMatches(value[key], childSchema, depth + 1))
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (schema.items &&
|
|
215
|
+
Array.isArray(value) &&
|
|
216
|
+
(typeof schema.items === "boolean" || (schema.items && typeof schema.items === "object"))) {
|
|
217
|
+
if (!value.every((item) => schemaMatches(item, schema.items, depth + 1)))
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
function evidenceMatches(record, requirement) {
|
|
223
|
+
return (record.verification.status === "verified" &&
|
|
224
|
+
record.kind === requirement.kind &&
|
|
225
|
+
(requirement.claim === undefined || record.claim === requirement.claim) &&
|
|
226
|
+
(requirement.artifactKind === undefined || record.artifact.artifact.kind === requirement.artifactKind) &&
|
|
227
|
+
requirement.producers.includes(record.provenance.producer.kind));
|
|
228
|
+
}
|
|
229
|
+
/** Validate branch completion, evidence, and declared output types at a join boundary. */
|
|
230
|
+
export function assertJoinSatisfied(contract, result) {
|
|
231
|
+
const parsed = parse(JoinContractSchema, contract, "join contract");
|
|
232
|
+
boundedJson(result, "join result");
|
|
233
|
+
const results = new Map(result.branches.map((branch) => [branch.id, branch]));
|
|
234
|
+
const declaredBranchIds = new Set(parsed.branches.map((branch) => branch.id));
|
|
235
|
+
for (const branch of result.branches) {
|
|
236
|
+
if (!declaredBranchIds.has(branch.id)) {
|
|
237
|
+
throw new JoinContractError("UNKNOWN_BRANCH", `join result contains undeclared branch "${branch.id}"`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const branch of parsed.branches) {
|
|
241
|
+
const current = results.get(branch.id);
|
|
242
|
+
if (!current || current.status === "missing") {
|
|
243
|
+
if (branch.required)
|
|
244
|
+
throw new JoinContractError("REQUIRED_BRANCH_MISSING", `required branch "${branch.id}" did not complete`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (current.status !== "completed") {
|
|
248
|
+
if (branch.required)
|
|
249
|
+
throw new JoinContractError("REQUIRED_BRANCH_INCOMPLETE", `required branch "${branch.id}" is ${current.status}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
for (const requirement of branch.evidence ?? []) {
|
|
253
|
+
const evidence = (current.evidence ?? [])
|
|
254
|
+
.map((item) => {
|
|
255
|
+
try {
|
|
256
|
+
return parseEvidenceRecord(item);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
.filter((item) => item !== null);
|
|
263
|
+
if (!evidence.some((item) => evidenceMatches(item, requirement))) {
|
|
264
|
+
throw new JoinContractError("EVIDENCE_MISSING", `branch "${branch.id}" is missing evidence requirement "${requirement.id}"`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (parsed.mode === "any" && !parsed.branches.some((branch) => results.get(branch.id)?.status === "completed")) {
|
|
269
|
+
throw new JoinContractError("NO_BRANCH_COMPLETED", "an any-mode join requires at least one completed branch");
|
|
270
|
+
}
|
|
271
|
+
for (const output of parsed.outputs) {
|
|
272
|
+
const branch = results.get(output.branchId);
|
|
273
|
+
if (!branch || branch.status !== "completed") {
|
|
274
|
+
if (parsed.branches.find((item) => item.id === output.branchId)?.required) {
|
|
275
|
+
throw new JoinContractError("OUTPUT_BRANCH_MISSING", `output "${output.id}" has no completed branch source`);
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const value = atPath(branch.output, output.path);
|
|
280
|
+
if (value === undefined)
|
|
281
|
+
throw new JoinContractError("OUTPUT_MISSING", `declared output "${output.id}" is missing`);
|
|
282
|
+
if (!schemaMatches(value, output.schema))
|
|
283
|
+
throw new JoinContractError("OUTPUT_TYPE_INVALID", `declared output "${output.id}" does not match its schema`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
export function parseJoinContract(value) {
|
|
287
|
+
return parse(JoinContractSchema, value, "join contract");
|
|
288
|
+
}
|
|
289
|
+
export function serializeJoinContract(value) {
|
|
290
|
+
return JSON.stringify(parseJoinContract(value));
|
|
291
|
+
}
|
|
292
|
+
export function parseRetryResumeIdempotencyContract(value) {
|
|
293
|
+
const result = RetryResumeIdempotencyContractSchema.safeParse(value);
|
|
294
|
+
if (!result.success)
|
|
295
|
+
throw new RetryResumeContractError("INVALID_CONTRACT", result.error.message);
|
|
296
|
+
return result.data;
|
|
297
|
+
}
|
|
298
|
+
export function parseEffectRetryEvidence(value) {
|
|
299
|
+
const result = EffectRetryEvidenceSchema.safeParse(value);
|
|
300
|
+
if (!result.success)
|
|
301
|
+
throw new RetryResumeContractError("INVALID_EVIDENCE", result.error.message);
|
|
302
|
+
return result.data;
|
|
303
|
+
}
|
|
304
|
+
/** Require an evidence record for effectful retry/resume; no effect is retried without proof. */
|
|
305
|
+
export function assertEffectRetryEvidence(contract, evidence) {
|
|
306
|
+
const parsed = parseRetryResumeIdempotencyContract(contract);
|
|
307
|
+
if (parsed.effect === "none" || (parsed.maxAttempts === 1 && parsed.maxResumes === 0))
|
|
308
|
+
return;
|
|
309
|
+
if (parsed.idempotency.mode === "not-required") {
|
|
310
|
+
throw new RetryResumeContractError("IDEMPOTENCY_REQUIRED", `effectful step "${parsed.stepId}" cannot retry or resume without idempotency`);
|
|
311
|
+
}
|
|
312
|
+
if (parsed.idempotency.mode !== "evidence-required")
|
|
313
|
+
return;
|
|
314
|
+
const valid = evidence
|
|
315
|
+
.map((item) => {
|
|
316
|
+
try {
|
|
317
|
+
return parseEffectRetryEvidence(item);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
.filter((item) => item !== null);
|
|
324
|
+
for (const requirement of parsed.evidence ?? []) {
|
|
325
|
+
const found = valid.some((item) => item.stepId === parsed.stepId && item.producer.kind === "runner" && item.id === requirement.id);
|
|
326
|
+
if (!found)
|
|
327
|
+
throw new RetryResumeContractError("EVIDENCE_MISSING", `missing retry evidence "${requirement.id}" for step "${parsed.stepId}"`);
|
|
328
|
+
}
|
|
329
|
+
}
|
package/dist/NodeBase.d.ts
CHANGED
|
@@ -13,6 +13,24 @@ export default abstract class NodeBase {
|
|
|
13
13
|
active: boolean;
|
|
14
14
|
stop: boolean;
|
|
15
15
|
originalConfig: ParamsDictionary;
|
|
16
|
+
/** Trusted manifest copied from the node descriptor; absent means unsafe to agents. */
|
|
17
|
+
capabilityManifest?: import("./CapabilityManifest.js").CapabilityManifestV1;
|
|
18
|
+
/** Raw descriptor value retained so invalid runtime metadata fails closed. */
|
|
19
|
+
capabilityManifestRaw?: unknown;
|
|
20
|
+
/** H1-02: model work must satisfy an explicit completion contract. */
|
|
21
|
+
agentStep?: import("./EnforcementContracts.js").AgentStepContract;
|
|
22
|
+
/** H1-02: policy-backed durable approval handoff metadata. */
|
|
23
|
+
approval?: import("./EnforcementContracts.js").ApprovalContract;
|
|
24
|
+
/** H1-02: runner-owned deterministic assertion gate. */
|
|
25
|
+
assertionGate?: import("./EnforcementContracts.js").AssertionGateContract;
|
|
26
|
+
/** H1-02: runner-owned trusted evidence gate. */
|
|
27
|
+
evidenceGate?: import("./EnforcementContracts.js").EvidenceGateContract;
|
|
28
|
+
/** Trust is a property of the node implementation, never model prose. */
|
|
29
|
+
outputTrust: import("./EnforcementContracts.js").OutputTrust;
|
|
30
|
+
/** H1-04 evidence-aware obligations and typed outputs at a join boundary. */
|
|
31
|
+
join?: import("./JoinContracts.js").JoinContract;
|
|
32
|
+
/** H1-04 bounded effect retry/resume idempotency declaration. */
|
|
33
|
+
retryResume?: import("./JoinContracts.js").RetryResumeIdempotencyContract;
|
|
16
34
|
/**
|
|
17
35
|
* Alternative state key for this step's output. When set, the runner
|
|
18
36
|
* stores result.data at `ctx.state[as]` instead of `ctx.state[name]`.
|
|
@@ -94,6 +112,8 @@ export default abstract class NodeBase {
|
|
|
94
112
|
* in v0.3.x — the schema includes a deferred-feature error message.
|
|
95
113
|
*/
|
|
96
114
|
wait?: boolean;
|
|
115
|
+
prepare(ctx: Context): Promise<void>;
|
|
116
|
+
protected validatePrepared(_ctx: Context): Promise<void>;
|
|
97
117
|
process(ctx: Context, step?: Step): Promise<ResponseContext>;
|
|
98
118
|
processFlow(ctx: Context): Promise<ResponseContext>;
|
|
99
119
|
abstract run(ctx: Context): Promise<ResponseContext>;
|
package/dist/NodeBase.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
import _ from "lodash";
|
|
2
1
|
import GlobalError from "./GlobalError.js";
|
|
3
|
-
import mapper from "./utils/Mapper.js";
|
|
2
|
+
import mapper, { cloneResolvable } from "./utils/Mapper.js";
|
|
4
3
|
import { MapperResolutionError } from "./utils/MapperResolutionError.js";
|
|
4
|
+
const preparedContexts = new WeakMap();
|
|
5
|
+
/**
|
|
6
|
+
* Resolve one step's config slice for this execution and publish the resolved
|
|
7
|
+
* COPY at `config[name]`, leaving the source slice unresolved. Returns the
|
|
8
|
+
* pristine source (the step's `originalConfig`).
|
|
9
|
+
*
|
|
10
|
+
* The mapper mutates in place, so before #874 every caller that might run a
|
|
11
|
+
* step more than once against one config — a forEach iteration, a loop body —
|
|
12
|
+
* had to hand it a deep clone of the WHOLE workflow config first, which is
|
|
13
|
+
* O(config) per iteration and O(config × steps) per run. Copying only the
|
|
14
|
+
* slice being resolved, and only the parts the mapper can reach, gives the
|
|
15
|
+
* same isolation for the cost of the one thing that changes.
|
|
16
|
+
*
|
|
17
|
+
* Safe because `RunnerSteps.runSteps` already gives every (nested, parallel)
|
|
18
|
+
* pipeline its own top-level config object: replacing a key is private to the
|
|
19
|
+
* pipeline that does it, while the slice VALUES stay shared and unresolved.
|
|
20
|
+
*/
|
|
21
|
+
function resolveSlice(config, name, map, ctx) {
|
|
22
|
+
const original = config[name];
|
|
23
|
+
config[name] = map(cloneResolvable(original), ctx);
|
|
24
|
+
return original;
|
|
25
|
+
}
|
|
5
26
|
export default class NodeBase {
|
|
6
27
|
flow = false;
|
|
7
28
|
name = "";
|
|
@@ -9,6 +30,24 @@ export default class NodeBase {
|
|
|
9
30
|
active = true;
|
|
10
31
|
stop = false;
|
|
11
32
|
originalConfig = {};
|
|
33
|
+
/** Trusted manifest copied from the node descriptor; absent means unsafe to agents. */
|
|
34
|
+
capabilityManifest;
|
|
35
|
+
/** Raw descriptor value retained so invalid runtime metadata fails closed. */
|
|
36
|
+
capabilityManifestRaw;
|
|
37
|
+
/** H1-02: model work must satisfy an explicit completion contract. */
|
|
38
|
+
agentStep;
|
|
39
|
+
/** H1-02: policy-backed durable approval handoff metadata. */
|
|
40
|
+
approval;
|
|
41
|
+
/** H1-02: runner-owned deterministic assertion gate. */
|
|
42
|
+
assertionGate;
|
|
43
|
+
/** H1-02: runner-owned trusted evidence gate. */
|
|
44
|
+
evidenceGate;
|
|
45
|
+
/** Trust is a property of the node implementation, never model prose. */
|
|
46
|
+
outputTrust = "model";
|
|
47
|
+
/** H1-04 evidence-aware obligations and typed outputs at a join boundary. */
|
|
48
|
+
join;
|
|
49
|
+
/** H1-04 bounded effect retry/resume idempotency declaration. */
|
|
50
|
+
retryResume;
|
|
12
51
|
// =========================================================================
|
|
13
52
|
// V2 persistence knobs — populated by Configuration.getSteps from the
|
|
14
53
|
// step definition. Read by PersistenceHelper.applyStepOutput.
|
|
@@ -98,15 +137,27 @@ export default class NodeBase {
|
|
|
98
137
|
* in v0.3.x — the schema includes a deferred-feature error message.
|
|
99
138
|
*/
|
|
100
139
|
wait;
|
|
140
|
+
async prepare(ctx) {
|
|
141
|
+
const config = ctx.config;
|
|
142
|
+
this.originalConfig = resolveSlice(config, this.name, this.blueprintMapper, ctx);
|
|
143
|
+
await this.validatePrepared(ctx);
|
|
144
|
+
let prepared = preparedContexts.get(ctx);
|
|
145
|
+
if (!prepared) {
|
|
146
|
+
prepared = new Set();
|
|
147
|
+
preparedContexts.set(ctx, prepared);
|
|
148
|
+
}
|
|
149
|
+
prepared.add(this);
|
|
150
|
+
}
|
|
151
|
+
async validatePrepared(_ctx) { }
|
|
101
152
|
async process(ctx, step) {
|
|
102
153
|
let response = {
|
|
103
154
|
success: true,
|
|
104
155
|
data: null,
|
|
105
156
|
error: null,
|
|
106
157
|
};
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
158
|
+
const prepared = preparedContexts.get(ctx)?.has(this) === true;
|
|
159
|
+
if (!prepared)
|
|
160
|
+
await this.prepare(ctx);
|
|
110
161
|
response = await this.run(ctx);
|
|
111
162
|
if (response.error)
|
|
112
163
|
throw response.error;
|
|
@@ -121,7 +172,7 @@ export default class NodeBase {
|
|
|
121
172
|
};
|
|
122
173
|
try {
|
|
123
174
|
const config = ctx.config;
|
|
124
|
-
|
|
175
|
+
resolveSlice(config, this.name, this.blueprintMapper, ctx);
|
|
125
176
|
response = await this.run(ctx);
|
|
126
177
|
}
|
|
127
178
|
catch (error) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type CapabilityEffect } from "./CapabilityManifest.js";
|
|
3
|
+
/** The scalar values accepted in a capability authority's constraint fragments. */
|
|
4
|
+
export declare const CapabilityFragmentValueSchema: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
5
|
+
/**
|
|
6
|
+
* A normalized authority envelope. An empty list means that the envelope does
|
|
7
|
+
* not grant that category; an omitted policy envelope means that policy did
|
|
8
|
+
* not add a further restriction.
|
|
9
|
+
*/
|
|
10
|
+
export interface CapabilityAuthority {
|
|
11
|
+
readonly effects: readonly CapabilityEffect[];
|
|
12
|
+
readonly capabilities: readonly string[];
|
|
13
|
+
readonly secrets: readonly string[];
|
|
14
|
+
readonly fragments: Readonly<Record<string, string | number | boolean>>;
|
|
15
|
+
}
|
|
16
|
+
export declare const CapabilityAuthoritySchema: z.ZodObject<{
|
|
17
|
+
effects: z.ZodArray<z.ZodEnum<["read", "write", "network", "filesystem", "process", "secret", "streaming", "destructive"]>, "many">;
|
|
18
|
+
capabilities: z.ZodArray<z.ZodString, "many">;
|
|
19
|
+
secrets: z.ZodArray<z.ZodString, "many">;
|
|
20
|
+
fragments: z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
21
|
+
}, "strict", z.ZodTypeAny, {
|
|
22
|
+
effects: ("read" | "write" | "network" | "filesystem" | "process" | "secret" | "streaming" | "destructive")[];
|
|
23
|
+
capabilities: string[];
|
|
24
|
+
secrets: string[];
|
|
25
|
+
fragments: Record<string, string | number | boolean>;
|
|
26
|
+
}, {
|
|
27
|
+
effects: ("read" | "write" | "network" | "filesystem" | "process" | "secret" | "streaming" | "destructive")[];
|
|
28
|
+
capabilities: string[];
|
|
29
|
+
secrets: string[];
|
|
30
|
+
fragments: Record<string, string | number | boolean>;
|
|
31
|
+
}>;
|
|
32
|
+
export declare class CapabilityAuthorityError extends Error {
|
|
33
|
+
readonly code = "CAPABILITY_AUTHORITY_INVALID";
|
|
34
|
+
readonly errors: readonly string[];
|
|
35
|
+
constructor(errors: readonly string[]);
|
|
36
|
+
}
|
|
37
|
+
/** Parse, validate, canonicalize, and freeze an authority envelope. */
|
|
38
|
+
export declare function parseCapabilityAuthority(value: unknown): CapabilityAuthority;
|
|
39
|
+
/**
|
|
40
|
+
* Compute the monotonic permission intersection. The operation is
|
|
41
|
+
* commutative, associative, and returns a canonical frozen value, making it
|
|
42
|
+
* safe to persist in requests and traces.
|
|
43
|
+
*/
|
|
44
|
+
export declare function intersectCapabilityAuthorities(...authorities: readonly CapabilityAuthority[]): CapabilityAuthority;
|
|
45
|
+
/** Return whether a child authority is no broader than its parent. */
|
|
46
|
+
export declare function isCapabilityAuthoritySubset(child: CapabilityAuthority, parent: CapabilityAuthority): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Validate child delegation before dispatch. Errors use stable category and
|
|
49
|
+
* sorted-value ordering so callers and conformance tests can inspect them.
|
|
50
|
+
*/
|
|
51
|
+
export declare function assertCapabilityAuthoritySubset(child: CapabilityAuthority, parent: CapabilityAuthority, path?: string): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CAPABILITY_EFFECTS } from "./CapabilityManifest.js";
|
|
3
|
+
/** The scalar values accepted in a capability authority's constraint fragments. */
|
|
4
|
+
export const CapabilityFragmentValueSchema = z.union([z.string(), z.number().finite(), z.boolean()]);
|
|
5
|
+
const capabilityIdentifier = z
|
|
6
|
+
.string()
|
|
7
|
+
.regex(/^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/, "must be a valid capability or secret reference name");
|
|
8
|
+
export const CapabilityAuthoritySchema = z
|
|
9
|
+
.object({
|
|
10
|
+
effects: z.array(z.enum(CAPABILITY_EFFECTS)),
|
|
11
|
+
capabilities: z.array(capabilityIdentifier),
|
|
12
|
+
secrets: z.array(capabilityIdentifier),
|
|
13
|
+
fragments: z.record(CapabilityFragmentValueSchema),
|
|
14
|
+
})
|
|
15
|
+
.strict();
|
|
16
|
+
export class CapabilityAuthorityError extends Error {
|
|
17
|
+
code = "CAPABILITY_AUTHORITY_INVALID";
|
|
18
|
+
errors;
|
|
19
|
+
constructor(errors) {
|
|
20
|
+
super(`Invalid capability authority: ${errors.join("; ")}`);
|
|
21
|
+
this.name = "CapabilityAuthorityError";
|
|
22
|
+
this.errors = [...errors];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function sortedUnique(values) {
|
|
26
|
+
return [...new Set(values)].sort();
|
|
27
|
+
}
|
|
28
|
+
function stableFragments(fragments) {
|
|
29
|
+
const result = {};
|
|
30
|
+
for (const key of Object.keys(fragments).sort())
|
|
31
|
+
result[key] = fragments[key];
|
|
32
|
+
return Object.freeze(result);
|
|
33
|
+
}
|
|
34
|
+
function normalize(value) {
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
effects: Object.freeze(sortedUnique(value.effects)),
|
|
37
|
+
capabilities: Object.freeze(sortedUnique(value.capabilities)),
|
|
38
|
+
secrets: Object.freeze(sortedUnique(value.secrets)),
|
|
39
|
+
fragments: stableFragments(value.fragments),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function formatIssue(path, message) {
|
|
43
|
+
return `${path.length > 0 ? `authority.${path.join(".")}` : "authority"} ${message}`;
|
|
44
|
+
}
|
|
45
|
+
/** Parse, validate, canonicalize, and freeze an authority envelope. */
|
|
46
|
+
export function parseCapabilityAuthority(value) {
|
|
47
|
+
const parsed = CapabilityAuthoritySchema.safeParse(value);
|
|
48
|
+
if (!parsed.success) {
|
|
49
|
+
const errors = parsed.error.issues
|
|
50
|
+
.map((issue) => formatIssue(issue.path, issue.message))
|
|
51
|
+
.sort((left, right) => left.localeCompare(right));
|
|
52
|
+
throw new CapabilityAuthorityError(errors);
|
|
53
|
+
}
|
|
54
|
+
return normalize(parsed.data);
|
|
55
|
+
}
|
|
56
|
+
function commonValues(left, right) {
|
|
57
|
+
const rightSet = new Set(right);
|
|
58
|
+
return [...new Set(left)].filter((value) => rightSet.has(value)).sort();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Compute the monotonic permission intersection. The operation is
|
|
62
|
+
* commutative, associative, and returns a canonical frozen value, making it
|
|
63
|
+
* safe to persist in requests and traces.
|
|
64
|
+
*/
|
|
65
|
+
export function intersectCapabilityAuthorities(...authorities) {
|
|
66
|
+
if (authorities.length === 0) {
|
|
67
|
+
return parseCapabilityAuthority({ effects: [], capabilities: [], secrets: [], fragments: {} });
|
|
68
|
+
}
|
|
69
|
+
let result = parseCapabilityAuthority(authorities[0]);
|
|
70
|
+
for (const authority of authorities.slice(1)) {
|
|
71
|
+
const next = parseCapabilityAuthority(authority);
|
|
72
|
+
const fragments = {};
|
|
73
|
+
for (const key of Object.keys(result.fragments).sort()) {
|
|
74
|
+
if (Object.prototype.hasOwnProperty.call(next.fragments, key) &&
|
|
75
|
+
Object.is(result.fragments[key], next.fragments[key])) {
|
|
76
|
+
fragments[key] = result.fragments[key];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
result = Object.freeze({
|
|
80
|
+
effects: Object.freeze(commonValues(result.effects, next.effects)),
|
|
81
|
+
capabilities: Object.freeze(commonValues(result.capabilities, next.capabilities)),
|
|
82
|
+
secrets: Object.freeze(commonValues(result.secrets, next.secrets)),
|
|
83
|
+
fragments: stableFragments(fragments),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
function isSubset(child, parent) {
|
|
89
|
+
const parentSet = new Set(parent);
|
|
90
|
+
return child.every((value) => parentSet.has(value));
|
|
91
|
+
}
|
|
92
|
+
/** Return whether a child authority is no broader than its parent. */
|
|
93
|
+
export function isCapabilityAuthoritySubset(child, parent) {
|
|
94
|
+
if (!isSubset(child.effects, parent.effects))
|
|
95
|
+
return false;
|
|
96
|
+
if (!isSubset(child.capabilities, parent.capabilities))
|
|
97
|
+
return false;
|
|
98
|
+
if (!isSubset(child.secrets, parent.secrets))
|
|
99
|
+
return false;
|
|
100
|
+
return Object.entries(child.fragments).every(([key, value]) => Object.is(parent.fragments[key], value));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validate child delegation before dispatch. Errors use stable category and
|
|
104
|
+
* sorted-value ordering so callers and conformance tests can inspect them.
|
|
105
|
+
*/
|
|
106
|
+
export function assertCapabilityAuthoritySubset(child, parent, path = "child authority") {
|
|
107
|
+
const errors = [];
|
|
108
|
+
const check = (category, childValues, parentValues) => {
|
|
109
|
+
const parentSet = new Set(parentValues);
|
|
110
|
+
const widened = childValues.filter((value) => !parentSet.has(value)).sort();
|
|
111
|
+
if (widened.length > 0)
|
|
112
|
+
errors.push(`${path}.${category} contains unauthorized value(s): ${widened.join(", ")}`);
|
|
113
|
+
};
|
|
114
|
+
check("effects", child.effects, parent.effects);
|
|
115
|
+
check("capabilities", child.capabilities, parent.capabilities);
|
|
116
|
+
check("secrets", child.secrets, parent.secrets);
|
|
117
|
+
const missingFragments = Object.entries(child.fragments)
|
|
118
|
+
.filter(([key, value]) => !Object.is(parent.fragments[key], value))
|
|
119
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
120
|
+
for (const [key, value] of missingFragments) {
|
|
121
|
+
errors.push(`${path}.fragments.${key} is not permitted: ${JSON.stringify(value)}`);
|
|
122
|
+
}
|
|
123
|
+
if (errors.length > 0)
|
|
124
|
+
throw new CapabilityAuthorityError(errors.sort((left, right) => left.localeCompare(right)));
|
|
125
|
+
}
|