@blokjs/shared 2.0.1 → 2.2.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.
Files changed (49) hide show
  1. package/dist/AgentSessionContracts.d.ts +316 -0
  2. package/dist/AgentSessionContracts.js +331 -0
  3. package/dist/BlokError.d.ts +55 -1
  4. package/dist/BlokError.js +99 -1
  5. package/dist/CapabilityContracts.d.ts +91 -0
  6. package/dist/CapabilityContracts.js +104 -0
  7. package/dist/CapabilityManifest.d.ts +67 -0
  8. package/dist/CapabilityManifest.js +172 -0
  9. package/dist/EnforcementContracts.d.ts +61 -0
  10. package/dist/EnforcementContracts.js +17 -0
  11. package/dist/EnforcementProfileContracts.d.ts +36 -0
  12. package/dist/EnforcementProfileContracts.js +55 -0
  13. package/dist/EvidenceContracts.d.ts +884 -0
  14. package/dist/EvidenceContracts.js +237 -0
  15. package/dist/GitCapabilityContracts.d.ts +103 -0
  16. package/dist/GitCapabilityContracts.js +222 -0
  17. package/dist/GlobalError.js +5 -1
  18. package/dist/GlobalLogger.d.ts +2 -0
  19. package/dist/GlobalLogger.js +4 -0
  20. package/dist/GraphContracts.d.ts +1643 -0
  21. package/dist/GraphContracts.js +333 -0
  22. package/dist/InteractionContracts.d.ts +76 -0
  23. package/dist/InteractionContracts.js +218 -0
  24. package/dist/JoinContracts.d.ts +593 -0
  25. package/dist/JoinContracts.js +329 -0
  26. package/dist/NodeBase.d.ts +20 -0
  27. package/dist/NodeBase.js +57 -6
  28. package/dist/PermissionAlgebra.d.ts +51 -0
  29. package/dist/PermissionAlgebra.js +125 -0
  30. package/dist/PolicyContracts.d.ts +184 -0
  31. package/dist/PolicyContracts.js +1 -0
  32. package/dist/ProcessCapabilityContracts.d.ts +146 -0
  33. package/dist/ProcessCapabilityContracts.js +263 -0
  34. package/dist/RuntimeContracts.d.ts +125 -0
  35. package/dist/RuntimeContracts.js +108 -0
  36. package/dist/SecretContracts.d.ts +43 -0
  37. package/dist/SecretContracts.js +1 -0
  38. package/dist/WasiComponentContracts.d.ts +582 -0
  39. package/dist/WasiComponentContracts.js +192 -0
  40. package/dist/WorkflowBindingContracts.d.ts +1062 -0
  41. package/dist/WorkflowBindingContracts.js +339 -0
  42. package/dist/index.d.ts +46 -15
  43. package/dist/index.js +26 -3
  44. package/dist/types/LoggerContext.d.ts +7 -0
  45. package/dist/utils/Mapper.d.ts +14 -0
  46. package/dist/utils/Mapper.js +32 -0
  47. package/dist/utils/lowerRefs.d.ts +25 -1
  48. package/dist/utils/lowerRefs.js +8 -3
  49. package/package.json +3 -2
