@axtary/actionpass 0.1.0 → 0.3.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/index.js CHANGED
@@ -1,10 +1,25 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
- import { decodeProtectedHeader, SignJWT, jwtVerify, } from "jose";
4
+ import { lock } from "proper-lockfile";
5
+ import { decodeProtectedHeader, calculateJwkThumbprint, importJWK, SignJWT, jwtVerify, } from "jose";
5
6
  import { z } from "zod";
7
+ import { ActionPassStatusClaimSchema, } from "./status-list.js";
8
+ import { ActionProvenanceSchema, } from "./provenance.js";
9
+ import { analyzePostgresSelect } from "./postgres.js";
10
+ import { explainDecision, } from "./explain.js";
11
+ import { AXTARY_DEV_SIGNING_KEY_ENV, devKeypair, } from "./dev-key.js";
12
+ export * from "./caep.js";
13
+ export * from "./dev-key.js";
14
+ export * from "./explain.js";
15
+ export * from "./postgres.js";
16
+ export * from "./provenance.js";
17
+ export * from "./status-list.js";
6
18
  export const ACTION_SCHEMA_VERSION = "axtary.action.v0";
7
- export const ACTIONPASS_VERSION = "axtary.actionpass.v0";
19
+ export const ACTIONPASS_V0_VERSION = "axtary.actionpass.v0";
20
+ export const ACTIONPASS_V1_VERSION = "axtary.actionpass.v1";
21
+ export const ACTIONPASS_V2_VERSION = "axtary.actionpass.v2";
22
+ export const ACTIONPASS_VERSION = ACTIONPASS_V0_VERSION;
8
23
  export const APPROVAL_ARTIFACT_VERSION = "axtary.approval.v0";
9
24
  export const ACTIONPASS_REVOCATION_VERSION = "axtary.actionpass_revocation.v0";
10
25
  export const ACTIONPASS_TRUST_STORE_VERSION = "axtary.actionpass_trust_store.v0";
@@ -13,6 +28,9 @@ export const LEDGER_PROVIDER_EVIDENCE_VERSION = "axtary.ledger_provider_evidence
13
28
  export const LEDGER_EXECUTION_OUTCOME_VERSION = "axtary.ledger_execution_outcome.v0";
14
29
  export const DEFAULT_EXPIRES_IN_SECONDS = 600;
15
30
  export const DEFAULT_SIGNING_ALGORITHM = "ES256";
31
+ export const DPOP_PROOF_TYP = "dpop+jwt";
32
+ export const DEFAULT_DPOP_MAX_AGE_SECONDS = 60;
33
+ export const DEFAULT_DPOP_CLOCK_SKEW_SECONDS = 5;
16
34
  const JsonValueSchema = z.lazy(() => z.union([
17
35
  z.string(),
18
36
  z.number().finite(),
@@ -21,6 +39,13 @@ const JsonValueSchema = z.lazy(() => z.union([
21
39
  z.array(JsonValueSchema),
22
40
  z.record(z.string(), JsonValueSchema),
23
41
  ]));
42
+ export const BudgetAmountSchema = z.record(z.string().min(1), z.number().finite().nonnegative());
43
+ export const ActionBudgetSchema = z.object({
44
+ reservationId: z.string().min(1),
45
+ cost: BudgetAmountSchema,
46
+ limit: BudgetAmountSchema,
47
+ commitStatus: z.enum(["pending", "committed", "rolled_back"]),
48
+ });
24
49
  export const AxtaryDecisionSchema = z.enum(["allow", "deny", "step_up"]);
25
50
  export const NormalizedActionSchema = z.object({
26
51
  schemaVersion: z.literal(ACTION_SCHEMA_VERSION).default(ACTION_SCHEMA_VERSION),
@@ -46,15 +71,18 @@ export const NormalizedActionSchema = z.object({
46
71
  serverIdentity: z.string().min(1),
47
72
  schemaVersion: z.string().min(1),
48
73
  definitionHash: z.string().min(1),
74
+ // Optional signed-publisher provenance (MCP signed definitions, M5.5).
75
+ // Absent for hash-only (unsigned) tools, so existing actions hash
76
+ // identically; present, they bind publisher identity + version lineage
77
+ // into the ActionPass and ledger.
78
+ publisher: z.string().min(1).optional(),
79
+ publisherKeyId: z.string().min(1).optional(),
80
+ semver: z.string().min(1).optional(),
81
+ priorDefinitionHash: z.string().min(1).optional(),
49
82
  })
50
83
  .optional(),
51
- budget: z
52
- .object({
53
- reservationId: z.string().min(1).optional(),
54
- limit: z.record(z.string(), JsonValueSchema).optional(),
55
- commitStatus: z.enum(["pending", "committed", "rolled_back"]).optional(),
56
- })
57
- .optional(),
84
+ provenance: ActionProvenanceSchema.optional(),
85
+ budget: ActionBudgetSchema.optional(),
58
86
  });
59
87
  export const PolicyDecisionSchema = z.object({
60
88
  decision: AxtaryDecisionSchema,
@@ -70,10 +98,37 @@ export const PolicyDecisionSchema = z.object({
70
98
  maxFilesChanged: z.number().int().nonnegative(),
71
99
  blockedPaths: z.array(z.string().min(1)),
72
100
  }),
101
+ rule: z
102
+ .object({
103
+ id: z.string().min(1),
104
+ matchedRuleIds: z.array(z.string().min(1)).default([]),
105
+ })
106
+ .optional(),
107
+ obligations: z
108
+ .object({
109
+ requiredApproverRoles: z.array(z.string().min(1)).default([]),
110
+ timeWindow: z
111
+ .object({
112
+ startHourUtc: z.number().int().min(0).max(23),
113
+ endHourUtc: z.number().int().min(0).max(23),
114
+ daysOfWeek: z.array(z.number().int().min(0).max(6)).default([]),
115
+ })
116
+ .optional(),
117
+ rateLimit: z
118
+ .object({
119
+ ruleId: z.string().min(1),
120
+ maxActions: z.number().int().positive(),
121
+ windowSeconds: z.number().int().positive(),
122
+ scope: z.enum(["actor", "tool", "resource", "tenant"]),
123
+ })
124
+ .optional(),
125
+ })
126
+ .optional(),
73
127
  });
74
128
  export const ApprovalSchema = z.object({
75
129
  mode: z.enum(["none", "human", "policy_override"]),
76
130
  approvedBy: z.string().min(1).optional(),
131
+ approverRoles: z.array(z.string().min(1)).optional(),
77
132
  approvalArtifact: z.string().min(1).optional(),
78
133
  approvalArtifactHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
79
134
  actionHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
@@ -85,6 +140,7 @@ export const ApprovalArtifactSchema = z.object({
85
140
  id: z.string().min(1),
86
141
  mode: z.enum(["human", "policy_override"]),
87
142
  approvedBy: z.string().min(1),
143
+ approverRoles: z.array(z.string().min(1)).optional(),
88
144
  approvedAt: z.string().datetime(),
89
145
  actionHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
90
146
  payloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
@@ -94,8 +150,7 @@ export const ApprovalArtifactSchema = z.object({
94
150
  reason: z.string().min(1).optional(),
95
151
  expiresAt: z.string().datetime().optional(),
96
152
  });
97
- export const ActionPassClaimsSchema = z.object({
98
- apv: z.literal(ACTIONPASS_VERSION),
153
+ const ActionPassBaseClaimsSchema = z.object({
99
154
  iss: z.string().min(1),
100
155
  sub: z.string().min(1),
101
156
  aud: z.union([z.string().min(1), z.array(z.string().min(1))]),
@@ -116,6 +171,8 @@ export const ActionPassClaimsSchema = z.object({
116
171
  reasons: z.array(z.string().min(1)),
117
172
  policy: PolicyDecisionSchema.shape.policy,
118
173
  payloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
174
+ provenanceHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
175
+ budget: ActionBudgetSchema.optional(),
119
176
  approval: ApprovalSchema.optional(),
120
177
  audit: z.object({
121
178
  traceId: z.string().min(1),
@@ -123,6 +180,50 @@ export const ActionPassClaimsSchema = z.object({
123
180
  ledgerHash: z.string().min(1).nullable(),
124
181
  }),
125
182
  });
183
+ export const ActionPassV0ClaimsSchema = z.object({
184
+ apv: z.literal(ACTIONPASS_V0_VERSION),
185
+ ...ActionPassBaseClaimsSchema.shape,
186
+ });
187
+ export const ActionPassV1ClaimsSchema = z.object({
188
+ apv: z.literal(ACTIONPASS_V1_VERSION),
189
+ ...ActionPassBaseClaimsSchema.shape,
190
+ cnf: z.object({
191
+ jkt: z.string().min(1),
192
+ }),
193
+ delegation: z
194
+ .object({
195
+ depth: z.number().int().positive(),
196
+ rootPassId: z.string().min(1),
197
+ })
198
+ .optional(),
199
+ });
200
+ export const ActionPassV2ClaimsSchema = z.object({
201
+ apv: z.literal(ACTIONPASS_V2_VERSION),
202
+ ...ActionPassBaseClaimsSchema.shape,
203
+ cnf: z.object({
204
+ jkt: z.string().min(1),
205
+ }),
206
+ delegation: z
207
+ .object({
208
+ depth: z.number().int().positive(),
209
+ rootPassId: z.string().min(1),
210
+ })
211
+ .optional(),
212
+ status: ActionPassStatusClaimSchema,
213
+ });
214
+ export const ActionPassClaimsSchema = z.union([
215
+ ActionPassV0ClaimsSchema,
216
+ ActionPassV1ClaimsSchema,
217
+ ActionPassV2ClaimsSchema,
218
+ ]);
219
+ export const DpopProofClaimsSchema = z.object({
220
+ htm: z.string().min(1),
221
+ htu: z.string().url(),
222
+ iat: z.number().int().positive(),
223
+ jti: z.string().min(1),
224
+ ath: z.string().min(1),
225
+ nonce: z.string().min(1).optional(),
226
+ });
126
227
  export const ActionPassRevocationSchema = z.object({
127
228
  schemaVersion: z.literal(ACTIONPASS_REVOCATION_VERSION),
128
229
  passId: z.string().min(1),
@@ -167,10 +268,12 @@ export const LedgerProviderSchema = z.enum([
167
268
  "slack",
168
269
  "linear",
169
270
  "docs",
271
+ "drive",
170
272
  "mcp",
171
273
  "aws",
172
274
  "gcp",
173
275
  "jira",
276
+ "postgres",
174
277
  ]);
175
278
  export const LedgerProviderEvidenceDiffSchema = z.object({
176
279
  provider: LedgerProviderSchema,
@@ -200,6 +303,117 @@ export const LedgerProviderEvidenceSchema = z.object({
200
303
  fieldChanges: z.array(LedgerProviderEvidenceFieldChangeSchema).default([]),
201
304
  diff: LedgerProviderEvidenceDiffSchema.optional(),
202
305
  });
306
+ export const GitHubNativeConnectorConfigSchema = z.object({
307
+ mode: z.enum(["fake", "rest", "app"]).default("fake"),
308
+ tokenEnv: z.string().min(1).default("GITHUB_TOKEN"),
309
+ apiBaseUrl: z.string().url().default("https://api.github.com"),
310
+ userAgent: z.string().min(1).default("axtary-local-proxy"),
311
+ appId: z.string().min(1).optional(),
312
+ appIdEnv: z.string().min(1).default("AXTARY_GITHUB_APP_ID"),
313
+ installationId: z.string().min(1).optional(),
314
+ installationIdEnv: z
315
+ .string()
316
+ .min(1)
317
+ .default("AXTARY_GITHUB_INSTALLATION_ID"),
318
+ privateKeyEnv: z
319
+ .string()
320
+ .min(1)
321
+ .default("AXTARY_GITHUB_APP_PRIVATE_KEY"),
322
+ });
323
+ export const DEFAULT_GITHUB_NATIVE_CONNECTOR_CONFIG = {
324
+ mode: "fake",
325
+ tokenEnv: "GITHUB_TOKEN",
326
+ apiBaseUrl: "https://api.github.com",
327
+ userAgent: "axtary-local-proxy",
328
+ appIdEnv: "AXTARY_GITHUB_APP_ID",
329
+ installationIdEnv: "AXTARY_GITHUB_INSTALLATION_ID",
330
+ privateKeyEnv: "AXTARY_GITHUB_APP_PRIVATE_KEY",
331
+ };
332
+ export const LinearNativeConnectorConfigSchema = z.object({
333
+ mode: z.enum(["fake", "graphql"]).default("fake"),
334
+ tokenEnv: z.string().min(1).default("LINEAR_API_KEY"),
335
+ apiUrl: z.string().url().default("https://api.linear.app/graphql"),
336
+ });
337
+ export const DEFAULT_LINEAR_NATIVE_CONNECTOR_CONFIG = {
338
+ mode: "fake",
339
+ tokenEnv: "LINEAR_API_KEY",
340
+ apiUrl: "https://api.linear.app/graphql",
341
+ };
342
+ export const JiraNativeConnectorConfigSchema = z.object({
343
+ mode: z.enum(["fake", "rest"]).default("fake"),
344
+ auth: z.enum(["api_token", "oauth"]).default("api_token"),
345
+ tokenEnv: z.string().min(1).default("JIRA_API_TOKEN"),
346
+ emailEnv: z.string().min(1).default("JIRA_EMAIL"),
347
+ siteUrl: z.string().url().default("https://example.atlassian.net"),
348
+ cloudId: z.string().min(1).optional(),
349
+ cloudIdEnv: z.string().min(1).default("AXTARY_JIRA_CLOUD_ID"),
350
+ });
351
+ export const DEFAULT_JIRA_NATIVE_CONNECTOR_CONFIG = {
352
+ mode: "fake",
353
+ auth: "api_token",
354
+ tokenEnv: "JIRA_API_TOKEN",
355
+ emailEnv: "JIRA_EMAIL",
356
+ siteUrl: "https://example.atlassian.net",
357
+ cloudIdEnv: "AXTARY_JIRA_CLOUD_ID",
358
+ };
359
+ export const PostgresNativeConnectorConfigSchema = z.object({
360
+ mode: z.enum(["off", "postgres"]).default("off"),
361
+ dsnEnv: z.string().min(1).default("AXTARY_POSTGRES_DSN"),
362
+ allowedTables: z.array(z.string().min(1)).default([]),
363
+ allowedFunctions: z.array(z.string().min(1)).default([]),
364
+ requireRls: z.boolean().default(true),
365
+ maxRows: z.number().int().positive().max(10_000).default(100),
366
+ statementTimeoutMs: z.number().int().positive().default(5_000),
367
+ lockTimeoutMs: z.number().int().positive().default(1_000),
368
+ idleTransactionTimeoutMs: z.number().int().positive().default(5_000),
369
+ });
370
+ export const DEFAULT_POSTGRES_NATIVE_CONNECTOR_CONFIG = {
371
+ mode: "off",
372
+ dsnEnv: "AXTARY_POSTGRES_DSN",
373
+ allowedTables: [],
374
+ allowedFunctions: [],
375
+ requireRls: true,
376
+ maxRows: 100,
377
+ statementTimeoutMs: 5_000,
378
+ lockTimeoutMs: 1_000,
379
+ idleTransactionTimeoutMs: 5_000,
380
+ };
381
+ export const DriveNativeConnectorConfigSchema = z.object({
382
+ mode: z.enum(["off", "rest"]).default("off"),
383
+ tokenEnv: z.string().min(1).default("AXTARY_DRIVE_ACCESS_TOKEN"),
384
+ selectedFileId: z.string().min(1).optional(),
385
+ apiBaseUrl: z
386
+ .string()
387
+ .url()
388
+ .default("https://www.googleapis.com/drive/v3"),
389
+ allowedMimeTypes: z
390
+ .array(z.string().min(1))
391
+ .default([
392
+ "application/vnd.google-apps.document",
393
+ "text/plain",
394
+ "text/markdown",
395
+ "text/html",
396
+ "text/csv",
397
+ "application/json",
398
+ "application/xml",
399
+ ]),
400
+ maxReadBytes: z.number().int().positive().max(10_000_000).default(100_000),
401
+ });
402
+ export const DEFAULT_DRIVE_NATIVE_CONNECTOR_CONFIG = {
403
+ mode: "off",
404
+ tokenEnv: "AXTARY_DRIVE_ACCESS_TOKEN",
405
+ apiBaseUrl: "https://www.googleapis.com/drive/v3",
406
+ allowedMimeTypes: [
407
+ "application/vnd.google-apps.document",
408
+ "text/plain",
409
+ "text/markdown",
410
+ "text/html",
411
+ "text/csv",
412
+ "application/json",
413
+ "application/xml",
414
+ ],
415
+ maxReadBytes: 100_000,
416
+ };
203
417
  export const LedgerExecutionOutcomeSchema = z
204
418
  .object({
205
419
  schemaVersion: z.literal(LEDGER_EXECUTION_OUTCOME_VERSION),
@@ -211,15 +425,50 @@ export const LedgerExecutionOutcomeSchema = z
211
425
  resourceLabel: z.string().min(1).optional(),
212
426
  failureReason: z.string().min(1).optional(),
213
427
  errorClass: z.string().min(1).optional(),
428
+ // Approval↔execution equivalence proof: when the executed action carried
429
+ // approval evidence, the runtime stamps the hash the approval was bound
430
+ // to and the hash of the payload it actually executed. Both fields are
431
+ // present together or not at all; export verification fails closed on an
432
+ // incomplete pair, on `executedPayloadHash` disagreeing with the record's
433
+ // own hash-chained `payloadHash`, and on `approved !== executed`.
434
+ approvedPayloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
435
+ executedPayloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
214
436
  sideEffectProof: z.record(z.string(), JsonValueSchema).default({}),
215
437
  })
216
438
  .strict();
439
+ export const DelegationLedgerEdgeSchema = z.object({
440
+ parentPassId: z.string().min(1),
441
+ childPassId: z.string().min(1),
442
+ depth: z.number().int().positive(),
443
+ rootPassId: z.string().min(1),
444
+ });
445
+ export const BudgetLedgerEventSchema = z.object({
446
+ reservationId: z.string().min(1).nullable(),
447
+ scope: z.string().min(1),
448
+ status: z.enum(["denied", "pending", "committed", "rolled_back"]),
449
+ expiresAt: z.string().datetime().nullable(),
450
+ cost: BudgetAmountSchema,
451
+ limit: BudgetAmountSchema,
452
+ committedBefore: BudgetAmountSchema,
453
+ committedAfter: BudgetAmountSchema,
454
+ reservedAfter: BudgetAmountSchema,
455
+ });
456
+ export const LedgerAuditContextSchema = z.object({
457
+ tenant: z.string().min(1).nullable(),
458
+ agentId: z.string().min(1),
459
+ humanOwner: z.string().min(1),
460
+ taskId: z.string().min(1),
461
+ tool: z.string().min(1),
462
+ resource: z.string().min(1),
463
+ });
217
464
  export const LedgerRecordSchema = z.object({
218
465
  schemaVersion: z.literal(LEDGER_SCHEMA_VERSION),
219
466
  id: z.string().min(1),
220
467
  occurredAt: z.string().datetime(),
468
+ auditContext: LedgerAuditContextSchema.optional(),
221
469
  actionHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
222
470
  payloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
471
+ provenanceHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
223
472
  decision: AxtaryDecisionSchema,
224
473
  reasons: z.array(z.string().min(1)),
225
474
  policy: PolicyDecisionSchema.shape.policy,
@@ -227,6 +476,8 @@ export const LedgerRecordSchema = z.object({
227
476
  executionOutcome: LedgerExecutionOutcomeSchema.optional(),
228
477
  traceId: z.string().min(1).optional(),
229
478
  actionPassId: z.string().min(1).nullable(),
479
+ delegation: DelegationLedgerEdgeSchema.optional(),
480
+ budget: BudgetLedgerEventSchema.optional(),
230
481
  correlationId: z.string().min(1).optional(),
231
482
  previousLedgerHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(),
232
483
  ledgerHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
@@ -358,6 +609,7 @@ export function createApprovalArtifact(input) {
358
609
  id: input.id ?? `approval_${randomUUID()}`,
359
610
  mode: input.mode,
360
611
  approvedBy: input.approvedBy,
612
+ approverRoles: input.approverRoles,
361
613
  approvedAt: (input.approvedAt ?? new Date()).toISOString(),
362
614
  actionHash: hashAction(action),
363
615
  payloadHash: hashPayload(action.capability.payload),
@@ -374,6 +626,7 @@ export function createApprovalArtifact(input) {
374
626
  approval: {
375
627
  mode: artifact.mode,
376
628
  approvedBy: artifact.approvedBy,
629
+ approverRoles: artifact.approverRoles,
377
630
  approvedAt: artifact.approvedAt,
378
631
  approvalArtifact: artifact.id,
379
632
  approvalArtifactHash: artifactHash,
@@ -383,6 +636,122 @@ export function createApprovalArtifact(input) {
383
636
  };
384
637
  }
385
638
  export async function issueActionPass(input) {
639
+ return issueActionPassProfile(input, ACTIONPASS_V0_VERSION);
640
+ }
641
+ export async function issueActionPassV1(input) {
642
+ const publicJwk = publicOnlyJwk(input.holderPublicJwk);
643
+ const jkt = await calculateJwkThumbprint(publicJwk, "sha256");
644
+ return (await issueActionPassProfile(input, ACTIONPASS_V1_VERSION, jkt));
645
+ }
646
+ export async function issueActionPassV2(input) {
647
+ const publicJwk = publicOnlyJwk(input.holderPublicJwk);
648
+ const jkt = await calculateJwkThumbprint(publicJwk, "sha256");
649
+ return (await issueActionPassProfile(input, ACTIONPASS_V2_VERSION, jkt));
650
+ }
651
+ export async function delegateActionPassV1(input) {
652
+ const delegationNow = input.now ?? input.parentChain.at(-1)?.currentDate ?? new Date();
653
+ const verifiedChain = await verifyDelegationChain({
654
+ chain: input.parentChain,
655
+ currentDate: delegationNow,
656
+ revocation: input.revocation,
657
+ });
658
+ if (!verifiedChain.valid) {
659
+ throw new Error(verifiedChain.reason);
660
+ }
661
+ const parentClaims = verifiedChain.claims.at(-1);
662
+ const parentAction = verifiedChain.actions.at(-1);
663
+ if (!parentClaims || !parentAction) {
664
+ throw new Error("delegation_parent_required");
665
+ }
666
+ const childAction = parseNormalizedAction(input.childAction);
667
+ if (input.tenant !== parentClaims.tenant) {
668
+ throw new Error("delegation_scope_broadened");
669
+ }
670
+ const attenuationError = validateDelegationAttenuation(parentAction, childAction);
671
+ if (attenuationError) {
672
+ throw new Error(attenuationError);
673
+ }
674
+ const depth = parentClaims.delegation
675
+ ? parentClaims.delegation.depth + 1
676
+ : 1;
677
+ const rootPassId = parentClaims.delegation?.rootPassId ?? parentClaims.jti;
678
+ const now = delegationNow;
679
+ const requestedTtl = input.expiresInSeconds ?? input.childDecision.constraints.expiresInSeconds;
680
+ const parentRemainingTtl = parentClaims.exp - toNumericDate(now);
681
+ if (parentRemainingTtl < 1) {
682
+ throw new Error("delegation_parent_expired");
683
+ }
684
+ const issued = await issueActionPassV1({
685
+ ...input,
686
+ action: childAction,
687
+ decision: input.childDecision,
688
+ holderPublicJwk: input.childHolderPublicJwk,
689
+ parentPassId: parentClaims.jti,
690
+ delegation: {
691
+ depth,
692
+ rootPassId,
693
+ },
694
+ expiresInSeconds: Math.min(requestedTtl, parentRemainingTtl),
695
+ });
696
+ const delegation = DelegationLedgerEdgeSchema.parse({
697
+ parentPassId: parentClaims.jti,
698
+ childPassId: issued.claims.jti,
699
+ depth,
700
+ rootPassId,
701
+ });
702
+ return {
703
+ ...issued,
704
+ delegation,
705
+ };
706
+ }
707
+ export async function delegateActionPassV2(input) {
708
+ const delegationNow = input.now ?? input.parentChain.at(-1)?.currentDate ?? new Date();
709
+ const verifiedChain = await verifyDelegationChain({
710
+ chain: input.parentChain,
711
+ currentDate: delegationNow,
712
+ revocation: input.revocation,
713
+ });
714
+ if (!verifiedChain.valid)
715
+ throw new Error(verifiedChain.reason);
716
+ if (verifiedChain.claims.some((claims) => claims.apv !== ACTIONPASS_V2_VERSION)) {
717
+ throw new Error("delegation_requires_actionpass_v2");
718
+ }
719
+ const parentClaims = verifiedChain.claims.at(-1);
720
+ const parentAction = verifiedChain.actions.at(-1);
721
+ const childAction = parseNormalizedAction(input.childAction);
722
+ if (input.tenant !== parentClaims.tenant) {
723
+ throw new Error("delegation_scope_broadened");
724
+ }
725
+ const attenuationError = validateDelegationAttenuation(parentAction, childAction);
726
+ if (attenuationError)
727
+ throw new Error(attenuationError);
728
+ const depth = parentClaims.delegation
729
+ ? parentClaims.delegation.depth + 1
730
+ : 1;
731
+ const rootPassId = parentClaims.delegation?.rootPassId ?? parentClaims.jti;
732
+ const parentRemainingTtl = parentClaims.exp - toNumericDate(delegationNow);
733
+ if (parentRemainingTtl < 1)
734
+ throw new Error("delegation_parent_expired");
735
+ const requestedTtl = input.expiresInSeconds ?? input.childDecision.constraints.expiresInSeconds;
736
+ const issued = await issueActionPassV2({
737
+ ...input,
738
+ action: childAction,
739
+ decision: input.childDecision,
740
+ holderPublicJwk: input.childHolderPublicJwk,
741
+ parentPassId: parentClaims.jti,
742
+ delegation: { depth, rootPassId },
743
+ status: input.childStatus,
744
+ expiresInSeconds: Math.min(requestedTtl, parentRemainingTtl),
745
+ });
746
+ const delegation = DelegationLedgerEdgeSchema.parse({
747
+ parentPassId: parentClaims.jti,
748
+ childPassId: issued.claims.jti,
749
+ depth,
750
+ rootPassId,
751
+ });
752
+ return { ...issued, delegation };
753
+ }
754
+ async function issueActionPassProfile(input, version, holderJkt) {
386
755
  const action = parseNormalizedAction(input.action);
387
756
  const decision = PolicyDecisionSchema.parse(input.decision);
388
757
  if (decision.decision !== "allow") {
@@ -392,7 +761,7 @@ export async function issueActionPass(input) {
392
761
  const issuedAt = toNumericDate(now);
393
762
  const expiresInSeconds = input.expiresInSeconds ?? decision.constraints.expiresInSeconds;
394
763
  const expiresAt = issuedAt + expiresInSeconds;
395
- const passId = `ap_${randomUUID()}`;
764
+ const passId = input.passId ?? `ap_${randomUUID()}`;
396
765
  const actionHash = hashAction(action);
397
766
  const payloadHash = hashPayload(action.capability.payload);
398
767
  const approval = bindApproval({
@@ -404,7 +773,7 @@ export async function issueActionPass(input) {
404
773
  now,
405
774
  });
406
775
  const claims = ActionPassClaimsSchema.parse({
407
- apv: ACTIONPASS_VERSION,
776
+ apv: version,
408
777
  iss: input.issuer,
409
778
  sub: action.actor.agentId,
410
779
  aud: action.capability.resource,
@@ -425,6 +794,19 @@ export async function issueActionPass(input) {
425
794
  reasons: decision.reasons,
426
795
  policy: decision.policy,
427
796
  payloadHash,
797
+ provenanceHash: action.provenance
798
+ ? hashProvenance(action.provenance)
799
+ : undefined,
800
+ budget: action.budget,
801
+ cnf: holderJkt ? { jkt: holderJkt } : undefined,
802
+ delegation: (version === ACTIONPASS_V1_VERSION || version === ACTIONPASS_V2_VERSION) &&
803
+ "delegation" in input &&
804
+ input.delegation
805
+ ? input.delegation
806
+ : undefined,
807
+ status: version === ACTIONPASS_V2_VERSION && "status" in input
808
+ ? input.status
809
+ : undefined,
428
810
  approval,
429
811
  audit: {
430
812
  traceId: input.traceId ?? `trace_${randomUUID()}`,
@@ -441,7 +823,197 @@ export async function issueActionPass(input) {
441
823
  .sign(input.signingKey);
442
824
  return { token, claims };
443
825
  }
826
+ export async function createDpopProof(input) {
827
+ const publicJwk = publicOnlyJwk(input.publicJwk);
828
+ const jkt = await calculateJwkThumbprint(publicJwk, "sha256");
829
+ const claims = DpopProofClaimsSchema.parse({
830
+ htm: normalizeDpopMethod(input.method),
831
+ htu: normalizeDpopUri(input.uri),
832
+ iat: toNumericDate(input.now ?? new Date()),
833
+ jti: input.proofId ?? `dpop_${randomUUID()}`,
834
+ ath: computeDpopAccessTokenHash(input.actionPass),
835
+ nonce: input.nonce,
836
+ });
837
+ const proof = await new SignJWT(claims)
838
+ .setProtectedHeader({
839
+ alg: input.algorithm ?? DEFAULT_SIGNING_ALGORITHM,
840
+ typ: DPOP_PROOF_TYP,
841
+ jwk: publicJwk,
842
+ })
843
+ .sign(input.signingKey);
844
+ return { proof, claims, jkt };
845
+ }
846
+ export class InMemoryDpopReplayStore {
847
+ entries = new Map();
848
+ consume(proofId, expiresAt, currentDate = new Date()) {
849
+ const now = currentDate.getTime();
850
+ for (const [id, expiry] of this.entries) {
851
+ if (expiry <= now) {
852
+ this.entries.delete(id);
853
+ }
854
+ }
855
+ if (this.entries.has(proofId)) {
856
+ return false;
857
+ }
858
+ this.entries.set(proofId, expiresAt.getTime());
859
+ return true;
860
+ }
861
+ }
862
+ /**
863
+ * Durable, inter-process replay store for DPoP proof `jti`s. A proof captured
864
+ * before a restart stays rejected after restart, for as long as it remains
865
+ * inside its own time window. Entries live only until their proof expires, so
866
+ * the on-disk set stays bounded to the in-flight window rather than growing
867
+ * unboundedly.
868
+ *
869
+ * This is a single-file local store, not a cross-host distributed one: it
870
+ * defends one machine's verifiers (shared across handlers/processes on that
871
+ * host). Authenticated cross-host distribution is out of scope here.
872
+ *
873
+ * Durability mirrors the local ledger: a `proper-lockfile` lease serializes the
874
+ * read-modify-write so `consume` is atomic across processes, appended lines are
875
+ * `fsync`ed before the lease releases, and the file is compacted in place once
876
+ * expired lines outnumber live ones so it cannot grow without bound.
877
+ */
878
+ export class FileDpopReplayStore {
879
+ filePath;
880
+ options;
881
+ constructor(filePath, options = {}) {
882
+ this.filePath = filePath;
883
+ this.options = options;
884
+ }
885
+ async consume(proofId, expiresAt, currentDate = new Date()) {
886
+ await mkdir(dirname(this.filePath), { recursive: true });
887
+ const release = await this.acquireLock();
888
+ try {
889
+ const now = currentDate.getTime();
890
+ const { live, deadCount } = await this.readLiveEntries(now);
891
+ if (live.has(proofId)) {
892
+ return false;
893
+ }
894
+ live.set(proofId, expiresAt.getTime());
895
+ // Compact when expired tombstones dominate, otherwise append O(1). This
896
+ // keeps the file proportional to the in-flight proof window.
897
+ if (deadCount > live.size) {
898
+ await this.compact(live);
899
+ }
900
+ else {
901
+ await this.append({ i: proofId, e: expiresAt.getTime() });
902
+ }
903
+ return true;
904
+ }
905
+ finally {
906
+ await release();
907
+ }
908
+ }
909
+ async acquireLock() {
910
+ const lockOptions = this.options.lock ?? {};
911
+ let compromisedReason = null;
912
+ const release = await lock(this.filePath, {
913
+ realpath: false,
914
+ lockfilePath: `${this.filePath}.lock`,
915
+ stale: lockOptions.staleMs ?? 10_000,
916
+ update: lockOptions.updateMs ?? 5_000,
917
+ retries: {
918
+ retries: lockOptions.retries ?? 100,
919
+ factor: 1.2,
920
+ minTimeout: lockOptions.minTimeoutMs ?? 10,
921
+ maxTimeout: lockOptions.maxTimeoutMs ?? 100,
922
+ },
923
+ onCompromised: (error) => {
924
+ compromisedReason = error.message;
925
+ },
926
+ });
927
+ if (compromisedReason !== null) {
928
+ await release();
929
+ throw new Error(`dpop_replay_store_lock_compromised:${compromisedReason}`);
930
+ }
931
+ return release;
932
+ }
933
+ async readLiveEntries(now) {
934
+ const live = new Map();
935
+ let deadCount = 0;
936
+ let text;
937
+ try {
938
+ text = await readFile(this.filePath, "utf8");
939
+ }
940
+ catch (error) {
941
+ if (isNodeError(error) && error.code === "ENOENT") {
942
+ return { live, deadCount };
943
+ }
944
+ throw error;
945
+ }
946
+ for (const line of text.split("\n")) {
947
+ const trimmed = line.trim();
948
+ if (!trimmed)
949
+ continue;
950
+ let entry;
951
+ try {
952
+ entry = JSON.parse(trimmed);
953
+ }
954
+ catch {
955
+ // A torn final line from a crash mid-append is ignored; the lease
956
+ // holder rewrites a clean file on the next compaction.
957
+ deadCount += 1;
958
+ continue;
959
+ }
960
+ if (typeof entry?.i !== "string" ||
961
+ typeof entry?.e !== "number" ||
962
+ entry.e <= now) {
963
+ deadCount += 1;
964
+ continue;
965
+ }
966
+ // Last write wins for a repeated id; the older line becomes a tombstone.
967
+ if (live.has(entry.i)) {
968
+ deadCount += 1;
969
+ }
970
+ live.set(entry.i, entry.e);
971
+ }
972
+ return { live, deadCount };
973
+ }
974
+ async append(entry) {
975
+ const handle = await open(this.filePath, "a", 0o600);
976
+ try {
977
+ await handle.chmod(0o600);
978
+ await handle.writeFile(`${JSON.stringify(entry)}\n`, "utf8");
979
+ await handle.sync();
980
+ }
981
+ finally {
982
+ await handle.close();
983
+ }
984
+ }
985
+ async compact(live) {
986
+ const lines = [...live.entries()]
987
+ .map(([i, e]) => `${JSON.stringify({ i, e })}\n`)
988
+ .join("");
989
+ const tmpPath = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
990
+ let renamed = false;
991
+ try {
992
+ const handle = await open(tmpPath, "wx", 0o600);
993
+ try {
994
+ await handle.writeFile(lines, "utf8");
995
+ await handle.sync();
996
+ }
997
+ finally {
998
+ await handle.close();
999
+ }
1000
+ await rename(tmpPath, this.filePath);
1001
+ renamed = true;
1002
+ }
1003
+ finally {
1004
+ if (!renamed) {
1005
+ await rm(tmpPath, { force: true });
1006
+ }
1007
+ }
1008
+ }
1009
+ }
444
1010
  export async function verifyActionPass(input) {
1011
+ return verifyActionPassInternal(input, true);
1012
+ }
1013
+ export async function verifyActionPassArtifact(input) {
1014
+ return verifyActionPassInternal(input, false);
1015
+ }
1016
+ async function verifyActionPassInternal(input, requireDpop) {
445
1017
  try {
446
1018
  const action = parseNormalizedAction(input.action);
447
1019
  const verificationKey = await resolveVerificationKey(input);
@@ -461,6 +1033,14 @@ export async function verifyActionPass(input) {
461
1033
  reason: bindingError,
462
1034
  };
463
1035
  }
1036
+ if ((claims.apv === ACTIONPASS_V1_VERSION ||
1037
+ claims.apv === ACTIONPASS_V2_VERSION) &&
1038
+ requireDpop) {
1039
+ const dpopError = await validateDpopProof(input, claims);
1040
+ if (dpopError) {
1041
+ return { valid: false, reason: dpopError };
1042
+ }
1043
+ }
464
1044
  const revocationError = await validateRevocation(input, claims);
465
1045
  if (revocationError) {
466
1046
  return {
@@ -468,6 +1048,12 @@ export async function verifyActionPass(input) {
468
1048
  reason: revocationError,
469
1049
  };
470
1050
  }
1051
+ if (claims.apv === ACTIONPASS_V2_VERSION) {
1052
+ const statusError = await validateDistributedStatus(input, claims);
1053
+ if (statusError) {
1054
+ return { valid: false, reason: statusError };
1055
+ }
1056
+ }
471
1057
  return {
472
1058
  valid: true,
473
1059
  claims,
@@ -481,6 +1067,61 @@ export async function verifyActionPass(input) {
481
1067
  };
482
1068
  }
483
1069
  }
1070
+ export async function verifyDelegationChain(input) {
1071
+ if (input.chain.length === 0) {
1072
+ return { valid: false, reason: "delegation_parent_required" };
1073
+ }
1074
+ const claims = [];
1075
+ const actions = [];
1076
+ for (let index = 0; index < input.chain.length; index += 1) {
1077
+ const entry = input.chain[index];
1078
+ const revocation = input.revocation;
1079
+ const verified = await verifyActionPassInternal({
1080
+ ...entry,
1081
+ currentDate: input.currentDate ??
1082
+ input.chain.at(-1)?.currentDate ??
1083
+ entry.currentDate,
1084
+ revokedPassIds: revocation?.revokedPassIds ?? entry.revokedPassIds,
1085
+ revocations: revocation?.revocations ?? entry.revocations,
1086
+ isRevoked: revocation?.isRevoked ?? entry.isRevoked,
1087
+ getStatus: revocation?.getStatus ?? entry.getStatus,
1088
+ }, index === input.chain.length - 1);
1089
+ if (!verified.valid) {
1090
+ return verified;
1091
+ }
1092
+ if (verified.claims.apv !== ACTIONPASS_V1_VERSION &&
1093
+ verified.claims.apv !== ACTIONPASS_V2_VERSION) {
1094
+ return { valid: false, reason: "delegation_requires_actionpass_v1" };
1095
+ }
1096
+ const action = parseNormalizedAction(entry.action);
1097
+ const currentClaims = verified.claims;
1098
+ if (index === 0) {
1099
+ if (currentClaims.audit.parentPassId !== null ||
1100
+ currentClaims.delegation !== undefined) {
1101
+ return { valid: false, reason: "delegation_chain_root_invalid" };
1102
+ }
1103
+ }
1104
+ else {
1105
+ const parentClaims = claims[index - 1];
1106
+ const parentAction = actions[index - 1];
1107
+ const expectedDepth = index;
1108
+ const expectedRootPassId = claims[0].jti;
1109
+ if (currentClaims.apv !== parentClaims.apv ||
1110
+ currentClaims.audit.parentPassId !== parentClaims.jti ||
1111
+ currentClaims.delegation?.depth !== expectedDepth ||
1112
+ currentClaims.delegation.rootPassId !== expectedRootPassId) {
1113
+ return { valid: false, reason: "delegation_chain_link_invalid" };
1114
+ }
1115
+ const attenuationError = validateDelegationAttenuation(parentAction, action);
1116
+ if (attenuationError) {
1117
+ return { valid: false, reason: attenuationError };
1118
+ }
1119
+ }
1120
+ claims.push(currentClaims);
1121
+ actions.push(action);
1122
+ }
1123
+ return { valid: true, claims, actions };
1124
+ }
484
1125
  export function revokeActionPass(input) {
485
1126
  return ActionPassRevocationSchema.parse({
486
1127
  schemaVersion: ACTIONPASS_REVOCATION_VERSION,
@@ -571,8 +1212,19 @@ export function recordDecision(input) {
571
1212
  schemaVersion: LEDGER_SCHEMA_VERSION,
572
1213
  id: `ledger_${randomUUID()}`,
573
1214
  occurredAt: (input.occurredAt ?? new Date()).toISOString(),
1215
+ auditContext: {
1216
+ tenant: action.actor.tenant ?? null,
1217
+ agentId: action.actor.agentId,
1218
+ humanOwner: action.actor.humanOwner,
1219
+ taskId: action.intent.taskId,
1220
+ tool: action.capability.tool,
1221
+ resource: action.capability.resource,
1222
+ },
574
1223
  actionHash,
575
1224
  payloadHash,
1225
+ provenanceHash: action.provenance
1226
+ ? hashProvenance(action.provenance)
1227
+ : undefined,
576
1228
  decision: decision.decision,
577
1229
  reasons: decision.reasons,
578
1230
  policy: decision.policy,
@@ -580,6 +1232,12 @@ export function recordDecision(input) {
580
1232
  executionOutcome: input.executionOutcome,
581
1233
  traceId,
582
1234
  actionPassId: input.actionPassId ?? null,
1235
+ delegation: input.delegation
1236
+ ? DelegationLedgerEdgeSchema.parse(input.delegation)
1237
+ : undefined,
1238
+ budget: input.budget
1239
+ ? BudgetLedgerEventSchema.parse(input.budget)
1240
+ : undefined,
583
1241
  correlationId: input.correlationId ?? undefined,
584
1242
  previousLedgerHash: input.previousLedgerHash ?? null,
585
1243
  };
@@ -588,55 +1246,558 @@ export function recordDecision(input) {
588
1246
  ledgerHash: hashJson(recordWithoutHash),
589
1247
  });
590
1248
  }
1249
+ /**
1250
+ * Canonical reason token marking a ledger record as the tamper-evident
1251
+ * revocation event for its pass. Matches the forensics `cascade_containment`
1252
+ * convention (spec §9.5) and the trust-store revocation authority (spec §7).
1253
+ */
1254
+ export const REVOCATION_LEDGER_REASON = "actionpass_revoked";
1255
+ /**
1256
+ * Build a tamper-evident revocation event for a previously recorded pass. The
1257
+ * event is a `deny` record naming the pass with `actionpass_revoked`, reusing
1258
+ * the revoked record's action/payload hashes, policy, and audit context so the
1259
+ * revocation is bound to the exact action it cancels. It carries no delegation
1260
+ * edge or budget event (those describe the original action, not the
1261
+ * revocation). The durable enforcement authority remains the trust store (§7);
1262
+ * this record is the auditable logbook entry, not the enforcement source.
1263
+ */
1264
+ export function recordActionPassRevocationEvent(input) {
1265
+ const source = LedgerRecordSchema.parse(input.revokedRecord);
1266
+ if (!source.actionPassId) {
1267
+ throw new Error("revocation_requires_action_pass_id");
1268
+ }
1269
+ const reasons = [REVOCATION_LEDGER_REASON];
1270
+ if (input.revokedBy)
1271
+ reasons.push(`revoked_by:${input.revokedBy}`);
1272
+ if (input.reason)
1273
+ reasons.push(`revocation_reason:${input.reason}`);
1274
+ const recordWithoutHash = {
1275
+ schemaVersion: LEDGER_SCHEMA_VERSION,
1276
+ id: `ledger_${randomUUID()}`,
1277
+ occurredAt: (input.occurredAt ?? new Date()).toISOString(),
1278
+ auditContext: source.auditContext,
1279
+ actionHash: source.actionHash,
1280
+ payloadHash: source.payloadHash,
1281
+ provenanceHash: source.provenanceHash,
1282
+ decision: "deny",
1283
+ reasons,
1284
+ policy: source.policy,
1285
+ traceId: `trace_${randomUUID()}`,
1286
+ actionPassId: source.actionPassId,
1287
+ correlationId: source.actionPassId,
1288
+ previousLedgerHash: input.previousLedgerHash ?? null,
1289
+ };
1290
+ return LedgerRecordSchema.parse({
1291
+ ...recordWithoutHash,
1292
+ ledgerHash: hashJson(recordWithoutHash),
1293
+ });
1294
+ }
591
1295
  export function hashPayload(payload) {
592
1296
  return hashJson(JsonValueSchema.parse(payload));
593
1297
  }
1298
+ export function hashProvenance(provenance) {
1299
+ return hashJson(ActionProvenanceSchema.parse(provenance));
1300
+ }
1301
+ function validateDelegationAttenuation(parent, child) {
1302
+ const parentRemaining = parent.intent.maxDelegationDepth ?? 0;
1303
+ const childRemaining = child.intent.maxDelegationDepth ?? 0;
1304
+ if (parentRemaining < 1 || childRemaining > parentRemaining - 1) {
1305
+ return "delegation_depth_exceeded";
1306
+ }
1307
+ if (child.actor.humanOwner !== parent.actor.humanOwner ||
1308
+ child.actor.tenant !== parent.actor.tenant ||
1309
+ child.intent.taskId !== parent.intent.taskId ||
1310
+ child.capability.tool !== parent.capability.tool ||
1311
+ child.capability.resource !== parent.capability.resource ||
1312
+ stableStringify(child.toolDefinition) !==
1313
+ stableStringify(parent.toolDefinition) ||
1314
+ !isJsonScopeSubset(child.capability.payload, parent.capability.payload, []) ||
1315
+ !areConstraintsAttenuated(parent.capability.constraints, child.capability.constraints)) {
1316
+ return "delegation_scope_broadened";
1317
+ }
1318
+ return null;
1319
+ }
1320
+ function isJsonScopeSubset(child, parent, path) {
1321
+ if (stableStringify(child) === stableStringify(parent)) {
1322
+ return true;
1323
+ }
1324
+ if (typeof child === "string" && typeof parent === "string") {
1325
+ return isPathLikeKey(path.at(-1)) && isPathInsideScope(child, parent);
1326
+ }
1327
+ if (Array.isArray(child) && Array.isArray(parent)) {
1328
+ return child.every((childEntry) => parent.some((parentEntry) => isJsonScopeSubset(childEntry, parentEntry, path)));
1329
+ }
1330
+ if (isJsonObject(child) && isJsonObject(parent)) {
1331
+ return Object.entries(child).every(([key, value]) => key in parent &&
1332
+ isJsonScopeSubset(value, parent[key], [...path, key]));
1333
+ }
1334
+ return false;
1335
+ }
1336
+ function areConstraintsAttenuated(parent, child) {
1337
+ if (!parent) {
1338
+ return true;
1339
+ }
1340
+ if (!child) {
1341
+ return false;
1342
+ }
1343
+ return Object.entries(parent).every(([key, value]) => key in child &&
1344
+ stableStringify(child[key]) === stableStringify(value));
1345
+ }
1346
+ function isPathLikeKey(key) {
1347
+ return Boolean(key && /(path|file|directory|prefix)/i.test(key));
1348
+ }
1349
+ function isPathInsideScope(child, parent) {
1350
+ if (parent.endsWith("/**")) {
1351
+ const prefix = parent.slice(0, -2);
1352
+ return child.startsWith(prefix);
1353
+ }
1354
+ if (parent.endsWith("/")) {
1355
+ return child.startsWith(parent);
1356
+ }
1357
+ return false;
1358
+ }
594
1359
  export function hashAction(action) {
595
1360
  return hashJson(parseNormalizedAction(action));
596
1361
  }
1362
+ export const GITHUB_NATIVE_CONNECTOR_GOVERNANCE = {
1363
+ provider: "github",
1364
+ connector: "github-rest",
1365
+ mode: "rest",
1366
+ configSchema: GitHubNativeConnectorConfigSchema,
1367
+ defaultConfig: DEFAULT_GITHUB_NATIVE_CONNECTOR_CONFIG,
1368
+ requiredScopes: [
1369
+ "contents:write",
1370
+ "metadata:read",
1371
+ "pull_requests:write",
1372
+ "checks:write",
1373
+ "issues:write",
1374
+ ],
1375
+ smokeCheck: "GET /user + X-Accepted-GitHub-Permissions",
1376
+ tools: [
1377
+ {
1378
+ capability: {
1379
+ id: "github-pr-create",
1380
+ provider: "github",
1381
+ connector: "github-rest",
1382
+ label: "Create pull request",
1383
+ tool: "github.pull_requests.create",
1384
+ operation: "write",
1385
+ status: "supported",
1386
+ supportedModes: ["fake", "rest", "app"],
1387
+ requiresActionPass: true,
1388
+ defaultPolicy: "step_up",
1389
+ payloadFields: [
1390
+ "title",
1391
+ "baseBranch",
1392
+ "filesChanged",
1393
+ "testsPassed",
1394
+ "touchesProduction",
1395
+ ],
1396
+ approvalTriggers: [
1397
+ "protected path",
1398
+ "production impact",
1399
+ "base branch mismatch",
1400
+ "missing tests",
1401
+ "large file set",
1402
+ ],
1403
+ credentialBoundary: "GitHub token stays in the proxy/adapter; the agent receives an ActionPass-bound result.",
1404
+ smokeCheck: "GET /user",
1405
+ },
1406
+ normalizeEvidence: (action, payload) => githubPullRequestEvidence(action, payload),
1407
+ },
1408
+ {
1409
+ capability: {
1410
+ id: "github-content-read",
1411
+ provider: "github",
1412
+ connector: "github-rest",
1413
+ label: "Read repository content",
1414
+ tool: "github.contents.read",
1415
+ operation: "read",
1416
+ status: "supported",
1417
+ supportedModes: ["fake", "rest", "app"],
1418
+ requiresActionPass: true,
1419
+ defaultPolicy: "deny_until_scoped",
1420
+ payloadFields: ["path", "ref"],
1421
+ approvalTriggers: ["denied secret or production path"],
1422
+ credentialBoundary: "Repository content is fetched by the adapter after policy checks deny protected paths.",
1423
+ smokeCheck: "GET /user",
1424
+ },
1425
+ normalizeEvidence: (action, payload) => githubContentEvidence(action, payload, "content_read"),
1426
+ },
1427
+ {
1428
+ capability: {
1429
+ id: "github-branch-create",
1430
+ provider: "github",
1431
+ connector: "github-rest",
1432
+ label: "Create branch",
1433
+ tool: "github.branches.create",
1434
+ operation: "write",
1435
+ status: "supported",
1436
+ supportedModes: ["fake", "rest", "app"],
1437
+ requiresActionPass: true,
1438
+ defaultPolicy: "allow",
1439
+ payloadFields: ["branch", "baseBranch", "sha", "allowExisting"],
1440
+ approvalTriggers: ["base branch mismatch"],
1441
+ credentialBoundary: "Branch creation runs through the proxy and is bound to the same ActionPass verification path.",
1442
+ smokeCheck: "GET /user",
1443
+ },
1444
+ normalizeEvidence: (action, payload) => githubBranchEvidence(action, payload),
1445
+ },
1446
+ {
1447
+ capability: {
1448
+ id: "github-content-write",
1449
+ provider: "github",
1450
+ connector: "github-rest",
1451
+ label: "Write repository content",
1452
+ tool: "github.contents.write",
1453
+ operation: "write",
1454
+ status: "supported",
1455
+ supportedModes: ["fake", "rest", "app"],
1456
+ requiresActionPass: true,
1457
+ defaultPolicy: "step_up",
1458
+ payloadFields: ["path", "branch", "message", "sha", "content"],
1459
+ approvalTriggers: ["protected path"],
1460
+ credentialBoundary: "File content is never written until policy and ActionPass verification both pass.",
1461
+ smokeCheck: "GET /user",
1462
+ },
1463
+ normalizeEvidence: (action, payload) => githubContentEvidence(action, payload, "content_write"),
1464
+ },
1465
+ {
1466
+ capability: {
1467
+ id: "github-review-comment-create",
1468
+ provider: "github",
1469
+ connector: "github-rest",
1470
+ label: "Create inline review comment",
1471
+ tool: "github.pull_request_review_comments.create",
1472
+ operation: "write",
1473
+ status: "supported",
1474
+ supportedModes: ["app"],
1475
+ requiresActionPass: true,
1476
+ defaultPolicy: "step_up",
1477
+ payloadFields: [
1478
+ "pullNumber",
1479
+ "body",
1480
+ "commitId",
1481
+ "path",
1482
+ "line",
1483
+ "side",
1484
+ ],
1485
+ approvalTriggers: ["protected path review"],
1486
+ credentialBoundary: "The exact inline comment target and body are authorized before GitHub receives the write.",
1487
+ smokeCheck: "GET /user + Pull requests: write",
1488
+ },
1489
+ normalizeEvidence: (action, payload) => githubReviewCommentEvidence(action, payload),
1490
+ },
1491
+ {
1492
+ capability: {
1493
+ id: "github-check-run-create",
1494
+ provider: "github",
1495
+ connector: "github-rest",
1496
+ label: "Create check run",
1497
+ tool: "github.check_runs.create",
1498
+ operation: "write",
1499
+ status: "supported",
1500
+ supportedModes: ["rest", "app"],
1501
+ requiresActionPass: true,
1502
+ defaultPolicy: "allow",
1503
+ payloadFields: [
1504
+ "name",
1505
+ "headSha",
1506
+ "status",
1507
+ "conclusion",
1508
+ "output",
1509
+ ],
1510
+ approvalTriggers: [],
1511
+ credentialBoundary: "Check name, commit SHA, status, conclusion, and output are payload-bound before execution.",
1512
+ smokeCheck: "GET /user + Checks: write",
1513
+ },
1514
+ normalizeEvidence: (action, payload) => githubCheckRunEvidence(action, payload),
1515
+ },
1516
+ {
1517
+ capability: {
1518
+ id: "github-issue-comment-create",
1519
+ provider: "github",
1520
+ connector: "github-rest",
1521
+ label: "Create issue comment",
1522
+ tool: "github.issue_comments.create",
1523
+ operation: "external_message",
1524
+ status: "supported",
1525
+ supportedModes: ["rest", "app"],
1526
+ requiresActionPass: true,
1527
+ defaultPolicy: "allow",
1528
+ payloadFields: ["issueNumber", "body"],
1529
+ approvalTriggers: [],
1530
+ credentialBoundary: "The exact issue number and outbound comment body are bound into the ActionPass.",
1531
+ smokeCheck: "GET /user + Issues: write",
1532
+ },
1533
+ normalizeEvidence: (action, payload) => githubIssueCommentEvidence(action, payload),
1534
+ },
1535
+ ],
1536
+ };
1537
+ export const LINEAR_NATIVE_CONNECTOR_GOVERNANCE = {
1538
+ provider: "linear",
1539
+ connector: "linear-graphql",
1540
+ mode: "graphql",
1541
+ configSchema: LinearNativeConnectorConfigSchema,
1542
+ defaultConfig: DEFAULT_LINEAR_NATIVE_CONNECTOR_CONFIG,
1543
+ requiredScopes: ["read", "write"],
1544
+ smokeCheck: "viewer",
1545
+ tools: [
1546
+ {
1547
+ capability: {
1548
+ id: "linear-issue-read",
1549
+ provider: "linear",
1550
+ connector: "linear-graphql",
1551
+ label: "Read issue",
1552
+ tool: "linear.issues.read",
1553
+ operation: "read",
1554
+ status: "supported",
1555
+ supportedModes: ["fake", "graphql"],
1556
+ requiresActionPass: true,
1557
+ defaultPolicy: "allow",
1558
+ payloadFields: ["issueKey", "projectKey"],
1559
+ approvalTriggers: [],
1560
+ credentialBoundary: "Linear API key stays adapter-side; policy pins issue access by project key.",
1561
+ smokeCheck: "viewer",
1562
+ },
1563
+ normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "linear", "issue_read"),
1564
+ },
1565
+ {
1566
+ capability: {
1567
+ id: "linear-comment-create",
1568
+ provider: "linear",
1569
+ connector: "linear-graphql",
1570
+ label: "Create issue comment",
1571
+ tool: "linear.comments.create",
1572
+ operation: "write",
1573
+ status: "supported",
1574
+ supportedModes: ["fake", "graphql"],
1575
+ requiresActionPass: true,
1576
+ defaultPolicy: "allow",
1577
+ payloadFields: ["issueKey", "projectKey", "body"],
1578
+ approvalTriggers: [],
1579
+ credentialBoundary: "Linear comments execute only through proxy policy and ActionPass verification.",
1580
+ smokeCheck: "viewer",
1581
+ },
1582
+ normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "linear", "comment_create"),
1583
+ },
1584
+ {
1585
+ capability: {
1586
+ id: "linear-issue-update",
1587
+ provider: "linear",
1588
+ connector: "linear-graphql",
1589
+ label: "Update issue",
1590
+ tool: "linear.issues.update",
1591
+ operation: "write",
1592
+ status: "supported",
1593
+ supportedModes: ["fake", "graphql"],
1594
+ requiresActionPass: true,
1595
+ defaultPolicy: "step_up",
1596
+ payloadFields: [
1597
+ "issueKey",
1598
+ "projectKey",
1599
+ "status",
1600
+ "title",
1601
+ "description",
1602
+ "priority",
1603
+ ],
1604
+ approvalTriggers: ["protected status"],
1605
+ credentialBoundary: "Issue mutations stay behind adapter credentials and project/status policy checks.",
1606
+ smokeCheck: "viewer",
1607
+ },
1608
+ normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "linear", "issue_update"),
1609
+ },
1610
+ ],
1611
+ };
1612
+ export const JIRA_NATIVE_CONNECTOR_GOVERNANCE = {
1613
+ provider: "jira",
1614
+ connector: "jira-rest",
1615
+ mode: "rest",
1616
+ configSchema: JiraNativeConnectorConfigSchema,
1617
+ defaultConfig: DEFAULT_JIRA_NATIVE_CONNECTOR_CONFIG,
1618
+ requiredScopes: ["read:jira-work", "write:jira-work"],
1619
+ smokeCheck: "GET /myself",
1620
+ tools: [
1621
+ {
1622
+ capability: {
1623
+ id: "jira-issue-read",
1624
+ provider: "jira",
1625
+ connector: "jira-rest",
1626
+ label: "Read issue",
1627
+ tool: "jira.issues.read",
1628
+ operation: "read",
1629
+ status: "supported",
1630
+ supportedModes: ["fake", "rest"],
1631
+ requiresActionPass: true,
1632
+ defaultPolicy: "allow",
1633
+ payloadFields: ["issueKey", "projectKey"],
1634
+ approvalTriggers: [],
1635
+ credentialBoundary: "Jira credentials stay adapter-side; policy pins issue access by project key.",
1636
+ smokeCheck: "GET /myself",
1637
+ },
1638
+ normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "jira", "issue_read"),
1639
+ },
1640
+ {
1641
+ capability: {
1642
+ id: "jira-comment-create",
1643
+ provider: "jira",
1644
+ connector: "jira-rest",
1645
+ label: "Create issue comment",
1646
+ tool: "jira.comments.create",
1647
+ operation: "write",
1648
+ status: "supported",
1649
+ supportedModes: ["fake", "rest"],
1650
+ requiresActionPass: true,
1651
+ defaultPolicy: "allow",
1652
+ payloadFields: ["issueKey", "projectKey", "body"],
1653
+ approvalTriggers: [],
1654
+ credentialBoundary: "Jira comments are converted to Atlassian Document Format only after policy and ActionPass verification.",
1655
+ smokeCheck: "GET /myself",
1656
+ },
1657
+ normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "jira", "comment_create"),
1658
+ },
1659
+ {
1660
+ capability: {
1661
+ id: "jira-issue-update",
1662
+ provider: "jira",
1663
+ connector: "jira-rest",
1664
+ label: "Update issue",
1665
+ tool: "jira.issues.update",
1666
+ operation: "write",
1667
+ status: "supported",
1668
+ supportedModes: ["fake", "rest"],
1669
+ requiresActionPass: true,
1670
+ defaultPolicy: "step_up",
1671
+ payloadFields: [
1672
+ "issueKey",
1673
+ "projectKey",
1674
+ "status",
1675
+ "title",
1676
+ "description",
1677
+ "priorityId",
1678
+ ],
1679
+ approvalTriggers: ["protected status"],
1680
+ credentialBoundary: "Jira field edits and workflow transitions execute only after exact-payload authorization.",
1681
+ smokeCheck: "GET /myself",
1682
+ },
1683
+ normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "jira", "issue_update"),
1684
+ },
1685
+ ],
1686
+ };
1687
+ export const POSTGRES_NATIVE_CONNECTOR_GOVERNANCE = {
1688
+ provider: "postgres",
1689
+ connector: "postgres-native",
1690
+ mode: "postgres",
1691
+ configSchema: PostgresNativeConnectorConfigSchema,
1692
+ defaultConfig: DEFAULT_POSTGRES_NATIVE_CONNECTOR_CONFIG,
1693
+ requiredScopes: [
1694
+ "LOGIN role",
1695
+ "SELECT on allowed tables",
1696
+ "NOBYPASSRLS",
1697
+ "read-only transactions",
1698
+ ],
1699
+ smokeCheck: "SELECT current_database/current_user + transaction_read_only + table RLS metadata",
1700
+ tools: [
1701
+ {
1702
+ capability: {
1703
+ id: "postgres-query-select",
1704
+ provider: "postgres",
1705
+ connector: "postgres-native",
1706
+ label: "Run scoped SELECT",
1707
+ tool: "postgres.query.select",
1708
+ operation: "read",
1709
+ status: "supported",
1710
+ supportedModes: ["postgres"],
1711
+ requiresActionPass: true,
1712
+ defaultPolicy: "deny_until_scoped",
1713
+ payloadFields: ["statement", "parameters", "maxRows"],
1714
+ approvalTriggers: ["unpredicated table scan"],
1715
+ credentialBoundary: "The DSN stays in the local adapter; only the exact parameterized SELECT and bounded result cross the authorization boundary.",
1716
+ smokeCheck: "current role/database, read-only state, and RLS metadata for configured tables",
1717
+ },
1718
+ normalizeEvidence: (action, payload) => postgresSelectEvidence(action, payload),
1719
+ },
1720
+ ],
1721
+ };
1722
+ export const DRIVE_NATIVE_CONNECTOR_GOVERNANCE = {
1723
+ provider: "drive",
1724
+ connector: "google-drive-rest",
1725
+ mode: "rest",
1726
+ configSchema: DriveNativeConnectorConfigSchema,
1727
+ defaultConfig: DEFAULT_DRIVE_NATIVE_CONNECTOR_CONFIG,
1728
+ requiredScopes: ["https://www.googleapis.com/auth/drive.file"],
1729
+ smokeCheck: "GET selected file metadata including isAppAuthorized (no content)",
1730
+ tools: [
1731
+ {
1732
+ capability: {
1733
+ id: "drive-file-read",
1734
+ provider: "drive",
1735
+ connector: "google-drive-rest",
1736
+ label: "Read selected Drive file",
1737
+ tool: "drive.files.read",
1738
+ operation: "read",
1739
+ status: "supported",
1740
+ supportedModes: ["rest"],
1741
+ requiresActionPass: true,
1742
+ defaultPolicy: "deny_until_scoped",
1743
+ payloadFields: ["fileId", "maxBytes"],
1744
+ approvalTriggers: [
1745
+ "file not policy-scoped",
1746
+ "file not selected through Google Picker",
1747
+ "byte limit exceeded",
1748
+ ],
1749
+ credentialBoundary: "The OAuth token and file content stay in the local enforcement plane; only a Picker-authorized, policy-scoped file can be returned.",
1750
+ smokeCheck: "selected-file metadata and provider isAppAuthorized state",
1751
+ },
1752
+ normalizeEvidence: (action, payload) => driveFileReadEvidence(action, payload),
1753
+ },
1754
+ ],
1755
+ };
1756
+ export const NATIVE_CONNECTOR_GOVERNANCE_REGISTRY = [
1757
+ GITHUB_NATIVE_CONNECTOR_GOVERNANCE,
1758
+ LINEAR_NATIVE_CONNECTOR_GOVERNANCE,
1759
+ JIRA_NATIVE_CONNECTOR_GOVERNANCE,
1760
+ POSTGRES_NATIVE_CONNECTOR_GOVERNANCE,
1761
+ DRIVE_NATIVE_CONNECTOR_GOVERNANCE,
1762
+ ];
1763
+ const providerEvidenceNormalizers = new Map([
1764
+ [
1765
+ "slack.chat.postMessage",
1766
+ (action, payload) => slackMessageEvidence(action, payload),
1767
+ ],
1768
+ [
1769
+ "docs.documents.search",
1770
+ (action, payload) => docsEvidence(action, payload, "document_search"),
1771
+ ],
1772
+ [
1773
+ "docs.documents.read",
1774
+ (action, payload) => docsEvidence(action, payload, "document_read"),
1775
+ ],
1776
+ ["mcp.tool.call", (action, payload) => mcpEvidence(action, payload)],
1777
+ [
1778
+ "aws.identity.get",
1779
+ (action, payload) => awsEvidence(action, payload, "identity_get"),
1780
+ ],
1781
+ [
1782
+ "aws.s3.objects.list",
1783
+ (action, payload) => awsEvidence(action, payload, "s3_objects_list"),
1784
+ ],
1785
+ [
1786
+ "gcp.projects.get",
1787
+ (action, payload) => gcpEvidence(action, payload, "project_get"),
1788
+ ],
1789
+ [
1790
+ "gcp.storage.objects.list",
1791
+ (action, payload) => gcpEvidence(action, payload, "storage_objects_list"),
1792
+ ],
1793
+ ...NATIVE_CONNECTOR_GOVERNANCE_REGISTRY.flatMap((descriptor) => descriptor.tools.map((tool) => [
1794
+ tool.capability.tool,
1795
+ tool.normalizeEvidence,
1796
+ ])),
1797
+ ]);
597
1798
  export function providerEvidenceForAction(actionInput) {
598
1799
  const action = parseNormalizedAction(actionInput);
599
- const payload = action.capability.payload;
600
- switch (action.capability.tool) {
601
- case "github.pull_requests.create":
602
- return githubPullRequestEvidence(action, payload);
603
- case "github.contents.read":
604
- return githubContentEvidence(action, payload, "content_read");
605
- case "github.contents.write":
606
- return githubContentEvidence(action, payload, "content_write");
607
- case "github.branches.create":
608
- return githubBranchEvidence(action, payload);
609
- case "slack.chat.postMessage":
610
- return slackMessageEvidence(action, payload);
611
- case "linear.issues.read":
612
- return linearIssueEvidence(action, payload, "issue_read");
613
- case "linear.comments.create":
614
- return linearIssueEvidence(action, payload, "comment_create");
615
- case "linear.issues.update":
616
- return linearIssueEvidence(action, payload, "issue_update");
617
- case "jira.issues.read":
618
- return issueTrackerEvidence(action, payload, "jira", "issue_read");
619
- case "jira.comments.create":
620
- return issueTrackerEvidence(action, payload, "jira", "comment_create");
621
- case "jira.issues.update":
622
- return issueTrackerEvidence(action, payload, "jira", "issue_update");
623
- case "docs.documents.search":
624
- return docsEvidence(action, payload, "document_search");
625
- case "docs.documents.read":
626
- return docsEvidence(action, payload, "document_read");
627
- case "mcp.tool.call":
628
- return mcpEvidence(action, payload);
629
- case "aws.identity.get":
630
- return awsEvidence(action, payload, "identity_get");
631
- case "aws.s3.objects.list":
632
- return awsEvidence(action, payload, "s3_objects_list");
633
- case "gcp.projects.get":
634
- return gcpEvidence(action, payload, "project_get");
635
- case "gcp.storage.objects.list":
636
- return gcpEvidence(action, payload, "storage_objects_list");
637
- default:
638
- return undefined;
639
- }
1800
+ return providerEvidenceNormalizers.get(action.capability.tool)?.(action, action.capability.payload);
640
1801
  }
641
1802
  export function hashJson(value) {
642
1803
  return `sha256:${createHash("sha256")
@@ -659,15 +1820,33 @@ function validatePassBinding(claims, action, payloadHash) {
659
1820
  if (claims.intent.taskId !== action.intent.taskId) {
660
1821
  return "task_id_mismatch";
661
1822
  }
1823
+ if (stableStringify(claims.intent) !== stableStringify(action.intent)) {
1824
+ return "intent_mismatch";
1825
+ }
662
1826
  if (claims.capability.tool !== action.capability.tool) {
663
1827
  return "tool_mismatch";
664
1828
  }
665
1829
  if (claims.capability.resource !== action.capability.resource) {
666
1830
  return "resource_mismatch";
667
1831
  }
1832
+ if (stableStringify(claims.capability.constraints) !==
1833
+ stableStringify(action.capability.constraints)) {
1834
+ return "constraints_mismatch";
1835
+ }
1836
+ if (stableStringify(claims.budget) !== stableStringify(action.budget)) {
1837
+ return "budget_mismatch";
1838
+ }
668
1839
  if (claims.payloadHash !== payloadHash) {
669
1840
  return "payload_hash_mismatch";
670
1841
  }
1842
+ const provenanceHash = action.provenance
1843
+ ? hashProvenance(action.provenance)
1844
+ : undefined;
1845
+ if (claims.provenanceHash !== provenanceHash) {
1846
+ return action.provenance
1847
+ ? "provenance_hash_mismatch"
1848
+ : "unexpected_provenance_hash";
1849
+ }
671
1850
  if (claims.approval?.payloadHash && claims.approval.payloadHash !== payloadHash) {
672
1851
  return "approval_payload_hash_mismatch";
673
1852
  }
@@ -676,6 +1855,100 @@ function validatePassBinding(claims, action, payloadHash) {
676
1855
  }
677
1856
  return null;
678
1857
  }
1858
+ async function validateDpopProof(input, claims) {
1859
+ if (!input.dpopProof)
1860
+ return "dpop_proof_required";
1861
+ if (!input.dpopMethod)
1862
+ return "dpop_method_required";
1863
+ if (!input.dpopUri)
1864
+ return "dpop_uri_required";
1865
+ try {
1866
+ const header = decodeProtectedHeader(input.dpopProof);
1867
+ if (header.typ !== DPOP_PROOF_TYP)
1868
+ return "dpop_type_mismatch";
1869
+ if (!header.jwk || typeof header.jwk !== "object") {
1870
+ return "dpop_public_jwk_required";
1871
+ }
1872
+ const proofJwk = publicOnlyJwk(header.jwk);
1873
+ const algorithm = header.alg;
1874
+ const algorithms = input.dpopAlgorithms ?? [DEFAULT_SIGNING_ALGORITHM];
1875
+ if (!algorithms.includes(algorithm))
1876
+ return "dpop_algorithm_not_allowed";
1877
+ const key = await importJWK(proofJwk, algorithm);
1878
+ const { payload } = await jwtVerify(input.dpopProof, key, {
1879
+ algorithms,
1880
+ currentDate: input.currentDate,
1881
+ });
1882
+ const proof = DpopProofClaimsSchema.parse(payload);
1883
+ const proofJkt = await calculateJwkThumbprint(proofJwk, "sha256");
1884
+ if (proofJkt !== claims.cnf.jkt)
1885
+ return "dpop_key_mismatch";
1886
+ if (proof.htm !== normalizeDpopMethod(input.dpopMethod)) {
1887
+ return "dpop_method_mismatch";
1888
+ }
1889
+ if (proof.htu !== normalizeDpopUri(input.dpopUri)) {
1890
+ return "dpop_uri_mismatch";
1891
+ }
1892
+ if (proof.ath !== computeDpopAccessTokenHash(input.token)) {
1893
+ return "dpop_actionpass_hash_mismatch";
1894
+ }
1895
+ if (input.dpopNonce !== undefined && proof.nonce !== input.dpopNonce) {
1896
+ return "dpop_nonce_mismatch";
1897
+ }
1898
+ const nowSeconds = toNumericDate(input.currentDate ?? new Date());
1899
+ const maxAge = input.dpopMaxAgeSeconds ?? DEFAULT_DPOP_MAX_AGE_SECONDS;
1900
+ const skew = input.dpopClockSkewSeconds ?? DEFAULT_DPOP_CLOCK_SKEW_SECONDS;
1901
+ if (proof.iat > nowSeconds + skew)
1902
+ return "dpop_proof_from_future";
1903
+ if (proof.iat < nowSeconds - maxAge - skew)
1904
+ return "dpop_proof_stale";
1905
+ if (!input.dpopReplayStore)
1906
+ return "dpop_replay_store_required";
1907
+ const accepted = await input.dpopReplayStore.consume(proof.jti, new Date((proof.iat + maxAge + skew) * 1000), input.currentDate);
1908
+ if (!accepted)
1909
+ return "dpop_replay_detected";
1910
+ return null;
1911
+ }
1912
+ catch (error) {
1913
+ return error instanceof Error && error.message.startsWith("dpop_")
1914
+ ? error.message
1915
+ : "dpop_proof_invalid";
1916
+ }
1917
+ }
1918
+ export function computeDpopAccessTokenHash(token) {
1919
+ return createHash("sha256").update(token, "ascii").digest("base64url");
1920
+ }
1921
+ export function normalizeDpopMethod(method) {
1922
+ const normalized = method.trim().toUpperCase();
1923
+ if (!normalized)
1924
+ throw new Error("dpop_method_required");
1925
+ return normalized;
1926
+ }
1927
+ export function normalizeDpopUri(uri) {
1928
+ const parsed = new URL(uri);
1929
+ parsed.search = "";
1930
+ parsed.hash = "";
1931
+ return parsed.toString();
1932
+ }
1933
+ export function actionPassDpopTarget(actionInput) {
1934
+ const action = parseNormalizedAction(actionInput);
1935
+ return {
1936
+ method: "POST",
1937
+ uri: `https://local.axtary.dev/actions/${encodeURIComponent(action.capability.tool)}`,
1938
+ };
1939
+ }
1940
+ function publicOnlyJwk(jwk) {
1941
+ if (!jwk.kty || jwk.kty === "oct") {
1942
+ throw new Error("dpop_public_jwk_invalid");
1943
+ }
1944
+ const publicJwk = { ...jwk };
1945
+ for (const member of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) {
1946
+ if (member in publicJwk) {
1947
+ throw new Error("dpop_public_jwk_invalid");
1948
+ }
1949
+ }
1950
+ return publicJwk;
1951
+ }
679
1952
  async function readTrustStore(filePath) {
680
1953
  const text = await readTrustStoreFile(filePath);
681
1954
  if (!text.trim()) {
@@ -691,6 +1964,7 @@ async function writeTrustStore(filePath, data) {
691
1964
  await writeFile(tmpPath, `${JSON.stringify(data, null, 2)}\n`, {
692
1965
  encoding: "utf8",
693
1966
  flag: "wx",
1967
+ mode: 0o600,
694
1968
  });
695
1969
  await rename(tmpPath, filePath);
696
1970
  }
@@ -752,7 +2026,13 @@ async function validateRevocation(input, claims) {
752
2026
  }
753
2027
  }
754
2028
  if (input.isRevoked) {
755
- const result = await input.isRevoked(claims);
2029
+ let result;
2030
+ try {
2031
+ result = await input.isRevoked(claims);
2032
+ }
2033
+ catch {
2034
+ return "actionpass_revocation_check_failed";
2035
+ }
756
2036
  if (result === true) {
757
2037
  return "actionpass_revoked";
758
2038
  }
@@ -762,6 +2042,27 @@ async function validateRevocation(input, claims) {
762
2042
  }
763
2043
  return null;
764
2044
  }
2045
+ async function validateDistributedStatus(input, claims) {
2046
+ if (!input.getStatus) {
2047
+ return "actionpass_status_check_required";
2048
+ }
2049
+ let status;
2050
+ try {
2051
+ status = await input.getStatus(claims);
2052
+ }
2053
+ catch {
2054
+ return "actionpass_status_check_failed";
2055
+ }
2056
+ if (status === 0)
2057
+ return null;
2058
+ if (status === 1)
2059
+ return "actionpass_revoked";
2060
+ if (status === 2)
2061
+ return "actionpass_suspended";
2062
+ if (typeof status === "string" && status.length > 0)
2063
+ return status;
2064
+ return "actionpass_status_invalid";
2065
+ }
765
2066
  function bindApproval(input) {
766
2067
  const approval = input.approval
767
2068
  ? ApprovalSchema.parse(input.approval)
@@ -803,6 +2104,7 @@ function bindApproval(input) {
803
2104
  return ApprovalSchema.parse({
804
2105
  mode: approval?.mode ?? artifact.mode,
805
2106
  approvedBy: approval?.approvedBy ?? artifact.approvedBy,
2107
+ approverRoles: approval?.approverRoles ?? artifact.approverRoles,
806
2108
  approvedAt: approval?.approvedAt ?? artifact.approvedAt,
807
2109
  approvalArtifact: approval?.approvalArtifact ?? artifact.id,
808
2110
  approvalArtifactHash: artifactHash,
@@ -813,12 +2115,16 @@ function bindApproval(input) {
813
2115
  export function validateApprovalArtifact(artifactInput, actionInput, now = new Date()) {
814
2116
  const artifact = ApprovalArtifactSchema.parse(artifactInput);
815
2117
  const action = parseNormalizedAction(actionInput);
816
- if (artifact.actionHash !== hashAction(action)) {
817
- return "approval_action_hash_mismatch";
818
- }
2118
+ // Payload before action: a swapped payload changes both hashes, and the
2119
+ // payload mismatch is the precise, demoable reason. Matches the verify-time
2120
+ // claim order and spec §6; the action-hash check still catches every
2121
+ // non-payload mutation.
819
2122
  if (artifact.payloadHash !== hashPayload(action.capability.payload)) {
820
2123
  return "approval_payload_hash_mismatch";
821
2124
  }
2125
+ if (artifact.actionHash !== hashAction(action)) {
2126
+ return "approval_action_hash_mismatch";
2127
+ }
822
2128
  if (artifact.taskId !== action.intent.taskId) {
823
2129
  return "approval_task_id_mismatch";
824
2130
  }
@@ -913,6 +2219,103 @@ function githubBranchEvidence(action, payload) {
913
2219
  }),
914
2220
  });
915
2221
  }
2222
+ function githubReviewCommentEvidence(action, payload) {
2223
+ const path = getString(payload.path) ?? "unknown";
2224
+ const pullNumber = getNumber(payload.pullNumber);
2225
+ const line = getNumber(payload.line);
2226
+ return LedgerProviderEvidenceSchema.parse({
2227
+ schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
2228
+ provider: "github",
2229
+ tool: action.capability.tool,
2230
+ operation: "pull_request_review_comment_create",
2231
+ resource: providerResource("github", "pull_request_line", `${action.capability.resource}:pull:${pullNumber ?? "unknown"}:${path}:${line ?? "unknown"}`),
2232
+ normalized: compactJsonRecord({
2233
+ pullNumber,
2234
+ commitId: getString(payload.commitId),
2235
+ path,
2236
+ line,
2237
+ side: getString(payload.side),
2238
+ startLine: getNumber(payload.startLine),
2239
+ startSide: getString(payload.startSide),
2240
+ body: redactCredentialLikeText(getString(payload.body) ?? ""),
2241
+ }),
2242
+ });
2243
+ }
2244
+ function githubCheckRunEvidence(action, payload) {
2245
+ const name = getString(payload.name) ?? "unknown";
2246
+ const headSha = getString(payload.headSha) ?? "unknown";
2247
+ const output = isJsonObject(payload.output) ? payload.output : {};
2248
+ return LedgerProviderEvidenceSchema.parse({
2249
+ schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
2250
+ provider: "github",
2251
+ tool: action.capability.tool,
2252
+ operation: "check_run_create",
2253
+ resource: providerResource("github", "commit", `${action.capability.resource}:${headSha}`, name),
2254
+ normalized: compactJsonRecord({
2255
+ name,
2256
+ headSha,
2257
+ status: getString(payload.status),
2258
+ conclusion: getString(payload.conclusion),
2259
+ detailsUrl: getString(payload.detailsUrl),
2260
+ externalId: getString(payload.externalId),
2261
+ outputTitle: getString(output.title),
2262
+ outputSummary: redactCredentialLikeText(getString(output.summary) ?? ""),
2263
+ }),
2264
+ });
2265
+ }
2266
+ function githubIssueCommentEvidence(action, payload) {
2267
+ const issueNumber = getNumber(payload.issueNumber);
2268
+ return LedgerProviderEvidenceSchema.parse({
2269
+ schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
2270
+ provider: "github",
2271
+ tool: action.capability.tool,
2272
+ operation: "issue_comment_create",
2273
+ resource: providerResource("github", "issue", `${action.capability.resource}:issue:${issueNumber ?? "unknown"}`),
2274
+ normalized: compactJsonRecord({
2275
+ issueNumber,
2276
+ body: redactCredentialLikeText(getString(payload.body) ?? ""),
2277
+ }),
2278
+ });
2279
+ }
2280
+ function postgresSelectEvidence(action, payload) {
2281
+ const statement = typeof payload.statement === "string" ? payload.statement : "";
2282
+ const analysis = analyzePostgresSelect(statement);
2283
+ const parameters = Array.isArray(payload.parameters) ? payload.parameters : [];
2284
+ const maxRows = typeof payload.maxRows === "number" && Number.isInteger(payload.maxRows)
2285
+ ? payload.maxRows
2286
+ : null;
2287
+ return LedgerProviderEvidenceSchema.parse({
2288
+ schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
2289
+ provider: "postgres",
2290
+ tool: action.capability.tool,
2291
+ operation: "query_select",
2292
+ resource: providerResource("postgres", "database", action.capability.resource),
2293
+ normalized: {
2294
+ statementHash: analysis.ok ? analysis.analysis.statementHash : null,
2295
+ predicateHash: analysis.ok ? analysis.analysis.predicateHash : null,
2296
+ tables: analysis.ok ? analysis.analysis.tables : [],
2297
+ functions: analysis.ok ? analysis.analysis.functions : [],
2298
+ parameterCount: analysis.ok ? analysis.analysis.parameterCount : 0,
2299
+ parameterHash: hashPayload(parameters),
2300
+ maxRows,
2301
+ },
2302
+ });
2303
+ }
2304
+ function driveFileReadEvidence(action, payload) {
2305
+ const fileId = getString(payload.fileId) ?? "unknown";
2306
+ return LedgerProviderEvidenceSchema.parse({
2307
+ schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
2308
+ provider: "drive",
2309
+ tool: action.capability.tool,
2310
+ operation: "file_read",
2311
+ resource: providerResource("drive", "file", `drive:file:${fileId}`, fileId),
2312
+ normalized: compactJsonRecord({
2313
+ fileId,
2314
+ scope: "https://www.googleapis.com/auth/drive.file",
2315
+ maxBytes: getNumber(payload.maxBytes),
2316
+ }),
2317
+ });
2318
+ }
916
2319
  function slackMessageEvidence(action, payload) {
917
2320
  const channel = getString(payload.channel) ?? "unknown";
918
2321
  const message = getString(payload.text) ?? getString(payload.message) ?? "";
@@ -937,9 +2340,6 @@ function slackMessageEvidence(action, payload) {
937
2340
  }),
938
2341
  });
939
2342
  }
940
- function linearIssueEvidence(action, payload, operation) {
941
- return issueTrackerEvidence(action, payload, "linear", operation);
942
- }
943
2343
  function issueTrackerEvidence(action, payload, provider, operation) {
944
2344
  const issueKey = getString(payload.issueKey) ?? getString(payload.issueId) ?? "unknown";
945
2345
  const projectKey = getString(payload.projectKey) ?? issueKey.split("-", 1)[0];
@@ -956,11 +2356,12 @@ function issueTrackerEvidence(action, payload, provider, operation) {
956
2356
  status: getString(payload.status),
957
2357
  stateId: getString(payload.stateId),
958
2358
  priority: getNumber(payload.priority),
2359
+ priorityId: getString(payload.priorityId),
959
2360
  commentBody: operation === "comment_create"
960
2361
  ? redactCredentialLikeText(getString(payload.body) ?? "")
961
2362
  : null,
962
2363
  }),
963
- fieldChanges: linearFieldChanges(payload),
2364
+ fieldChanges: issueTrackerFieldChanges(payload),
964
2365
  });
965
2366
  }
966
2367
  function docsEvidence(action, payload, operation) {
@@ -1089,13 +2490,14 @@ function changedFilesFromPayload(payload) {
1089
2490
  .filter((path) => Boolean(path));
1090
2491
  return [...new Set([...getStringArray(payload.filesChanged), ...changes])];
1091
2492
  }
1092
- function linearFieldChanges(payload) {
2493
+ function issueTrackerFieldChanges(payload) {
1093
2494
  return [
1094
2495
  fieldChange("title", payload.fromTitle, payload.title),
1095
2496
  fieldChange("description", payload.fromDescription, payload.description),
1096
2497
  fieldChange("status", payload.fromStatus, payload.status),
1097
2498
  fieldChange("stateId", payload.fromStateId, payload.stateId),
1098
2499
  fieldChange("priority", payload.fromPriority, payload.priority),
2500
+ fieldChange("priorityId", payload.fromPriorityId, payload.priorityId),
1099
2501
  ].filter((change) => change !== null);
1100
2502
  }
1101
2503
  function fieldChange(field, before, after) {
@@ -1187,7 +2589,9 @@ function canonicalize(value) {
1187
2589
  if (isPlainObject(value)) {
1188
2590
  return Object.fromEntries(Object.entries(value)
1189
2591
  .filter(([, entryValue]) => entryValue !== undefined)
1190
- .sort(([left], [right]) => left.localeCompare(right))
2592
+ // UTF-16 code unit order per RFC 8785 (JCS); locale-sensitive sorts
2593
+ // would make hashes non-portable across environments.
2594
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
1191
2595
  .map(([key, entryValue]) => [key, canonicalize(entryValue)]));
1192
2596
  }
1193
2597
  return value;
@@ -1201,4 +2605,262 @@ function isPlainObject(value) {
1201
2605
  function isNodeError(error) {
1202
2606
  return error instanceof Error && "code" in error;
1203
2607
  }
2608
+ // ─────────────────────────────────────────────────────────────────────────────
2609
+ // SDK facade (PRD §8.2)
2610
+ //
2611
+ // The ergonomic developer entry point. It exposes the five PRD verbs —
2612
+ // `authorize`, `verify`, `record`, `revoke`, `explain` — over a flat request
2613
+ // shape and delegates to the tested core functions above. It adds no new
2614
+ // authorization logic and changes no token/ledger/policy shape; it only maps the
2615
+ // flat request to a NormalizedAction, threads a local hash-linked ledger head,
2616
+ // and promotes the deterministic reason prose (`explainDecision`).
2617
+ //
2618
+ // Keys: if no signing key is configured, the facade resolves a persistent dev
2619
+ // keypair (`devKeypair()` — env-backed or a 0600 local keyring file) so the
2620
+ // quickstart runs with zero key plumbing and its passes verify across process
2621
+ // restarts. Configure { signingKey, verificationKey } (or a hosted issuer) for
2622
+ // production.
2623
+ // ─────────────────────────────────────────────────────────────────────────────
2624
+ export const AXTARY_DEFAULT_ISSUER = "axtary-dev";
2625
+ export const AXTARY_DEFAULT_TENANT = "local";
2626
+ export const AXTARY_DEFAULT_RUNTIME = "sdk";
2627
+ function isNormalizedActionShape(value) {
2628
+ return (typeof value === "object" &&
2629
+ value !== null &&
2630
+ "actor" in value &&
2631
+ "capability" in value &&
2632
+ "intent" in value);
2633
+ }
2634
+ function isAuthorizeResultShape(value) {
2635
+ return (typeof value === "object" &&
2636
+ value !== null &&
2637
+ "action" in value &&
2638
+ "decision" in value &&
2639
+ typeof value.decision === "object");
2640
+ }
2641
+ function isAxtaryPassShape(value) {
2642
+ return (typeof value === "object" &&
2643
+ value !== null &&
2644
+ "token" in value &&
2645
+ "claims" in value);
2646
+ }
2647
+ export class Axtary {
2648
+ issuer;
2649
+ tenant;
2650
+ runtime;
2651
+ defaultPolicy;
2652
+ algorithm;
2653
+ quiet;
2654
+ configuredSigningKey;
2655
+ configuredKeyId;
2656
+ configuredVerificationKey;
2657
+ keyPath;
2658
+ signerPromise;
2659
+ ledgerHead = null;
2660
+ revocations = new Map();
2661
+ constructor(config = {}) {
2662
+ this.issuer = config.issuer ?? AXTARY_DEFAULT_ISSUER;
2663
+ this.tenant = config.tenant ?? AXTARY_DEFAULT_TENANT;
2664
+ this.runtime = config.runtime ?? AXTARY_DEFAULT_RUNTIME;
2665
+ this.defaultPolicy = config.policy;
2666
+ this.algorithm = config.algorithm ?? DEFAULT_SIGNING_ALGORITHM;
2667
+ this.quiet = config.quiet ?? false;
2668
+ this.configuredSigningKey = config.signingKey;
2669
+ this.configuredKeyId = config.signingKeyId;
2670
+ this.configuredVerificationKey = config.verificationKey;
2671
+ this.keyPath = config.keyPath;
2672
+ }
2673
+ /** Current local ledger head (hash of the last record this instance wrote). */
2674
+ get ledgerHash() {
2675
+ return this.ledgerHead;
2676
+ }
2677
+ async resolveSigner() {
2678
+ if (!this.signerPromise) {
2679
+ this.signerPromise = (async () => {
2680
+ if (this.configuredSigningKey) {
2681
+ return {
2682
+ signingKey: this.configuredSigningKey,
2683
+ keyId: this.configuredKeyId,
2684
+ verificationKey: this.configuredVerificationKey,
2685
+ algorithm: this.algorithm,
2686
+ };
2687
+ }
2688
+ const dev = await devKeypair({ path: this.keyPath });
2689
+ if (!this.quiet) {
2690
+ const origin = dev.source === "env"
2691
+ ? `from ${AXTARY_DEV_SIGNING_KEY_ENV}`
2692
+ : `at ${dev.path}`;
2693
+ console.warn("[axtary] No signing key configured — issuing ActionPasses with a " +
2694
+ `persistent local dev key (${origin}). These passes verify across ` +
2695
+ "process restarts on this machine. Pass { signingKey, " +
2696
+ "verificationKey } to createAxtary() (or use a hosted issuer) for " +
2697
+ "production.");
2698
+ }
2699
+ return {
2700
+ signingKey: dev.signingKey,
2701
+ keyId: dev.signingKeyId,
2702
+ verificationKey: dev.verificationKey,
2703
+ algorithm: dev.algorithm,
2704
+ };
2705
+ })();
2706
+ }
2707
+ return this.signerPromise;
2708
+ }
2709
+ toNormalizedAction(request) {
2710
+ if (isNormalizedActionShape(request)) {
2711
+ return parseNormalizedAction(request);
2712
+ }
2713
+ return parseNormalizedAction({
2714
+ schemaVersion: ACTION_SCHEMA_VERSION,
2715
+ actor: {
2716
+ agentId: request.agent,
2717
+ humanOwner: request.human,
2718
+ runtime: request.runtime ?? this.runtime,
2719
+ tenant: request.tenant ?? this.tenant,
2720
+ },
2721
+ intent: {
2722
+ taskId: request.task ?? `task_${randomUUID()}`,
2723
+ declaredGoal: request.intent,
2724
+ maxDelegationDepth: request.maxDelegationDepth,
2725
+ },
2726
+ capability: {
2727
+ tool: request.tool,
2728
+ resource: request.resource,
2729
+ payload: request.payload ?? {},
2730
+ constraints: request.constraints,
2731
+ },
2732
+ toolDefinition: request.toolDefinition,
2733
+ provenance: request.provenance,
2734
+ budget: request.budget,
2735
+ });
2736
+ }
2737
+ /**
2738
+ * Decide an action and, for an allow, issue an ActionPass. Always returns a
2739
+ * decision (the policy evaluation is keyless); a deny/step-up returns
2740
+ * `pass: null`. Threads the local ledger head so successive calls chain.
2741
+ */
2742
+ async authorize(request, options = {}) {
2743
+ const signer = await this.resolveSigner();
2744
+ const action = this.toNormalizedAction(request);
2745
+ const result = await authorize({
2746
+ action,
2747
+ issuer: this.issuer,
2748
+ tenant: this.tenant,
2749
+ signingKey: signer.signingKey,
2750
+ keyId: signer.keyId,
2751
+ algorithm: signer.algorithm,
2752
+ policy: options.policy ?? this.defaultPolicy,
2753
+ approval: options.approval,
2754
+ approvalArtifact: options.approvalArtifact,
2755
+ previousLedgerHash: this.ledgerHead,
2756
+ now: options.now,
2757
+ });
2758
+ this.ledgerHead = result.ledger.ledgerHash;
2759
+ return {
2760
+ status: result.decision.decision,
2761
+ decision: result.decision,
2762
+ reasons: result.decision.reasons,
2763
+ explanation: explainDecision(result.decision),
2764
+ payloadHash: result.payloadHash,
2765
+ action: result.action,
2766
+ pass: result.actionPass,
2767
+ ledger: result.ledger,
2768
+ };
2769
+ }
2770
+ /**
2771
+ * Verify a previously issued pass against an action. Accepts an authorize
2772
+ * result, a pass object, or a raw token; the action defaults to the result's
2773
+ * action when one is supplied. Fails closed for a pass this instance revoked.
2774
+ */
2775
+ async verify(pass, action) {
2776
+ let token;
2777
+ let actionInput = action;
2778
+ if (typeof pass === "string") {
2779
+ token = pass;
2780
+ }
2781
+ else if (isAuthorizeResultShape(pass)) {
2782
+ token = pass.pass?.token ?? null;
2783
+ actionInput = actionInput ?? pass.action;
2784
+ }
2785
+ else if (isAxtaryPassShape(pass)) {
2786
+ token = pass.token;
2787
+ }
2788
+ else {
2789
+ token = null;
2790
+ }
2791
+ if (!token) {
2792
+ return { valid: false, reason: "actionpass_not_issued" };
2793
+ }
2794
+ if (!actionInput) {
2795
+ return { valid: false, reason: "verify_requires_action" };
2796
+ }
2797
+ const signer = await this.resolveSigner();
2798
+ if (!signer.verificationKey) {
2799
+ return { valid: false, reason: "verification_key_required" };
2800
+ }
2801
+ return verifyActionPass({
2802
+ token,
2803
+ action: this.toNormalizedAction(actionInput),
2804
+ verificationKey: signer.verificationKey,
2805
+ issuer: this.issuer,
2806
+ algorithms: [signer.algorithm],
2807
+ revocations: this.revocations.values(),
2808
+ });
2809
+ }
2810
+ /**
2811
+ * Append an execution result to the local ledger, chained after the decision
2812
+ * record from `authorize`. Returns the tamper-evident record; durable
2813
+ * persistence is the proxy/CLI's responsibility.
2814
+ */
2815
+ record(input) {
2816
+ const action = this.toNormalizedAction(input.action);
2817
+ const passId = typeof input.pass === "string"
2818
+ ? input.pass
2819
+ : (input.pass?.claims.jti ?? null);
2820
+ const executionOutcome = input.outcome
2821
+ ? LedgerExecutionOutcomeSchema.parse({
2822
+ schemaVersion: LEDGER_EXECUTION_OUTCOME_VERSION,
2823
+ ...input.outcome,
2824
+ })
2825
+ : undefined;
2826
+ const record = recordDecision({
2827
+ action,
2828
+ decision: input.decision,
2829
+ actionPassId: passId,
2830
+ executionOutcome,
2831
+ previousLedgerHash: input.previousLedgerHash ?? this.ledgerHead,
2832
+ correlationId: input.correlationId ?? passId,
2833
+ });
2834
+ this.ledgerHead = record.ledgerHash;
2835
+ return record;
2836
+ }
2837
+ /**
2838
+ * Revoke a pass by id. The revocation is held in this instance so a later
2839
+ * `verify` of that pass fails closed. Durable cross-process revocation is the
2840
+ * CLI/trust-store path (`axtary revoke`).
2841
+ */
2842
+ revoke(passId, options = {}) {
2843
+ const revocation = revokeActionPass({
2844
+ passId,
2845
+ revokedBy: options.revokedBy,
2846
+ reason: options.reason,
2847
+ revokedAt: options.revokedAt,
2848
+ });
2849
+ this.revocations.set(passId, revocation);
2850
+ return revocation;
2851
+ }
2852
+ /** Deterministic, human-readable explanation of a decision (no model call). */
2853
+ explain(decision) {
2854
+ const policyDecision = isAuthorizeResultShape(decision)
2855
+ ? decision.decision
2856
+ : decision;
2857
+ return explainDecision(policyDecision);
2858
+ }
2859
+ }
2860
+ /** Create an Axtary SDK instance. */
2861
+ export function createAxtary(config = {}) {
2862
+ return new Axtary(config);
2863
+ }
2864
+ /** Default Axtary SDK instance (PRD §8.2 `import { axtary }`). */
2865
+ export const axtary = createAxtary();
1204
2866
  //# sourceMappingURL=index.js.map