@@ -204,7 +204,9 @@ export default class BlokError extends GlobalError {
204
204
  export declare const WORKFLOW_INPUT_VALIDATION = "WORKFLOW_INPUT_VALIDATION";
205
205
  /**
206
206
  * True when `err` is the ADR-0015 **input-validation gate's** deterministic
207
- * failure — a `GlobalError` carrying the {@link WORKFLOW_INPUT_VALIDATION} tag.
207
+ * failure — a `GlobalError` carrying the {@link WORKFLOW_INPUT_VALIDATION} tag
208
+ * (in practice a {@link WorkflowInputValidationError}; the tag is what's matched
209
+ * so an error that crossed a serialization boundary still classifies).
208
210
  *
209
211
  * Triggers consult this to route the error to DLQ / drop / a 4xx response
210
212
  * instead of a poison-message loop (worker burning its retry budget, pub/sub
@@ -217,3 +219,55 @@ export declare const WORKFLOW_INPUT_VALIDATION = "WORKFLOW_INPUT_VALIDATION";
217
219
  * never surfaced by the webhook 4xx path.
218
220
  */
219
221
  export declare function isNonRetryableValidationError(err: unknown): boolean;
222
+ /**
223
+ * Selective retry — THE matcher for a step's `retry.nonRetryableErrorNames`.
224
+ * Inspects the error name through wrapped causes: both plain `Error.name` and
225
+ * `GlobalError.context.name` count, and `cause` chains are walked to a bounded
226
+ * depth so an enriched or re-thrown error cannot hide a non-retryable
227
+ * classification.
228
+ *
229
+ * `RunnerSteps` calls this to short-circuit STEP retries, then records the
230
+ * verdict on the propagating error with {@link markNonRetryableStepError}.
231
+ * Transports read the verdict back with {@link isNonRetryableStepError} rather
232
+ * than re-deriving it, so JOB-level retry (#679 — BullMQ and every other worker
233
+ * adapter) agrees with step-level retry by construction instead of via a second
234
+ * implementation that could drift.
235
+ */
236
+ export declare function isNonRetryableError(error: unknown, names: string[] | undefined): boolean;
237
+ /** Record a {@link isNonRetryableError} match on `error` (no-op for non-objects). */
238
+ export declare function markNonRetryableStepError(error: unknown): void;
239
+ /**
240
+ * True when the runner classified this failure — or one it wraps — as declared
241
+ * non-retryable. Walks `cause` to the same bounded depth as
242
+ * {@link isNonRetryableError} so an outer wrap cannot hide the verdict.
243
+ */
244
+ export declare function isNonRetryableStepError(error: unknown): boolean;
245
+ /** One Zod issue, flattened to plain data for the wire. */
246
+ export interface WorkflowInputValidationIssue {
247
+ path: (string | number)[];
248
+ message: string;
249
+ code: string;
250
+ }
251
+ /** What the ADR-0015 gate knows about the rejection. */
252
+ export interface WorkflowInputValidationInfo {
253
+ /** The workflow whose declared `input` schema rejected the payload. */
254
+ workflowName: string;
255
+ /** Every Zod issue, in schema order. */
256
+ issues: WorkflowInputValidationIssue[];
257
+ }
258
+ /**
259
+ * ADR 0015 — thrown by the input gate in `TriggerBase.run()` when the invoking
260
+ * trigger's payload fails the workflow's declared `input` Zod.
261
+ *
262
+ * Named + exported so callers can `instanceof` it, matching the vocabulary of
263
+ * the other gate errors (`ConcurrencyLimitError`, `QueueExpiredError`,
264
+ * `MapperResolutionError`). It extends `GlobalError` — carrying code 400, the
265
+ * {@link WORKFLOW_INPUT_VALIDATION} tag on `context.name`, and the structured
266
+ * `validation_errors` json — so every existing transport translation
267
+ * (HTTP 400, MCP `isError`, gRPC error status, worker/pubsub/webhook DLQ
268
+ * routing via {@link isNonRetryableValidationError}) keeps working untouched.
269
+ */
270
+ export declare class WorkflowInputValidationError extends GlobalError {
271
+ readonly info: WorkflowInputValidationInfo;
272
+ constructor(info: WorkflowInputValidationInfo);
273
+ }
package/dist/BlokError.js CHANGED
@@ -335,7 +335,9 @@ export default class BlokError extends GlobalError {
335
335
  export const WORKFLOW_INPUT_VALIDATION = "WORKFLOW_INPUT_VALIDATION";
336
336
  /**
337
337
  * True when `err` is the ADR-0015 **input-validation gate's** deterministic
338
- * failure — a `GlobalError` carrying the {@link WORKFLOW_INPUT_VALIDATION} tag.
338
+ * failure — a `GlobalError` carrying the {@link WORKFLOW_INPUT_VALIDATION} tag
339
+ * (in practice a {@link WorkflowInputValidationError}; the tag is what's matched
340
+ * so an error that crossed a serialization boundary still classifies).
339
341
  *
340
342
  * Triggers consult this to route the error to DLQ / drop / a 4xx response
341
343
  * instead of a poison-message loop (worker burning its retry budget, pub/sub
@@ -350,3 +352,99 @@ export const WORKFLOW_INPUT_VALIDATION = "WORKFLOW_INPUT_VALIDATION";
350
352
  export function isNonRetryableValidationError(err) {
351
353
  return err instanceof GlobalError && err.context.name === WORKFLOW_INPUT_VALIDATION;
352
354
  }
355
+ /**
356
+ * How far a `cause` chain is walked when classifying an error. Bounded so a
357
+ * self-referential or pathologically deep chain can't spin.
358
+ */
359
+ const CAUSE_WALK_DEPTH = 8;
360
+ /**
361
+ * Selective retry — THE matcher for a step's `retry.nonRetryableErrorNames`.
362
+ * Inspects the error name through wrapped causes: both plain `Error.name` and
363
+ * `GlobalError.context.name` count, and `cause` chains are walked to a bounded
364
+ * depth so an enriched or re-thrown error cannot hide a non-retryable
365
+ * classification.
366
+ *
367
+ * `RunnerSteps` calls this to short-circuit STEP retries, then records the
368
+ * verdict on the propagating error with {@link markNonRetryableStepError}.
369
+ * Transports read the verdict back with {@link isNonRetryableStepError} rather
370
+ * than re-deriving it, so JOB-level retry (#679 — BullMQ and every other worker
371
+ * adapter) agrees with step-level retry by construction instead of via a second
372
+ * implementation that could drift.
373
+ */
374
+ export function isNonRetryableError(error, names) {
375
+ if (!names || names.length === 0)
376
+ return false;
377
+ let current = error;
378
+ for (let depth = 0; depth < CAUSE_WALK_DEPTH && current && typeof current === "object"; depth++) {
379
+ const candidate = current;
380
+ if (typeof candidate.name === "string" && names.includes(candidate.name))
381
+ return true;
382
+ if (candidate.context &&
383
+ typeof candidate.context === "object" &&
384
+ typeof candidate.context.name === "string" &&
385
+ names.includes(candidate.context.name)) {
386
+ return true;
387
+ }
388
+ current = candidate.cause;
389
+ }
390
+ return false;
391
+ }
392
+ /**
393
+ * Property the runner stamps on a failure {@link isNonRetryableError} matched.
394
+ * Sticky-TRUE only: its absence means "never classified", never "retryable" —
395
+ * the runner re-wraps errors several times (step enrichment → GlobalError
396
+ * unwrap) and an intermediate wrap whose own step declared nothing must not
397
+ * erase an inner verdict.
398
+ */
399
+ const NON_RETRYABLE_STEP_ERROR = "_blokNonRetryable";
400
+ /** Record a {@link isNonRetryableError} match on `error` (no-op for non-objects). */
401
+ export function markNonRetryableStepError(error) {
402
+ if (error && typeof error === "object") {
403
+ error[NON_RETRYABLE_STEP_ERROR] = true;
404
+ }
405
+ }
406
+ /**
407
+ * True when the runner classified this failure — or one it wraps — as declared
408
+ * non-retryable. Walks `cause` to the same bounded depth as
409
+ * {@link isNonRetryableError} so an outer wrap cannot hide the verdict.
410
+ */
411
+ export function isNonRetryableStepError(error) {
412
+ let current = error;
413
+ for (let depth = 0; depth < CAUSE_WALK_DEPTH && current && typeof current === "object"; depth++) {
414
+ if (current[NON_RETRYABLE_STEP_ERROR] === true)
415
+ return true;
416
+ current = current.cause;
417
+ }
418
+ return false;
419
+ }
420
+ /**
421
+ * ADR 0015 — thrown by the input gate in `TriggerBase.run()` when the invoking
422
+ * trigger's payload fails the workflow's declared `input` Zod.
423
+ *
424
+ * Named + exported so callers can `instanceof` it, matching the vocabulary of
425
+ * the other gate errors (`ConcurrencyLimitError`, `QueueExpiredError`,
426
+ * `MapperResolutionError`). It extends `GlobalError` — carrying code 400, the
427
+ * {@link WORKFLOW_INPUT_VALIDATION} tag on `context.name`, and the structured
428
+ * `validation_errors` json — so every existing transport translation
429
+ * (HTTP 400, MCP `isError`, gRPC error status, worker/pubsub/webhook DLQ
430
+ * routing via {@link isNonRetryableValidationError}) keeps working untouched.
431
+ */
432
+ export class WorkflowInputValidationError extends GlobalError {
433
+ info;
434
+ constructor(info) {
435
+ const summary = info.issues.map((i) => `${i.path.join(".") || "(root)"} (${i.message})`).join(", ");
436
+ super(`Input validation failed for workflow '${info.workflowName}': ${summary}`);
437
+ // `GlobalError`'s own constructor pins the prototype to GlobalError.prototype,
438
+ // so a subclass MUST re-pin or `instanceof WorkflowInputValidationError` is false.
439
+ Object.setPrototypeOf(this, WorkflowInputValidationError.prototype);
440
+ this.name = "WorkflowInputValidationError";
441
+ this.info = info;
442
+ this.setCode(400);
443
+ this.setName(WORKFLOW_INPUT_VALIDATION);
444
+ this.setJson({
445
+ error: "Input validation failed",
446
+ workflowName: info.workflowName,
447
+ validation_errors: info.issues,
448
+ });
449
+ }
450
+ }
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import type { PolicyContext, PolicyDecision, PolicyEvaluationResult, PolicyProvider } from "./PolicyContracts.js";
3
+ import type { PrincipalIdentity, RequestedCapabilityScope } from "./PolicyContracts.js";
4
+ /** Version shared by the H3 capability contracts. */
5
+ export declare const AGENT_CAPABILITY_CONTRACT_VERSION: "1";
6
+ export declare const CAPABILITY_MAX_ID_LENGTH = 128;
7
+ export declare const CAPABILITY_MAX_PATH_LENGTH = 4096;
8
+ export declare const CAPABILITY_MAX_LIST_ITEMS = 1024;
9
+ export declare const CAPABILITY_MAX_OUTPUT_CHUNK_BYTES: number;
10
+ export interface CapabilityOwner {
11
+ readonly principal: PrincipalIdentity;
12
+ readonly sessionId: string;
13
+ readonly turnId?: string;
14
+ readonly taskId: string;
15
+ }
16
+ /**
17
+ * An opaque reference into the workspace capability (#927). H3 contracts do
18
+ * not canonicalize paths or access the host filesystem; the filesystem
19
+ * capability owns that boundary.
20
+ */
21
+ export interface WorkspacePathRef {
22
+ readonly workspaceId: string;
23
+ readonly path: string;
24
+ }
25
+ export interface CapabilityRequestContext {
26
+ readonly policy: PolicyContext;
27
+ readonly owner: CapabilityOwner;
28
+ }
29
+ /** Every trusted adapter must authorize before doing host-side work. */
30
+ export interface CapabilityAuthorizationPort {
31
+ authorize(request: PolicyContext): Promise<PolicyEvaluationResult>;
32
+ readonly provider?: PolicyProvider;
33
+ }
34
+ export declare class CapabilityContractError extends Error {
35
+ readonly code = "CAPABILITY_CONTRACT_INVALID";
36
+ constructor(message: string);
37
+ }
38
+ declare const identifier: z.ZodString;
39
+ declare const path: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>;
40
+ declare const timestamp: z.ZodEffects<z.ZodString, string, string>;
41
+ declare const digest: z.ZodEffects<z.ZodString, string, string>;
42
+ export declare const CapabilityOwnerSchema: z.ZodObject<{
43
+ principal: z.ZodObject<{
44
+ id: z.ZodString;
45
+ kind: z.ZodString;
46
+ }, "strip", z.ZodTypeAny, {
47
+ id: string;
48
+ kind: string;
49
+ }, {
50
+ id: string;
51
+ kind: string;
52
+ }>;
53
+ sessionId: z.ZodString;
54
+ turnId: z.ZodOptional<z.ZodString>;
55
+ taskId: z.ZodString;
56
+ }, "strip", z.ZodTypeAny, {
57
+ sessionId: string;
58
+ principal: {
59
+ id: string;
60
+ kind: string;
61
+ };
62
+ taskId: string;
63
+ turnId?: string | undefined;
64
+ }, {
65
+ sessionId: string;
66
+ principal: {
67
+ id: string;
68
+ kind: string;
69
+ };
70
+ taskId: string;
71
+ turnId?: string | undefined;
72
+ }>;
73
+ export declare const WorkspacePathRefSchema: z.ZodObject<{
74
+ workspaceId: z.ZodString;
75
+ path: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>;
76
+ }, "strip", z.ZodTypeAny, {
77
+ path: string;
78
+ workspaceId: string;
79
+ }, {
80
+ path: string;
81
+ workspaceId: string;
82
+ }>;
83
+ export declare function parseCapabilityOwner(value: unknown): CapabilityOwner;
84
+ export declare function parseWorkspacePathRef(value: unknown): WorkspacePathRef;
85
+ export declare function parseCapabilityAuthorization(decision: PolicyDecision, result?: PolicyEvaluationResult): PolicyEvaluationResult;
86
+ export declare function assertAuthorized(result: PolicyEvaluationResult, options?: {
87
+ allowSandbox?: boolean;
88
+ }): void;
89
+ export declare function assertOwned(owner: CapabilityOwner, expected: CapabilityOwner): void;
90
+ export declare function capabilityScope(effects: RequestedCapabilityScope["effects"], capabilities: readonly string[], secrets?: readonly string[]): RequestedCapabilityScope;
91
+ export { digest, identifier, path, timestamp };
@@ -0,0 +1,104 @@
1
+ import { z } from "zod";
2
+ /** Version shared by the H3 capability contracts. */
3
+ export const AGENT_CAPABILITY_CONTRACT_VERSION = "1";
4
+ export const CAPABILITY_MAX_ID_LENGTH = 128;
5
+ export const CAPABILITY_MAX_PATH_LENGTH = 4_096;
6
+ export const CAPABILITY_MAX_LIST_ITEMS = 1_024;
7
+ export const CAPABILITY_MAX_OUTPUT_CHUNK_BYTES = 64 * 1024;
8
+ export class CapabilityContractError extends Error {
9
+ code = "CAPABILITY_CONTRACT_INVALID";
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "CapabilityContractError";
13
+ }
14
+ }
15
+ const identifier = z
16
+ .string()
17
+ .min(1)
18
+ .max(CAPABILITY_MAX_ID_LENGTH)
19
+ .regex(/^[A-Za-z][A-Za-z0-9._:/-]*$/);
20
+ const path = z
21
+ .string()
22
+ .min(1)
23
+ .max(CAPABILITY_MAX_PATH_LENGTH)
24
+ .refine((value) => !value.includes("\0"), "must not contain NUL")
25
+ .refine((value) => !value.startsWith("/") && !/^[A-Za-z]:[\\/]/.test(value), "must be workspace-relative")
26
+ .refine((value) => !value.split(/[\\/]+/).includes(".."), "must not escape the workspace");
27
+ const timestamp = z.string().refine((value) => {
28
+ if (!Number.isFinite(Date.parse(value)))
29
+ return false;
30
+ return new Date(value).toISOString() === value;
31
+ }, "must be a canonical ISO timestamp");
32
+ const sessionId = z.string().min(1).max(CAPABILITY_MAX_ID_LENGTH);
33
+ const digest = z
34
+ .string()
35
+ .regex(/^(?:sha256):[0-9a-f]{64}$|^(?:sha512):[0-9a-f]{128}$/i)
36
+ .transform((value) => value.toLowerCase());
37
+ const ownerSchema = z.object({
38
+ principal: z.object({ id: identifier, kind: identifier }),
39
+ // Session IDs are opaque references and the control plane uses UUIDs.
40
+ sessionId,
41
+ turnId: identifier.optional(),
42
+ taskId: identifier,
43
+ });
44
+ const workspacePathSchema = z.object({ workspaceId: identifier, path });
45
+ export const CapabilityOwnerSchema = ownerSchema;
46
+ export const WorkspacePathRefSchema = workspacePathSchema;
47
+ function parse(schema, value, label) {
48
+ const result = schema.safeParse(value);
49
+ if (!result.success) {
50
+ throw new CapabilityContractError(result.error.issues
51
+ .map((issue) => `${label}${issue.path.length > 0 ? `.${issue.path.join(".")}` : ""} ${issue.message}`)
52
+ .join("; "));
53
+ }
54
+ return result.data;
55
+ }
56
+ function immutable(value) {
57
+ const snapshot = structuredClone(value);
58
+ const freeze = (item) => {
59
+ if (item === null || typeof item !== "object" || Object.isFrozen(item))
60
+ return;
61
+ for (const child of Object.values(item))
62
+ freeze(child);
63
+ Object.freeze(item);
64
+ };
65
+ freeze(snapshot);
66
+ return snapshot;
67
+ }
68
+ export function parseCapabilityOwner(value) {
69
+ return immutable(parse(ownerSchema, value, "capability owner"));
70
+ }
71
+ export function parseWorkspacePathRef(value) {
72
+ return immutable(parse(workspacePathSchema, value, "workspace path"));
73
+ }
74
+ export function parseCapabilityAuthorization(decision, result) {
75
+ if (!result || result.decision.id !== decision.id) {
76
+ throw new CapabilityContractError("authorization result does not match the policy decision");
77
+ }
78
+ return result;
79
+ }
80
+ export function assertAuthorized(result, options = {}) {
81
+ if (result.decision.kind === "allow")
82
+ return;
83
+ if (options.allowSandbox && result.decision.kind === "require-sandbox" && result.sandbox?.proof)
84
+ return;
85
+ throw new CapabilityContractError(`capability execution is not authorized: ${result.decision.kind}`);
86
+ }
87
+ export function assertOwned(owner, expected) {
88
+ if (owner.principal.id !== expected.principal.id ||
89
+ owner.principal.kind !== expected.principal.kind ||
90
+ owner.sessionId !== expected.sessionId ||
91
+ owner.taskId !== expected.taskId ||
92
+ (owner.turnId ?? "") !== (expected.turnId ?? "")) {
93
+ throw new CapabilityContractError("capability handle is owned by a different task context");
94
+ }
95
+ }
96
+ export function capabilityScope(effects, capabilities, secrets = []) {
97
+ return {
98
+ effects: [...new Set(effects)].sort(),
99
+ capabilities: [...new Set(capabilities)].sort(),
100
+ secrets: [...new Set(secrets)].sort(),
101
+ fragments: {},
102
+ };
103
+ }
104
+ export { digest, identifier, path, timestamp };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Language-neutral operational metadata for nodes and workflows (ADR 0003).
3
+ *
4
+ * Schemas describe values; this manifest describes risk. It is deliberately
5
+ * data-only so every SDK can emit the same JSON through gRPC `ListNodes`.
6
+ * Unknown object fields are ignored when parsing v1, allowing additive wire
7
+ * evolution, while an unknown VERSION remains ineligible for agent execution.
8
+ */
9
+ export declare const CAPABILITY_MANIFEST_VERSION: "1";
10
+ export declare const CAPABILITY_EFFECTS: readonly ["read", "write", "network", "filesystem", "process", "secret", "streaming", "destructive"];
11
+ export declare const CAPABILITY_CLASSIFICATIONS: readonly ["agent-compatible", "trusted-legacy", "denied-to-agents"];
12
+ export declare const CAPABILITY_DETERMINISM: readonly ["deterministic", "time-dependent", "random", "external", "unknown"];
13
+ export declare const CAPABILITY_IDEMPOTENCY: readonly ["idempotent", "conditionally-idempotent", "non-idempotent", "unknown"];
14
+ export declare const CAPABILITY_MATURITY: readonly ["stable", "beta", "experimental", "deprecated"];
15
+ export type CapabilityEffect = (typeof CAPABILITY_EFFECTS)[number];
16
+ export type CapabilityClassification = (typeof CAPABILITY_CLASSIFICATIONS)[number];
17
+ export type CapabilityDeterminism = (typeof CAPABILITY_DETERMINISM)[number];
18
+ export type CapabilityIdempotency = (typeof CAPABILITY_IDEMPOTENCY)[number];
19
+ export type CapabilityMaturity = (typeof CAPABILITY_MATURITY)[number];
20
+ export interface CapabilityResourceBounds {
21
+ maxDurationMs?: number;
22
+ maxMemoryBytes?: number;
23
+ maxInputBytes?: number;
24
+ maxOutputBytes?: number;
25
+ maxConcurrency?: number;
26
+ }
27
+ export interface CapabilityManifestV1 {
28
+ version: typeof CAPABILITY_MANIFEST_VERSION;
29
+ /** Agent compatibility is explicit; absence never implies safety. */
30
+ classification: CapabilityClassification;
31
+ /** Empty means pure: no externally observable operational effect. */
32
+ effects: CapabilityEffect[];
33
+ /** Fine-grained capability identifiers, e.g. `network.http` or `fs.workspace.read`. */
34
+ capabilities: string[];
35
+ /** Opaque secret reference NAMES only. Never put credentials here. */
36
+ secrets: string[];
37
+ determinism: CapabilityDeterminism;
38
+ idempotency: CapabilityIdempotency;
39
+ maturity: CapabilityMaturity;
40
+ resources?: CapabilityResourceBounds;
41
+ /** Optional applicability constraints. Empty/absent means unrestricted. */
42
+ runtimes?: string[];
43
+ triggers?: string[];
44
+ }
45
+ export type CapabilityManifestStatus = "declared" | "missing" | "invalid";
46
+ export interface CapabilityManifestAssessment {
47
+ status: CapabilityManifestStatus;
48
+ manifest: CapabilityManifestV1 | null;
49
+ errors: string[];
50
+ agentEligible: boolean;
51
+ reason: "eligible" | "missing-manifest" | "invalid-manifest" | "trusted-legacy" | "denied-to-agents";
52
+ }
53
+ export declare class CapabilityManifestError extends Error {
54
+ readonly errors: readonly string[];
55
+ constructor(errors: readonly string[]);
56
+ }
57
+ /** Parse and normalize a v1 manifest. Additive unknown fields are ignored. */
58
+ export declare function parseCapabilityManifest(value: unknown): CapabilityManifestV1;
59
+ /** Stable JSON bytes/order for catalog reflection and cross-runtime fixtures. */
60
+ export declare function serializeCapabilityManifest(value: unknown): string;
61
+ /**
62
+ * Catalog/policy boundary. Missing and invalid metadata are explicit and
63
+ * agent-ineligible; ordinary execution remains unchanged until policy opts in.
64
+ */
65
+ export declare function assessCapabilityManifest(value: unknown): CapabilityManifestAssessment;
66
+ /** Fail-closed helper for the future agent execution policy boundary. */
67
+ export declare function requireAgentEligibleManifest(value: unknown): CapabilityManifestV1;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Language-neutral operational metadata for nodes and workflows (ADR 0003).
3
+ *
4
+ * Schemas describe values; this manifest describes risk. It is deliberately
5
+ * data-only so every SDK can emit the same JSON through gRPC `ListNodes`.
6
+ * Unknown object fields are ignored when parsing v1, allowing additive wire
7
+ * evolution, while an unknown VERSION remains ineligible for agent execution.
8
+ */
9
+ export const CAPABILITY_MANIFEST_VERSION = "1";
10
+ export const CAPABILITY_EFFECTS = [
11
+ "read",
12
+ "write",
13
+ "network",
14
+ "filesystem",
15
+ "process",
16
+ "secret",
17
+ "streaming",
18
+ "destructive",
19
+ ];
20
+ export const CAPABILITY_CLASSIFICATIONS = ["agent-compatible", "trusted-legacy", "denied-to-agents"];
21
+ export const CAPABILITY_DETERMINISM = ["deterministic", "time-dependent", "random", "external", "unknown"];
22
+ export const CAPABILITY_IDEMPOTENCY = ["idempotent", "conditionally-idempotent", "non-idempotent", "unknown"];
23
+ export const CAPABILITY_MATURITY = ["stable", "beta", "experimental", "deprecated"];
24
+ export class CapabilityManifestError extends Error {
25
+ errors;
26
+ constructor(errors) {
27
+ super(`Invalid capability manifest: ${errors.join("; ")}`);
28
+ this.name = "CapabilityManifestError";
29
+ this.errors = [...errors];
30
+ }
31
+ }
32
+ const IDENTIFIER = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/;
33
+ const EFFECT_SET = new Set(CAPABILITY_EFFECTS);
34
+ const CLASSIFICATION_SET = new Set(CAPABILITY_CLASSIFICATIONS);
35
+ const DETERMINISM_SET = new Set(CAPABILITY_DETERMINISM);
36
+ const IDEMPOTENCY_SET = new Set(CAPABILITY_IDEMPOTENCY);
37
+ const MATURITY_SET = new Set(CAPABILITY_MATURITY);
38
+ const RESOURCE_KEYS = ["maxDurationMs", "maxMemoryBytes", "maxInputBytes", "maxOutputBytes", "maxConcurrency"];
39
+ function isRecord(value) {
40
+ return value !== null && typeof value === "object" && !Array.isArray(value);
41
+ }
42
+ function enumValue(value, path, allowed, errors) {
43
+ if (typeof value !== "string" || !allowed.has(value)) {
44
+ errors.push(`${path} must be one of: ${[...allowed].join(", ")}`);
45
+ return undefined;
46
+ }
47
+ return value;
48
+ }
49
+ function stringList(value, path, errors, allowed) {
50
+ if (!Array.isArray(value)) {
51
+ errors.push(`${path} must be an array`);
52
+ return [];
53
+ }
54
+ const result = new Set();
55
+ for (let i = 0; i < value.length; i++) {
56
+ const item = value[i];
57
+ if (typeof item !== "string" || (!allowed && !IDENTIFIER.test(item)) || (allowed && !allowed.has(item))) {
58
+ errors.push(`${path}[${i}] is not a valid ${allowed ? "value" : "identifier"}`);
59
+ continue;
60
+ }
61
+ result.add(item);
62
+ }
63
+ return [...result].sort();
64
+ }
65
+ function resourceBounds(value, errors) {
66
+ if (value === undefined)
67
+ return undefined;
68
+ if (!isRecord(value)) {
69
+ errors.push("resources must be an object");
70
+ return undefined;
71
+ }
72
+ const result = {};
73
+ for (const key of RESOURCE_KEYS) {
74
+ const raw = value[key];
75
+ if (raw === undefined)
76
+ continue;
77
+ if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw <= 0) {
78
+ errors.push(`resources.${key} must be a positive safe integer`);
79
+ continue;
80
+ }
81
+ result[key] = raw;
82
+ }
83
+ return Object.keys(result).length > 0 ? result : undefined;
84
+ }
85
+ /** Parse and normalize a v1 manifest. Additive unknown fields are ignored. */
86
+ export function parseCapabilityManifest(value) {
87
+ const errors = [];
88
+ if (!isRecord(value))
89
+ throw new CapabilityManifestError(["manifest must be an object"]);
90
+ if (value.version !== CAPABILITY_MANIFEST_VERSION) {
91
+ errors.push(`version must be ${CAPABILITY_MANIFEST_VERSION}`);
92
+ }
93
+ const classification = enumValue(value.classification, "classification", CLASSIFICATION_SET, errors);
94
+ const determinism = enumValue(value.determinism, "determinism", DETERMINISM_SET, errors);
95
+ const idempotency = enumValue(value.idempotency, "idempotency", IDEMPOTENCY_SET, errors);
96
+ const maturity = enumValue(value.maturity, "maturity", MATURITY_SET, errors);
97
+ const effects = stringList(value.effects, "effects", errors, EFFECT_SET);
98
+ const capabilities = stringList(value.capabilities, "capabilities", errors);
99
+ const secrets = stringList(value.secrets, "secrets", errors);
100
+ const resources = resourceBounds(value.resources, errors);
101
+ const runtimes = value.runtimes === undefined ? undefined : stringList(value.runtimes, "runtimes", errors);
102
+ const triggers = value.triggers === undefined ? undefined : stringList(value.triggers, "triggers", errors);
103
+ if (errors.length > 0 || !classification || !determinism || !idempotency || !maturity) {
104
+ throw new CapabilityManifestError(errors);
105
+ }
106
+ return {
107
+ version: CAPABILITY_MANIFEST_VERSION,
108
+ classification,
109
+ effects,
110
+ capabilities,
111
+ secrets,
112
+ determinism,
113
+ idempotency,
114
+ maturity,
115
+ ...(resources ? { resources } : {}),
116
+ ...(runtimes ? { runtimes } : {}),
117
+ ...(triggers ? { triggers } : {}),
118
+ };
119
+ }
120
+ /** Stable JSON bytes/order for catalog reflection and cross-runtime fixtures. */
121
+ export function serializeCapabilityManifest(value) {
122
+ return JSON.stringify(parseCapabilityManifest(value));
123
+ }
124
+ /**
125
+ * Catalog/policy boundary. Missing and invalid metadata are explicit and
126
+ * agent-ineligible; ordinary execution remains unchanged until policy opts in.
127
+ */
128
+ export function assessCapabilityManifest(value) {
129
+ if (value === undefined || value === null || value === "") {
130
+ return {
131
+ status: "missing",
132
+ manifest: null,
133
+ errors: [],
134
+ agentEligible: false,
135
+ reason: "missing-manifest",
136
+ };
137
+ }
138
+ try {
139
+ const manifest = parseCapabilityManifest(value);
140
+ const eligible = manifest.classification === "agent-compatible";
141
+ const reason = eligible
142
+ ? "eligible"
143
+ : manifest.classification === "trusted-legacy"
144
+ ? "trusted-legacy"
145
+ : "denied-to-agents";
146
+ return {
147
+ status: "declared",
148
+ manifest,
149
+ errors: [],
150
+ agentEligible: eligible,
151
+ reason,
152
+ };
153
+ }
154
+ catch (error) {
155
+ const errors = error instanceof CapabilityManifestError ? [...error.errors] : [String(error)];
156
+ return {
157
+ status: "invalid",
158
+ manifest: null,
159
+ errors,
160
+ agentEligible: false,
161
+ reason: "invalid-manifest",
162
+ };
163
+ }
164
+ }
165
+ /** Fail-closed helper for the future agent execution policy boundary. */
166
+ export function requireAgentEligibleManifest(value) {
167
+ const assessment = assessCapabilityManifest(value);
168
+ if (assessment.agentEligible && assessment.manifest)
169
+ return assessment.manifest;
170
+ const detail = assessment.errors.length > 0 ? assessment.errors : [assessment.reason];
171
+ throw new CapabilityManifestError(detail);
172
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * H1-02 contracts shared by authoring, the runner, and control-plane adapters.
3
+ *
4
+ * These are deliberately data-only. A model can return data that resembles a
5
+ * trusted record, but the runner decides whether the producing step is allowed
6
+ * to establish trusted provenance.
7
+ */
8
+ export declare const ENFORCEMENT_CONTRACT_VERSION: "1";
9
+ export type OutputTrust = "model" | "trusted";
10
+ export interface AgentCompletionContract {
11
+ readonly required?: boolean;
12
+ /** Dot-separated output path; defaults to `completed`. */
13
+ readonly path?: string;
14
+ /** Expected value at `path`; defaults to `true`. */
15
+ readonly equals?: unknown;
16
+ }
17
+ export interface AgentStepContract {
18
+ readonly version: typeof ENFORCEMENT_CONTRACT_VERSION;
19
+ readonly objective: string;
20
+ readonly completion: AgentCompletionContract;
21
+ }
22
+ export interface ApprovalContract {
23
+ readonly version: typeof ENFORCEMENT_CONTRACT_VERSION;
24
+ readonly reason: string;
25
+ readonly scope?: string;
26
+ }
27
+ export interface AssertionGateContract {
28
+ readonly version: typeof ENFORCEMENT_CONTRACT_VERSION;
29
+ readonly path?: string;
30
+ readonly equals?: unknown;
31
+ readonly truthy?: boolean;
32
+ readonly message?: string;
33
+ }
34
+ export interface EvidenceGateRequirement {
35
+ readonly artifactId: string;
36
+ readonly artifactVersion: string;
37
+ readonly producerStepId: string;
38
+ }
39
+ export interface EvidenceGateContract {
40
+ readonly version: typeof ENFORCEMENT_CONTRACT_VERSION;
41
+ readonly requirements: readonly EvidenceGateRequirement[];
42
+ }
43
+ export interface TrustedEvidence {
44
+ readonly version: typeof ENFORCEMENT_CONTRACT_VERSION;
45
+ readonly provenance: "trusted";
46
+ readonly producer: {
47
+ readonly stepId: string;
48
+ readonly workflow: string;
49
+ };
50
+ readonly artifact: {
51
+ readonly id: string;
52
+ readonly version: string;
53
+ };
54
+ readonly verified: true;
55
+ readonly verification?: string;
56
+ }
57
+ export declare class EnforcementViolationError extends Error {
58
+ readonly reasonCode: string;
59
+ readonly code = "ENFORCEMENT_REJECTED";
60
+ constructor(reasonCode: string, message: string);
61
+ }