@axtary/actionpass 0.1.0 → 0.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.
- package/README.md +88 -6
- package/dist/caep.d.ts +89 -0
- package/dist/caep.d.ts.map +1 -0
- package/dist/caep.js +258 -0
- package/dist/caep.js.map +1 -0
- package/dist/dev-key.d.ts +50 -0
- package/dist/dev-key.d.ts.map +1 -0
- package/dist/dev-key.js +103 -0
- package/dist/dev-key.js.map +1 -0
- package/dist/explain.d.ts +12 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +147 -0
- package/dist/explain.js.map +1 -0
- package/dist/index.d.ts +1007 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1709 -62
- package/dist/index.js.map +1 -1
- package/dist/postgres.d.ts +21 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +112 -0
- package/dist/postgres.js.map +1 -0
- package/dist/provenance.d.ts +59 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +67 -0
- package/dist/provenance.js.map +1 -0
- package/dist/status-list.d.ts +158 -0
- package/dist/status-list.d.ts.map +1 -0
- package/dist/status-list.js +447 -0
- package/dist/status-list.js.map +1 -0
- package/package.json +3 -1
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 {
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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),
|
|
@@ -214,12 +428,39 @@ export const LedgerExecutionOutcomeSchema = z
|
|
|
214
428
|
sideEffectProof: z.record(z.string(), JsonValueSchema).default({}),
|
|
215
429
|
})
|
|
216
430
|
.strict();
|
|
431
|
+
export const DelegationLedgerEdgeSchema = z.object({
|
|
432
|
+
parentPassId: z.string().min(1),
|
|
433
|
+
childPassId: z.string().min(1),
|
|
434
|
+
depth: z.number().int().positive(),
|
|
435
|
+
rootPassId: z.string().min(1),
|
|
436
|
+
});
|
|
437
|
+
export const BudgetLedgerEventSchema = z.object({
|
|
438
|
+
reservationId: z.string().min(1).nullable(),
|
|
439
|
+
scope: z.string().min(1),
|
|
440
|
+
status: z.enum(["denied", "pending", "committed", "rolled_back"]),
|
|
441
|
+
expiresAt: z.string().datetime().nullable(),
|
|
442
|
+
cost: BudgetAmountSchema,
|
|
443
|
+
limit: BudgetAmountSchema,
|
|
444
|
+
committedBefore: BudgetAmountSchema,
|
|
445
|
+
committedAfter: BudgetAmountSchema,
|
|
446
|
+
reservedAfter: BudgetAmountSchema,
|
|
447
|
+
});
|
|
448
|
+
export const LedgerAuditContextSchema = z.object({
|
|
449
|
+
tenant: z.string().min(1).nullable(),
|
|
450
|
+
agentId: z.string().min(1),
|
|
451
|
+
humanOwner: z.string().min(1),
|
|
452
|
+
taskId: z.string().min(1),
|
|
453
|
+
tool: z.string().min(1),
|
|
454
|
+
resource: z.string().min(1),
|
|
455
|
+
});
|
|
217
456
|
export const LedgerRecordSchema = z.object({
|
|
218
457
|
schemaVersion: z.literal(LEDGER_SCHEMA_VERSION),
|
|
219
458
|
id: z.string().min(1),
|
|
220
459
|
occurredAt: z.string().datetime(),
|
|
460
|
+
auditContext: LedgerAuditContextSchema.optional(),
|
|
221
461
|
actionHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
222
462
|
payloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
463
|
+
provenanceHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
|
|
223
464
|
decision: AxtaryDecisionSchema,
|
|
224
465
|
reasons: z.array(z.string().min(1)),
|
|
225
466
|
policy: PolicyDecisionSchema.shape.policy,
|
|
@@ -227,6 +468,8 @@ export const LedgerRecordSchema = z.object({
|
|
|
227
468
|
executionOutcome: LedgerExecutionOutcomeSchema.optional(),
|
|
228
469
|
traceId: z.string().min(1).optional(),
|
|
229
470
|
actionPassId: z.string().min(1).nullable(),
|
|
471
|
+
delegation: DelegationLedgerEdgeSchema.optional(),
|
|
472
|
+
budget: BudgetLedgerEventSchema.optional(),
|
|
230
473
|
correlationId: z.string().min(1).optional(),
|
|
231
474
|
previousLedgerHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(),
|
|
232
475
|
ledgerHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
@@ -358,6 +601,7 @@ export function createApprovalArtifact(input) {
|
|
|
358
601
|
id: input.id ?? `approval_${randomUUID()}`,
|
|
359
602
|
mode: input.mode,
|
|
360
603
|
approvedBy: input.approvedBy,
|
|
604
|
+
approverRoles: input.approverRoles,
|
|
361
605
|
approvedAt: (input.approvedAt ?? new Date()).toISOString(),
|
|
362
606
|
actionHash: hashAction(action),
|
|
363
607
|
payloadHash: hashPayload(action.capability.payload),
|
|
@@ -374,6 +618,7 @@ export function createApprovalArtifact(input) {
|
|
|
374
618
|
approval: {
|
|
375
619
|
mode: artifact.mode,
|
|
376
620
|
approvedBy: artifact.approvedBy,
|
|
621
|
+
approverRoles: artifact.approverRoles,
|
|
377
622
|
approvedAt: artifact.approvedAt,
|
|
378
623
|
approvalArtifact: artifact.id,
|
|
379
624
|
approvalArtifactHash: artifactHash,
|
|
@@ -383,6 +628,122 @@ export function createApprovalArtifact(input) {
|
|
|
383
628
|
};
|
|
384
629
|
}
|
|
385
630
|
export async function issueActionPass(input) {
|
|
631
|
+
return issueActionPassProfile(input, ACTIONPASS_V0_VERSION);
|
|
632
|
+
}
|
|
633
|
+
export async function issueActionPassV1(input) {
|
|
634
|
+
const publicJwk = publicOnlyJwk(input.holderPublicJwk);
|
|
635
|
+
const jkt = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
636
|
+
return (await issueActionPassProfile(input, ACTIONPASS_V1_VERSION, jkt));
|
|
637
|
+
}
|
|
638
|
+
export async function issueActionPassV2(input) {
|
|
639
|
+
const publicJwk = publicOnlyJwk(input.holderPublicJwk);
|
|
640
|
+
const jkt = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
641
|
+
return (await issueActionPassProfile(input, ACTIONPASS_V2_VERSION, jkt));
|
|
642
|
+
}
|
|
643
|
+
export async function delegateActionPassV1(input) {
|
|
644
|
+
const delegationNow = input.now ?? input.parentChain.at(-1)?.currentDate ?? new Date();
|
|
645
|
+
const verifiedChain = await verifyDelegationChain({
|
|
646
|
+
chain: input.parentChain,
|
|
647
|
+
currentDate: delegationNow,
|
|
648
|
+
revocation: input.revocation,
|
|
649
|
+
});
|
|
650
|
+
if (!verifiedChain.valid) {
|
|
651
|
+
throw new Error(verifiedChain.reason);
|
|
652
|
+
}
|
|
653
|
+
const parentClaims = verifiedChain.claims.at(-1);
|
|
654
|
+
const parentAction = verifiedChain.actions.at(-1);
|
|
655
|
+
if (!parentClaims || !parentAction) {
|
|
656
|
+
throw new Error("delegation_parent_required");
|
|
657
|
+
}
|
|
658
|
+
const childAction = parseNormalizedAction(input.childAction);
|
|
659
|
+
if (input.tenant !== parentClaims.tenant) {
|
|
660
|
+
throw new Error("delegation_scope_broadened");
|
|
661
|
+
}
|
|
662
|
+
const attenuationError = validateDelegationAttenuation(parentAction, childAction);
|
|
663
|
+
if (attenuationError) {
|
|
664
|
+
throw new Error(attenuationError);
|
|
665
|
+
}
|
|
666
|
+
const depth = parentClaims.delegation
|
|
667
|
+
? parentClaims.delegation.depth + 1
|
|
668
|
+
: 1;
|
|
669
|
+
const rootPassId = parentClaims.delegation?.rootPassId ?? parentClaims.jti;
|
|
670
|
+
const now = delegationNow;
|
|
671
|
+
const requestedTtl = input.expiresInSeconds ?? input.childDecision.constraints.expiresInSeconds;
|
|
672
|
+
const parentRemainingTtl = parentClaims.exp - toNumericDate(now);
|
|
673
|
+
if (parentRemainingTtl < 1) {
|
|
674
|
+
throw new Error("delegation_parent_expired");
|
|
675
|
+
}
|
|
676
|
+
const issued = await issueActionPassV1({
|
|
677
|
+
...input,
|
|
678
|
+
action: childAction,
|
|
679
|
+
decision: input.childDecision,
|
|
680
|
+
holderPublicJwk: input.childHolderPublicJwk,
|
|
681
|
+
parentPassId: parentClaims.jti,
|
|
682
|
+
delegation: {
|
|
683
|
+
depth,
|
|
684
|
+
rootPassId,
|
|
685
|
+
},
|
|
686
|
+
expiresInSeconds: Math.min(requestedTtl, parentRemainingTtl),
|
|
687
|
+
});
|
|
688
|
+
const delegation = DelegationLedgerEdgeSchema.parse({
|
|
689
|
+
parentPassId: parentClaims.jti,
|
|
690
|
+
childPassId: issued.claims.jti,
|
|
691
|
+
depth,
|
|
692
|
+
rootPassId,
|
|
693
|
+
});
|
|
694
|
+
return {
|
|
695
|
+
...issued,
|
|
696
|
+
delegation,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
export async function delegateActionPassV2(input) {
|
|
700
|
+
const delegationNow = input.now ?? input.parentChain.at(-1)?.currentDate ?? new Date();
|
|
701
|
+
const verifiedChain = await verifyDelegationChain({
|
|
702
|
+
chain: input.parentChain,
|
|
703
|
+
currentDate: delegationNow,
|
|
704
|
+
revocation: input.revocation,
|
|
705
|
+
});
|
|
706
|
+
if (!verifiedChain.valid)
|
|
707
|
+
throw new Error(verifiedChain.reason);
|
|
708
|
+
if (verifiedChain.claims.some((claims) => claims.apv !== ACTIONPASS_V2_VERSION)) {
|
|
709
|
+
throw new Error("delegation_requires_actionpass_v2");
|
|
710
|
+
}
|
|
711
|
+
const parentClaims = verifiedChain.claims.at(-1);
|
|
712
|
+
const parentAction = verifiedChain.actions.at(-1);
|
|
713
|
+
const childAction = parseNormalizedAction(input.childAction);
|
|
714
|
+
if (input.tenant !== parentClaims.tenant) {
|
|
715
|
+
throw new Error("delegation_scope_broadened");
|
|
716
|
+
}
|
|
717
|
+
const attenuationError = validateDelegationAttenuation(parentAction, childAction);
|
|
718
|
+
if (attenuationError)
|
|
719
|
+
throw new Error(attenuationError);
|
|
720
|
+
const depth = parentClaims.delegation
|
|
721
|
+
? parentClaims.delegation.depth + 1
|
|
722
|
+
: 1;
|
|
723
|
+
const rootPassId = parentClaims.delegation?.rootPassId ?? parentClaims.jti;
|
|
724
|
+
const parentRemainingTtl = parentClaims.exp - toNumericDate(delegationNow);
|
|
725
|
+
if (parentRemainingTtl < 1)
|
|
726
|
+
throw new Error("delegation_parent_expired");
|
|
727
|
+
const requestedTtl = input.expiresInSeconds ?? input.childDecision.constraints.expiresInSeconds;
|
|
728
|
+
const issued = await issueActionPassV2({
|
|
729
|
+
...input,
|
|
730
|
+
action: childAction,
|
|
731
|
+
decision: input.childDecision,
|
|
732
|
+
holderPublicJwk: input.childHolderPublicJwk,
|
|
733
|
+
parentPassId: parentClaims.jti,
|
|
734
|
+
delegation: { depth, rootPassId },
|
|
735
|
+
status: input.childStatus,
|
|
736
|
+
expiresInSeconds: Math.min(requestedTtl, parentRemainingTtl),
|
|
737
|
+
});
|
|
738
|
+
const delegation = DelegationLedgerEdgeSchema.parse({
|
|
739
|
+
parentPassId: parentClaims.jti,
|
|
740
|
+
childPassId: issued.claims.jti,
|
|
741
|
+
depth,
|
|
742
|
+
rootPassId,
|
|
743
|
+
});
|
|
744
|
+
return { ...issued, delegation };
|
|
745
|
+
}
|
|
746
|
+
async function issueActionPassProfile(input, version, holderJkt) {
|
|
386
747
|
const action = parseNormalizedAction(input.action);
|
|
387
748
|
const decision = PolicyDecisionSchema.parse(input.decision);
|
|
388
749
|
if (decision.decision !== "allow") {
|
|
@@ -392,7 +753,7 @@ export async function issueActionPass(input) {
|
|
|
392
753
|
const issuedAt = toNumericDate(now);
|
|
393
754
|
const expiresInSeconds = input.expiresInSeconds ?? decision.constraints.expiresInSeconds;
|
|
394
755
|
const expiresAt = issuedAt + expiresInSeconds;
|
|
395
|
-
const passId = `ap_${randomUUID()}`;
|
|
756
|
+
const passId = input.passId ?? `ap_${randomUUID()}`;
|
|
396
757
|
const actionHash = hashAction(action);
|
|
397
758
|
const payloadHash = hashPayload(action.capability.payload);
|
|
398
759
|
const approval = bindApproval({
|
|
@@ -404,7 +765,7 @@ export async function issueActionPass(input) {
|
|
|
404
765
|
now,
|
|
405
766
|
});
|
|
406
767
|
const claims = ActionPassClaimsSchema.parse({
|
|
407
|
-
apv:
|
|
768
|
+
apv: version,
|
|
408
769
|
iss: input.issuer,
|
|
409
770
|
sub: action.actor.agentId,
|
|
410
771
|
aud: action.capability.resource,
|
|
@@ -425,6 +786,19 @@ export async function issueActionPass(input) {
|
|
|
425
786
|
reasons: decision.reasons,
|
|
426
787
|
policy: decision.policy,
|
|
427
788
|
payloadHash,
|
|
789
|
+
provenanceHash: action.provenance
|
|
790
|
+
? hashProvenance(action.provenance)
|
|
791
|
+
: undefined,
|
|
792
|
+
budget: action.budget,
|
|
793
|
+
cnf: holderJkt ? { jkt: holderJkt } : undefined,
|
|
794
|
+
delegation: (version === ACTIONPASS_V1_VERSION || version === ACTIONPASS_V2_VERSION) &&
|
|
795
|
+
"delegation" in input &&
|
|
796
|
+
input.delegation
|
|
797
|
+
? input.delegation
|
|
798
|
+
: undefined,
|
|
799
|
+
status: version === ACTIONPASS_V2_VERSION && "status" in input
|
|
800
|
+
? input.status
|
|
801
|
+
: undefined,
|
|
428
802
|
approval,
|
|
429
803
|
audit: {
|
|
430
804
|
traceId: input.traceId ?? `trace_${randomUUID()}`,
|
|
@@ -441,7 +815,194 @@ export async function issueActionPass(input) {
|
|
|
441
815
|
.sign(input.signingKey);
|
|
442
816
|
return { token, claims };
|
|
443
817
|
}
|
|
818
|
+
export async function createDpopProof(input) {
|
|
819
|
+
const publicJwk = publicOnlyJwk(input.publicJwk);
|
|
820
|
+
const jkt = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
821
|
+
const claims = DpopProofClaimsSchema.parse({
|
|
822
|
+
htm: normalizeDpopMethod(input.method),
|
|
823
|
+
htu: normalizeDpopUri(input.uri),
|
|
824
|
+
iat: toNumericDate(input.now ?? new Date()),
|
|
825
|
+
jti: input.proofId ?? `dpop_${randomUUID()}`,
|
|
826
|
+
ath: computeDpopAccessTokenHash(input.actionPass),
|
|
827
|
+
nonce: input.nonce,
|
|
828
|
+
});
|
|
829
|
+
const proof = await new SignJWT(claims)
|
|
830
|
+
.setProtectedHeader({
|
|
831
|
+
alg: input.algorithm ?? DEFAULT_SIGNING_ALGORITHM,
|
|
832
|
+
typ: DPOP_PROOF_TYP,
|
|
833
|
+
jwk: publicJwk,
|
|
834
|
+
})
|
|
835
|
+
.sign(input.signingKey);
|
|
836
|
+
return { proof, claims, jkt };
|
|
837
|
+
}
|
|
838
|
+
export class InMemoryDpopReplayStore {
|
|
839
|
+
entries = new Map();
|
|
840
|
+
consume(proofId, expiresAt, currentDate = new Date()) {
|
|
841
|
+
const now = currentDate.getTime();
|
|
842
|
+
for (const [id, expiry] of this.entries) {
|
|
843
|
+
if (expiry <= now) {
|
|
844
|
+
this.entries.delete(id);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (this.entries.has(proofId)) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
this.entries.set(proofId, expiresAt.getTime());
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Durable, inter-process replay store for DPoP proof `jti`s. A proof captured
|
|
856
|
+
* before a restart stays rejected after restart, for as long as it remains
|
|
857
|
+
* inside its own time window. Entries live only until their proof expires, so
|
|
858
|
+
* the on-disk set stays bounded to the in-flight window rather than growing
|
|
859
|
+
* unboundedly.
|
|
860
|
+
*
|
|
861
|
+
* This is a single-file local store, not a cross-host distributed one: it
|
|
862
|
+
* defends one machine's verifiers (shared across handlers/processes on that
|
|
863
|
+
* host). Authenticated cross-host distribution is out of scope here.
|
|
864
|
+
*
|
|
865
|
+
* Durability mirrors the local ledger: a `proper-lockfile` lease serializes the
|
|
866
|
+
* read-modify-write so `consume` is atomic across processes, appended lines are
|
|
867
|
+
* `fsync`ed before the lease releases, and the file is compacted in place once
|
|
868
|
+
* expired lines outnumber live ones so it cannot grow without bound.
|
|
869
|
+
*/
|
|
870
|
+
export class FileDpopReplayStore {
|
|
871
|
+
filePath;
|
|
872
|
+
options;
|
|
873
|
+
constructor(filePath, options = {}) {
|
|
874
|
+
this.filePath = filePath;
|
|
875
|
+
this.options = options;
|
|
876
|
+
}
|
|
877
|
+
async consume(proofId, expiresAt, currentDate = new Date()) {
|
|
878
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
879
|
+
const release = await this.acquireLock();
|
|
880
|
+
try {
|
|
881
|
+
const now = currentDate.getTime();
|
|
882
|
+
const { live, deadCount } = await this.readLiveEntries(now);
|
|
883
|
+
if (live.has(proofId)) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
live.set(proofId, expiresAt.getTime());
|
|
887
|
+
// Compact when expired tombstones dominate, otherwise append O(1). This
|
|
888
|
+
// keeps the file proportional to the in-flight proof window.
|
|
889
|
+
if (deadCount > live.size) {
|
|
890
|
+
await this.compact(live);
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
await this.append({ i: proofId, e: expiresAt.getTime() });
|
|
894
|
+
}
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
finally {
|
|
898
|
+
await release();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async acquireLock() {
|
|
902
|
+
const lockOptions = this.options.lock ?? {};
|
|
903
|
+
let compromisedReason = null;
|
|
904
|
+
const release = await lock(this.filePath, {
|
|
905
|
+
realpath: false,
|
|
906
|
+
lockfilePath: `${this.filePath}.lock`,
|
|
907
|
+
stale: lockOptions.staleMs ?? 10_000,
|
|
908
|
+
update: lockOptions.updateMs ?? 5_000,
|
|
909
|
+
retries: {
|
|
910
|
+
retries: lockOptions.retries ?? 100,
|
|
911
|
+
factor: 1.2,
|
|
912
|
+
minTimeout: lockOptions.minTimeoutMs ?? 10,
|
|
913
|
+
maxTimeout: lockOptions.maxTimeoutMs ?? 100,
|
|
914
|
+
},
|
|
915
|
+
onCompromised: (error) => {
|
|
916
|
+
compromisedReason = error.message;
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
if (compromisedReason !== null) {
|
|
920
|
+
await release();
|
|
921
|
+
throw new Error(`dpop_replay_store_lock_compromised:${compromisedReason}`);
|
|
922
|
+
}
|
|
923
|
+
return release;
|
|
924
|
+
}
|
|
925
|
+
async readLiveEntries(now) {
|
|
926
|
+
const live = new Map();
|
|
927
|
+
let deadCount = 0;
|
|
928
|
+
let text;
|
|
929
|
+
try {
|
|
930
|
+
text = await readFile(this.filePath, "utf8");
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
934
|
+
return { live, deadCount };
|
|
935
|
+
}
|
|
936
|
+
throw error;
|
|
937
|
+
}
|
|
938
|
+
for (const line of text.split("\n")) {
|
|
939
|
+
const trimmed = line.trim();
|
|
940
|
+
if (!trimmed)
|
|
941
|
+
continue;
|
|
942
|
+
let entry;
|
|
943
|
+
try {
|
|
944
|
+
entry = JSON.parse(trimmed);
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
// A torn final line from a crash mid-append is ignored; the lease
|
|
948
|
+
// holder rewrites a clean file on the next compaction.
|
|
949
|
+
deadCount += 1;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (typeof entry?.i !== "string" ||
|
|
953
|
+
typeof entry?.e !== "number" ||
|
|
954
|
+
entry.e <= now) {
|
|
955
|
+
deadCount += 1;
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
// Last write wins for a repeated id; the older line becomes a tombstone.
|
|
959
|
+
if (live.has(entry.i)) {
|
|
960
|
+
deadCount += 1;
|
|
961
|
+
}
|
|
962
|
+
live.set(entry.i, entry.e);
|
|
963
|
+
}
|
|
964
|
+
return { live, deadCount };
|
|
965
|
+
}
|
|
966
|
+
async append(entry) {
|
|
967
|
+
const handle = await open(this.filePath, "a", 0o600);
|
|
968
|
+
try {
|
|
969
|
+
await handle.chmod(0o600);
|
|
970
|
+
await handle.writeFile(`${JSON.stringify(entry)}\n`, "utf8");
|
|
971
|
+
await handle.sync();
|
|
972
|
+
}
|
|
973
|
+
finally {
|
|
974
|
+
await handle.close();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async compact(live) {
|
|
978
|
+
const lines = [...live.entries()]
|
|
979
|
+
.map(([i, e]) => `${JSON.stringify({ i, e })}\n`)
|
|
980
|
+
.join("");
|
|
981
|
+
const tmpPath = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
982
|
+
let renamed = false;
|
|
983
|
+
try {
|
|
984
|
+
const handle = await open(tmpPath, "wx", 0o600);
|
|
985
|
+
try {
|
|
986
|
+
await handle.writeFile(lines, "utf8");
|
|
987
|
+
await handle.sync();
|
|
988
|
+
}
|
|
989
|
+
finally {
|
|
990
|
+
await handle.close();
|
|
991
|
+
}
|
|
992
|
+
await rename(tmpPath, this.filePath);
|
|
993
|
+
renamed = true;
|
|
994
|
+
}
|
|
995
|
+
finally {
|
|
996
|
+
if (!renamed) {
|
|
997
|
+
await rm(tmpPath, { force: true });
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
444
1002
|
export async function verifyActionPass(input) {
|
|
1003
|
+
return verifyActionPassInternal(input, true);
|
|
1004
|
+
}
|
|
1005
|
+
async function verifyActionPassInternal(input, requireDpop) {
|
|
445
1006
|
try {
|
|
446
1007
|
const action = parseNormalizedAction(input.action);
|
|
447
1008
|
const verificationKey = await resolveVerificationKey(input);
|
|
@@ -461,6 +1022,14 @@ export async function verifyActionPass(input) {
|
|
|
461
1022
|
reason: bindingError,
|
|
462
1023
|
};
|
|
463
1024
|
}
|
|
1025
|
+
if ((claims.apv === ACTIONPASS_V1_VERSION ||
|
|
1026
|
+
claims.apv === ACTIONPASS_V2_VERSION) &&
|
|
1027
|
+
requireDpop) {
|
|
1028
|
+
const dpopError = await validateDpopProof(input, claims);
|
|
1029
|
+
if (dpopError) {
|
|
1030
|
+
return { valid: false, reason: dpopError };
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
464
1033
|
const revocationError = await validateRevocation(input, claims);
|
|
465
1034
|
if (revocationError) {
|
|
466
1035
|
return {
|
|
@@ -468,6 +1037,12 @@ export async function verifyActionPass(input) {
|
|
|
468
1037
|
reason: revocationError,
|
|
469
1038
|
};
|
|
470
1039
|
}
|
|
1040
|
+
if (claims.apv === ACTIONPASS_V2_VERSION) {
|
|
1041
|
+
const statusError = await validateDistributedStatus(input, claims);
|
|
1042
|
+
if (statusError) {
|
|
1043
|
+
return { valid: false, reason: statusError };
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
471
1046
|
return {
|
|
472
1047
|
valid: true,
|
|
473
1048
|
claims,
|
|
@@ -481,6 +1056,61 @@ export async function verifyActionPass(input) {
|
|
|
481
1056
|
};
|
|
482
1057
|
}
|
|
483
1058
|
}
|
|
1059
|
+
export async function verifyDelegationChain(input) {
|
|
1060
|
+
if (input.chain.length === 0) {
|
|
1061
|
+
return { valid: false, reason: "delegation_parent_required" };
|
|
1062
|
+
}
|
|
1063
|
+
const claims = [];
|
|
1064
|
+
const actions = [];
|
|
1065
|
+
for (let index = 0; index < input.chain.length; index += 1) {
|
|
1066
|
+
const entry = input.chain[index];
|
|
1067
|
+
const revocation = input.revocation;
|
|
1068
|
+
const verified = await verifyActionPassInternal({
|
|
1069
|
+
...entry,
|
|
1070
|
+
currentDate: input.currentDate ??
|
|
1071
|
+
input.chain.at(-1)?.currentDate ??
|
|
1072
|
+
entry.currentDate,
|
|
1073
|
+
revokedPassIds: revocation?.revokedPassIds ?? entry.revokedPassIds,
|
|
1074
|
+
revocations: revocation?.revocations ?? entry.revocations,
|
|
1075
|
+
isRevoked: revocation?.isRevoked ?? entry.isRevoked,
|
|
1076
|
+
getStatus: revocation?.getStatus ?? entry.getStatus,
|
|
1077
|
+
}, index === input.chain.length - 1);
|
|
1078
|
+
if (!verified.valid) {
|
|
1079
|
+
return verified;
|
|
1080
|
+
}
|
|
1081
|
+
if (verified.claims.apv !== ACTIONPASS_V1_VERSION &&
|
|
1082
|
+
verified.claims.apv !== ACTIONPASS_V2_VERSION) {
|
|
1083
|
+
return { valid: false, reason: "delegation_requires_actionpass_v1" };
|
|
1084
|
+
}
|
|
1085
|
+
const action = parseNormalizedAction(entry.action);
|
|
1086
|
+
const currentClaims = verified.claims;
|
|
1087
|
+
if (index === 0) {
|
|
1088
|
+
if (currentClaims.audit.parentPassId !== null ||
|
|
1089
|
+
currentClaims.delegation !== undefined) {
|
|
1090
|
+
return { valid: false, reason: "delegation_chain_root_invalid" };
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
const parentClaims = claims[index - 1];
|
|
1095
|
+
const parentAction = actions[index - 1];
|
|
1096
|
+
const expectedDepth = index;
|
|
1097
|
+
const expectedRootPassId = claims[0].jti;
|
|
1098
|
+
if (currentClaims.apv !== parentClaims.apv ||
|
|
1099
|
+
currentClaims.audit.parentPassId !== parentClaims.jti ||
|
|
1100
|
+
currentClaims.delegation?.depth !== expectedDepth ||
|
|
1101
|
+
currentClaims.delegation.rootPassId !== expectedRootPassId) {
|
|
1102
|
+
return { valid: false, reason: "delegation_chain_link_invalid" };
|
|
1103
|
+
}
|
|
1104
|
+
const attenuationError = validateDelegationAttenuation(parentAction, action);
|
|
1105
|
+
if (attenuationError) {
|
|
1106
|
+
return { valid: false, reason: attenuationError };
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
claims.push(currentClaims);
|
|
1110
|
+
actions.push(action);
|
|
1111
|
+
}
|
|
1112
|
+
return { valid: true, claims, actions };
|
|
1113
|
+
}
|
|
484
1114
|
export function revokeActionPass(input) {
|
|
485
1115
|
return ActionPassRevocationSchema.parse({
|
|
486
1116
|
schemaVersion: ACTIONPASS_REVOCATION_VERSION,
|
|
@@ -571,8 +1201,19 @@ export function recordDecision(input) {
|
|
|
571
1201
|
schemaVersion: LEDGER_SCHEMA_VERSION,
|
|
572
1202
|
id: `ledger_${randomUUID()}`,
|
|
573
1203
|
occurredAt: (input.occurredAt ?? new Date()).toISOString(),
|
|
1204
|
+
auditContext: {
|
|
1205
|
+
tenant: action.actor.tenant ?? null,
|
|
1206
|
+
agentId: action.actor.agentId,
|
|
1207
|
+
humanOwner: action.actor.humanOwner,
|
|
1208
|
+
taskId: action.intent.taskId,
|
|
1209
|
+
tool: action.capability.tool,
|
|
1210
|
+
resource: action.capability.resource,
|
|
1211
|
+
},
|
|
574
1212
|
actionHash,
|
|
575
1213
|
payloadHash,
|
|
1214
|
+
provenanceHash: action.provenance
|
|
1215
|
+
? hashProvenance(action.provenance)
|
|
1216
|
+
: undefined,
|
|
576
1217
|
decision: decision.decision,
|
|
577
1218
|
reasons: decision.reasons,
|
|
578
1219
|
policy: decision.policy,
|
|
@@ -580,6 +1221,12 @@ export function recordDecision(input) {
|
|
|
580
1221
|
executionOutcome: input.executionOutcome,
|
|
581
1222
|
traceId,
|
|
582
1223
|
actionPassId: input.actionPassId ?? null,
|
|
1224
|
+
delegation: input.delegation
|
|
1225
|
+
? DelegationLedgerEdgeSchema.parse(input.delegation)
|
|
1226
|
+
: undefined,
|
|
1227
|
+
budget: input.budget
|
|
1228
|
+
? BudgetLedgerEventSchema.parse(input.budget)
|
|
1229
|
+
: undefined,
|
|
583
1230
|
correlationId: input.correlationId ?? undefined,
|
|
584
1231
|
previousLedgerHash: input.previousLedgerHash ?? null,
|
|
585
1232
|
};
|
|
@@ -588,55 +1235,558 @@ export function recordDecision(input) {
|
|
|
588
1235
|
ledgerHash: hashJson(recordWithoutHash),
|
|
589
1236
|
});
|
|
590
1237
|
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Canonical reason token marking a ledger record as the tamper-evident
|
|
1240
|
+
* revocation event for its pass. Matches the forensics `cascade_containment`
|
|
1241
|
+
* convention (spec §9.5) and the trust-store revocation authority (spec §7).
|
|
1242
|
+
*/
|
|
1243
|
+
export const REVOCATION_LEDGER_REASON = "actionpass_revoked";
|
|
1244
|
+
/**
|
|
1245
|
+
* Build a tamper-evident revocation event for a previously recorded pass. The
|
|
1246
|
+
* event is a `deny` record naming the pass with `actionpass_revoked`, reusing
|
|
1247
|
+
* the revoked record's action/payload hashes, policy, and audit context so the
|
|
1248
|
+
* revocation is bound to the exact action it cancels. It carries no delegation
|
|
1249
|
+
* edge or budget event (those describe the original action, not the
|
|
1250
|
+
* revocation). The durable enforcement authority remains the trust store (§7);
|
|
1251
|
+
* this record is the auditable logbook entry, not the enforcement source.
|
|
1252
|
+
*/
|
|
1253
|
+
export function recordActionPassRevocationEvent(input) {
|
|
1254
|
+
const source = LedgerRecordSchema.parse(input.revokedRecord);
|
|
1255
|
+
if (!source.actionPassId) {
|
|
1256
|
+
throw new Error("revocation_requires_action_pass_id");
|
|
1257
|
+
}
|
|
1258
|
+
const reasons = [REVOCATION_LEDGER_REASON];
|
|
1259
|
+
if (input.revokedBy)
|
|
1260
|
+
reasons.push(`revoked_by:${input.revokedBy}`);
|
|
1261
|
+
if (input.reason)
|
|
1262
|
+
reasons.push(`revocation_reason:${input.reason}`);
|
|
1263
|
+
const recordWithoutHash = {
|
|
1264
|
+
schemaVersion: LEDGER_SCHEMA_VERSION,
|
|
1265
|
+
id: `ledger_${randomUUID()}`,
|
|
1266
|
+
occurredAt: (input.occurredAt ?? new Date()).toISOString(),
|
|
1267
|
+
auditContext: source.auditContext,
|
|
1268
|
+
actionHash: source.actionHash,
|
|
1269
|
+
payloadHash: source.payloadHash,
|
|
1270
|
+
provenanceHash: source.provenanceHash,
|
|
1271
|
+
decision: "deny",
|
|
1272
|
+
reasons,
|
|
1273
|
+
policy: source.policy,
|
|
1274
|
+
traceId: `trace_${randomUUID()}`,
|
|
1275
|
+
actionPassId: source.actionPassId,
|
|
1276
|
+
correlationId: source.actionPassId,
|
|
1277
|
+
previousLedgerHash: input.previousLedgerHash ?? null,
|
|
1278
|
+
};
|
|
1279
|
+
return LedgerRecordSchema.parse({
|
|
1280
|
+
...recordWithoutHash,
|
|
1281
|
+
ledgerHash: hashJson(recordWithoutHash),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
591
1284
|
export function hashPayload(payload) {
|
|
592
1285
|
return hashJson(JsonValueSchema.parse(payload));
|
|
593
1286
|
}
|
|
1287
|
+
export function hashProvenance(provenance) {
|
|
1288
|
+
return hashJson(ActionProvenanceSchema.parse(provenance));
|
|
1289
|
+
}
|
|
1290
|
+
function validateDelegationAttenuation(parent, child) {
|
|
1291
|
+
const parentRemaining = parent.intent.maxDelegationDepth ?? 0;
|
|
1292
|
+
const childRemaining = child.intent.maxDelegationDepth ?? 0;
|
|
1293
|
+
if (parentRemaining < 1 || childRemaining > parentRemaining - 1) {
|
|
1294
|
+
return "delegation_depth_exceeded";
|
|
1295
|
+
}
|
|
1296
|
+
if (child.actor.humanOwner !== parent.actor.humanOwner ||
|
|
1297
|
+
child.actor.tenant !== parent.actor.tenant ||
|
|
1298
|
+
child.intent.taskId !== parent.intent.taskId ||
|
|
1299
|
+
child.capability.tool !== parent.capability.tool ||
|
|
1300
|
+
child.capability.resource !== parent.capability.resource ||
|
|
1301
|
+
stableStringify(child.toolDefinition) !==
|
|
1302
|
+
stableStringify(parent.toolDefinition) ||
|
|
1303
|
+
!isJsonScopeSubset(child.capability.payload, parent.capability.payload, []) ||
|
|
1304
|
+
!areConstraintsAttenuated(parent.capability.constraints, child.capability.constraints)) {
|
|
1305
|
+
return "delegation_scope_broadened";
|
|
1306
|
+
}
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
function isJsonScopeSubset(child, parent, path) {
|
|
1310
|
+
if (stableStringify(child) === stableStringify(parent)) {
|
|
1311
|
+
return true;
|
|
1312
|
+
}
|
|
1313
|
+
if (typeof child === "string" && typeof parent === "string") {
|
|
1314
|
+
return isPathLikeKey(path.at(-1)) && isPathInsideScope(child, parent);
|
|
1315
|
+
}
|
|
1316
|
+
if (Array.isArray(child) && Array.isArray(parent)) {
|
|
1317
|
+
return child.every((childEntry) => parent.some((parentEntry) => isJsonScopeSubset(childEntry, parentEntry, path)));
|
|
1318
|
+
}
|
|
1319
|
+
if (isJsonObject(child) && isJsonObject(parent)) {
|
|
1320
|
+
return Object.entries(child).every(([key, value]) => key in parent &&
|
|
1321
|
+
isJsonScopeSubset(value, parent[key], [...path, key]));
|
|
1322
|
+
}
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
function areConstraintsAttenuated(parent, child) {
|
|
1326
|
+
if (!parent) {
|
|
1327
|
+
return true;
|
|
1328
|
+
}
|
|
1329
|
+
if (!child) {
|
|
1330
|
+
return false;
|
|
1331
|
+
}
|
|
1332
|
+
return Object.entries(parent).every(([key, value]) => key in child &&
|
|
1333
|
+
stableStringify(child[key]) === stableStringify(value));
|
|
1334
|
+
}
|
|
1335
|
+
function isPathLikeKey(key) {
|
|
1336
|
+
return Boolean(key && /(path|file|directory|prefix)/i.test(key));
|
|
1337
|
+
}
|
|
1338
|
+
function isPathInsideScope(child, parent) {
|
|
1339
|
+
if (parent.endsWith("/**")) {
|
|
1340
|
+
const prefix = parent.slice(0, -2);
|
|
1341
|
+
return child.startsWith(prefix);
|
|
1342
|
+
}
|
|
1343
|
+
if (parent.endsWith("/")) {
|
|
1344
|
+
return child.startsWith(parent);
|
|
1345
|
+
}
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
594
1348
|
export function hashAction(action) {
|
|
595
1349
|
return hashJson(parseNormalizedAction(action));
|
|
596
1350
|
}
|
|
1351
|
+
export const GITHUB_NATIVE_CONNECTOR_GOVERNANCE = {
|
|
1352
|
+
provider: "github",
|
|
1353
|
+
connector: "github-rest",
|
|
1354
|
+
mode: "rest",
|
|
1355
|
+
configSchema: GitHubNativeConnectorConfigSchema,
|
|
1356
|
+
defaultConfig: DEFAULT_GITHUB_NATIVE_CONNECTOR_CONFIG,
|
|
1357
|
+
requiredScopes: [
|
|
1358
|
+
"contents:write",
|
|
1359
|
+
"metadata:read",
|
|
1360
|
+
"pull_requests:write",
|
|
1361
|
+
"checks:write",
|
|
1362
|
+
"issues:write",
|
|
1363
|
+
],
|
|
1364
|
+
smokeCheck: "GET /user + X-Accepted-GitHub-Permissions",
|
|
1365
|
+
tools: [
|
|
1366
|
+
{
|
|
1367
|
+
capability: {
|
|
1368
|
+
id: "github-pr-create",
|
|
1369
|
+
provider: "github",
|
|
1370
|
+
connector: "github-rest",
|
|
1371
|
+
label: "Create pull request",
|
|
1372
|
+
tool: "github.pull_requests.create",
|
|
1373
|
+
operation: "write",
|
|
1374
|
+
status: "supported",
|
|
1375
|
+
supportedModes: ["fake", "rest", "app"],
|
|
1376
|
+
requiresActionPass: true,
|
|
1377
|
+
defaultPolicy: "step_up",
|
|
1378
|
+
payloadFields: [
|
|
1379
|
+
"title",
|
|
1380
|
+
"baseBranch",
|
|
1381
|
+
"filesChanged",
|
|
1382
|
+
"testsPassed",
|
|
1383
|
+
"touchesProduction",
|
|
1384
|
+
],
|
|
1385
|
+
approvalTriggers: [
|
|
1386
|
+
"protected path",
|
|
1387
|
+
"production impact",
|
|
1388
|
+
"base branch mismatch",
|
|
1389
|
+
"missing tests",
|
|
1390
|
+
"large file set",
|
|
1391
|
+
],
|
|
1392
|
+
credentialBoundary: "GitHub token stays in the proxy/adapter; the agent receives an ActionPass-bound result.",
|
|
1393
|
+
smokeCheck: "GET /user",
|
|
1394
|
+
},
|
|
1395
|
+
normalizeEvidence: (action, payload) => githubPullRequestEvidence(action, payload),
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
capability: {
|
|
1399
|
+
id: "github-content-read",
|
|
1400
|
+
provider: "github",
|
|
1401
|
+
connector: "github-rest",
|
|
1402
|
+
label: "Read repository content",
|
|
1403
|
+
tool: "github.contents.read",
|
|
1404
|
+
operation: "read",
|
|
1405
|
+
status: "supported",
|
|
1406
|
+
supportedModes: ["fake", "rest", "app"],
|
|
1407
|
+
requiresActionPass: true,
|
|
1408
|
+
defaultPolicy: "deny_until_scoped",
|
|
1409
|
+
payloadFields: ["path", "ref"],
|
|
1410
|
+
approvalTriggers: ["denied secret or production path"],
|
|
1411
|
+
credentialBoundary: "Repository content is fetched by the adapter after policy checks deny protected paths.",
|
|
1412
|
+
smokeCheck: "GET /user",
|
|
1413
|
+
},
|
|
1414
|
+
normalizeEvidence: (action, payload) => githubContentEvidence(action, payload, "content_read"),
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
capability: {
|
|
1418
|
+
id: "github-branch-create",
|
|
1419
|
+
provider: "github",
|
|
1420
|
+
connector: "github-rest",
|
|
1421
|
+
label: "Create branch",
|
|
1422
|
+
tool: "github.branches.create",
|
|
1423
|
+
operation: "write",
|
|
1424
|
+
status: "supported",
|
|
1425
|
+
supportedModes: ["fake", "rest", "app"],
|
|
1426
|
+
requiresActionPass: true,
|
|
1427
|
+
defaultPolicy: "allow",
|
|
1428
|
+
payloadFields: ["branch", "baseBranch", "sha", "allowExisting"],
|
|
1429
|
+
approvalTriggers: ["base branch mismatch"],
|
|
1430
|
+
credentialBoundary: "Branch creation runs through the proxy and is bound to the same ActionPass verification path.",
|
|
1431
|
+
smokeCheck: "GET /user",
|
|
1432
|
+
},
|
|
1433
|
+
normalizeEvidence: (action, payload) => githubBranchEvidence(action, payload),
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
capability: {
|
|
1437
|
+
id: "github-content-write",
|
|
1438
|
+
provider: "github",
|
|
1439
|
+
connector: "github-rest",
|
|
1440
|
+
label: "Write repository content",
|
|
1441
|
+
tool: "github.contents.write",
|
|
1442
|
+
operation: "write",
|
|
1443
|
+
status: "supported",
|
|
1444
|
+
supportedModes: ["fake", "rest", "app"],
|
|
1445
|
+
requiresActionPass: true,
|
|
1446
|
+
defaultPolicy: "step_up",
|
|
1447
|
+
payloadFields: ["path", "branch", "message", "sha", "content"],
|
|
1448
|
+
approvalTriggers: ["protected path"],
|
|
1449
|
+
credentialBoundary: "File content is never written until policy and ActionPass verification both pass.",
|
|
1450
|
+
smokeCheck: "GET /user",
|
|
1451
|
+
},
|
|
1452
|
+
normalizeEvidence: (action, payload) => githubContentEvidence(action, payload, "content_write"),
|
|
1453
|
+
},
|
|
1454
|
+
{
|
|
1455
|
+
capability: {
|
|
1456
|
+
id: "github-review-comment-create",
|
|
1457
|
+
provider: "github",
|
|
1458
|
+
connector: "github-rest",
|
|
1459
|
+
label: "Create inline review comment",
|
|
1460
|
+
tool: "github.pull_request_review_comments.create",
|
|
1461
|
+
operation: "write",
|
|
1462
|
+
status: "supported",
|
|
1463
|
+
supportedModes: ["app"],
|
|
1464
|
+
requiresActionPass: true,
|
|
1465
|
+
defaultPolicy: "step_up",
|
|
1466
|
+
payloadFields: [
|
|
1467
|
+
"pullNumber",
|
|
1468
|
+
"body",
|
|
1469
|
+
"commitId",
|
|
1470
|
+
"path",
|
|
1471
|
+
"line",
|
|
1472
|
+
"side",
|
|
1473
|
+
],
|
|
1474
|
+
approvalTriggers: ["protected path review"],
|
|
1475
|
+
credentialBoundary: "The exact inline comment target and body are authorized before GitHub receives the write.",
|
|
1476
|
+
smokeCheck: "GET /user + Pull requests: write",
|
|
1477
|
+
},
|
|
1478
|
+
normalizeEvidence: (action, payload) => githubReviewCommentEvidence(action, payload),
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
capability: {
|
|
1482
|
+
id: "github-check-run-create",
|
|
1483
|
+
provider: "github",
|
|
1484
|
+
connector: "github-rest",
|
|
1485
|
+
label: "Create check run",
|
|
1486
|
+
tool: "github.check_runs.create",
|
|
1487
|
+
operation: "write",
|
|
1488
|
+
status: "supported",
|
|
1489
|
+
supportedModes: ["rest", "app"],
|
|
1490
|
+
requiresActionPass: true,
|
|
1491
|
+
defaultPolicy: "allow",
|
|
1492
|
+
payloadFields: [
|
|
1493
|
+
"name",
|
|
1494
|
+
"headSha",
|
|
1495
|
+
"status",
|
|
1496
|
+
"conclusion",
|
|
1497
|
+
"output",
|
|
1498
|
+
],
|
|
1499
|
+
approvalTriggers: [],
|
|
1500
|
+
credentialBoundary: "Check name, commit SHA, status, conclusion, and output are payload-bound before execution.",
|
|
1501
|
+
smokeCheck: "GET /user + Checks: write",
|
|
1502
|
+
},
|
|
1503
|
+
normalizeEvidence: (action, payload) => githubCheckRunEvidence(action, payload),
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
capability: {
|
|
1507
|
+
id: "github-issue-comment-create",
|
|
1508
|
+
provider: "github",
|
|
1509
|
+
connector: "github-rest",
|
|
1510
|
+
label: "Create issue comment",
|
|
1511
|
+
tool: "github.issue_comments.create",
|
|
1512
|
+
operation: "external_message",
|
|
1513
|
+
status: "supported",
|
|
1514
|
+
supportedModes: ["rest", "app"],
|
|
1515
|
+
requiresActionPass: true,
|
|
1516
|
+
defaultPolicy: "allow",
|
|
1517
|
+
payloadFields: ["issueNumber", "body"],
|
|
1518
|
+
approvalTriggers: [],
|
|
1519
|
+
credentialBoundary: "The exact issue number and outbound comment body are bound into the ActionPass.",
|
|
1520
|
+
smokeCheck: "GET /user + Issues: write",
|
|
1521
|
+
},
|
|
1522
|
+
normalizeEvidence: (action, payload) => githubIssueCommentEvidence(action, payload),
|
|
1523
|
+
},
|
|
1524
|
+
],
|
|
1525
|
+
};
|
|
1526
|
+
export const LINEAR_NATIVE_CONNECTOR_GOVERNANCE = {
|
|
1527
|
+
provider: "linear",
|
|
1528
|
+
connector: "linear-graphql",
|
|
1529
|
+
mode: "graphql",
|
|
1530
|
+
configSchema: LinearNativeConnectorConfigSchema,
|
|
1531
|
+
defaultConfig: DEFAULT_LINEAR_NATIVE_CONNECTOR_CONFIG,
|
|
1532
|
+
requiredScopes: ["read", "write"],
|
|
1533
|
+
smokeCheck: "viewer",
|
|
1534
|
+
tools: [
|
|
1535
|
+
{
|
|
1536
|
+
capability: {
|
|
1537
|
+
id: "linear-issue-read",
|
|
1538
|
+
provider: "linear",
|
|
1539
|
+
connector: "linear-graphql",
|
|
1540
|
+
label: "Read issue",
|
|
1541
|
+
tool: "linear.issues.read",
|
|
1542
|
+
operation: "read",
|
|
1543
|
+
status: "supported",
|
|
1544
|
+
supportedModes: ["fake", "graphql"],
|
|
1545
|
+
requiresActionPass: true,
|
|
1546
|
+
defaultPolicy: "allow",
|
|
1547
|
+
payloadFields: ["issueKey", "projectKey"],
|
|
1548
|
+
approvalTriggers: [],
|
|
1549
|
+
credentialBoundary: "Linear API key stays adapter-side; policy pins issue access by project key.",
|
|
1550
|
+
smokeCheck: "viewer",
|
|
1551
|
+
},
|
|
1552
|
+
normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "linear", "issue_read"),
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
capability: {
|
|
1556
|
+
id: "linear-comment-create",
|
|
1557
|
+
provider: "linear",
|
|
1558
|
+
connector: "linear-graphql",
|
|
1559
|
+
label: "Create issue comment",
|
|
1560
|
+
tool: "linear.comments.create",
|
|
1561
|
+
operation: "write",
|
|
1562
|
+
status: "supported",
|
|
1563
|
+
supportedModes: ["fake", "graphql"],
|
|
1564
|
+
requiresActionPass: true,
|
|
1565
|
+
defaultPolicy: "allow",
|
|
1566
|
+
payloadFields: ["issueKey", "projectKey", "body"],
|
|
1567
|
+
approvalTriggers: [],
|
|
1568
|
+
credentialBoundary: "Linear comments execute only through proxy policy and ActionPass verification.",
|
|
1569
|
+
smokeCheck: "viewer",
|
|
1570
|
+
},
|
|
1571
|
+
normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "linear", "comment_create"),
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
capability: {
|
|
1575
|
+
id: "linear-issue-update",
|
|
1576
|
+
provider: "linear",
|
|
1577
|
+
connector: "linear-graphql",
|
|
1578
|
+
label: "Update issue",
|
|
1579
|
+
tool: "linear.issues.update",
|
|
1580
|
+
operation: "write",
|
|
1581
|
+
status: "supported",
|
|
1582
|
+
supportedModes: ["fake", "graphql"],
|
|
1583
|
+
requiresActionPass: true,
|
|
1584
|
+
defaultPolicy: "step_up",
|
|
1585
|
+
payloadFields: [
|
|
1586
|
+
"issueKey",
|
|
1587
|
+
"projectKey",
|
|
1588
|
+
"status",
|
|
1589
|
+
"title",
|
|
1590
|
+
"description",
|
|
1591
|
+
"priority",
|
|
1592
|
+
],
|
|
1593
|
+
approvalTriggers: ["protected status"],
|
|
1594
|
+
credentialBoundary: "Issue mutations stay behind adapter credentials and project/status policy checks.",
|
|
1595
|
+
smokeCheck: "viewer",
|
|
1596
|
+
},
|
|
1597
|
+
normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "linear", "issue_update"),
|
|
1598
|
+
},
|
|
1599
|
+
],
|
|
1600
|
+
};
|
|
1601
|
+
export const JIRA_NATIVE_CONNECTOR_GOVERNANCE = {
|
|
1602
|
+
provider: "jira",
|
|
1603
|
+
connector: "jira-rest",
|
|
1604
|
+
mode: "rest",
|
|
1605
|
+
configSchema: JiraNativeConnectorConfigSchema,
|
|
1606
|
+
defaultConfig: DEFAULT_JIRA_NATIVE_CONNECTOR_CONFIG,
|
|
1607
|
+
requiredScopes: ["read:jira-work", "write:jira-work"],
|
|
1608
|
+
smokeCheck: "GET /myself",
|
|
1609
|
+
tools: [
|
|
1610
|
+
{
|
|
1611
|
+
capability: {
|
|
1612
|
+
id: "jira-issue-read",
|
|
1613
|
+
provider: "jira",
|
|
1614
|
+
connector: "jira-rest",
|
|
1615
|
+
label: "Read issue",
|
|
1616
|
+
tool: "jira.issues.read",
|
|
1617
|
+
operation: "read",
|
|
1618
|
+
status: "supported",
|
|
1619
|
+
supportedModes: ["fake", "rest"],
|
|
1620
|
+
requiresActionPass: true,
|
|
1621
|
+
defaultPolicy: "allow",
|
|
1622
|
+
payloadFields: ["issueKey", "projectKey"],
|
|
1623
|
+
approvalTriggers: [],
|
|
1624
|
+
credentialBoundary: "Jira credentials stay adapter-side; policy pins issue access by project key.",
|
|
1625
|
+
smokeCheck: "GET /myself",
|
|
1626
|
+
},
|
|
1627
|
+
normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "jira", "issue_read"),
|
|
1628
|
+
},
|
|
1629
|
+
{
|
|
1630
|
+
capability: {
|
|
1631
|
+
id: "jira-comment-create",
|
|
1632
|
+
provider: "jira",
|
|
1633
|
+
connector: "jira-rest",
|
|
1634
|
+
label: "Create issue comment",
|
|
1635
|
+
tool: "jira.comments.create",
|
|
1636
|
+
operation: "write",
|
|
1637
|
+
status: "supported",
|
|
1638
|
+
supportedModes: ["fake", "rest"],
|
|
1639
|
+
requiresActionPass: true,
|
|
1640
|
+
defaultPolicy: "allow",
|
|
1641
|
+
payloadFields: ["issueKey", "projectKey", "body"],
|
|
1642
|
+
approvalTriggers: [],
|
|
1643
|
+
credentialBoundary: "Jira comments are converted to Atlassian Document Format only after policy and ActionPass verification.",
|
|
1644
|
+
smokeCheck: "GET /myself",
|
|
1645
|
+
},
|
|
1646
|
+
normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "jira", "comment_create"),
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
capability: {
|
|
1650
|
+
id: "jira-issue-update",
|
|
1651
|
+
provider: "jira",
|
|
1652
|
+
connector: "jira-rest",
|
|
1653
|
+
label: "Update issue",
|
|
1654
|
+
tool: "jira.issues.update",
|
|
1655
|
+
operation: "write",
|
|
1656
|
+
status: "supported",
|
|
1657
|
+
supportedModes: ["fake", "rest"],
|
|
1658
|
+
requiresActionPass: true,
|
|
1659
|
+
defaultPolicy: "step_up",
|
|
1660
|
+
payloadFields: [
|
|
1661
|
+
"issueKey",
|
|
1662
|
+
"projectKey",
|
|
1663
|
+
"status",
|
|
1664
|
+
"title",
|
|
1665
|
+
"description",
|
|
1666
|
+
"priorityId",
|
|
1667
|
+
],
|
|
1668
|
+
approvalTriggers: ["protected status"],
|
|
1669
|
+
credentialBoundary: "Jira field edits and workflow transitions execute only after exact-payload authorization.",
|
|
1670
|
+
smokeCheck: "GET /myself",
|
|
1671
|
+
},
|
|
1672
|
+
normalizeEvidence: (action, payload) => issueTrackerEvidence(action, payload, "jira", "issue_update"),
|
|
1673
|
+
},
|
|
1674
|
+
],
|
|
1675
|
+
};
|
|
1676
|
+
export const POSTGRES_NATIVE_CONNECTOR_GOVERNANCE = {
|
|
1677
|
+
provider: "postgres",
|
|
1678
|
+
connector: "postgres-native",
|
|
1679
|
+
mode: "postgres",
|
|
1680
|
+
configSchema: PostgresNativeConnectorConfigSchema,
|
|
1681
|
+
defaultConfig: DEFAULT_POSTGRES_NATIVE_CONNECTOR_CONFIG,
|
|
1682
|
+
requiredScopes: [
|
|
1683
|
+
"LOGIN role",
|
|
1684
|
+
"SELECT on allowed tables",
|
|
1685
|
+
"NOBYPASSRLS",
|
|
1686
|
+
"read-only transactions",
|
|
1687
|
+
],
|
|
1688
|
+
smokeCheck: "SELECT current_database/current_user + transaction_read_only + table RLS metadata",
|
|
1689
|
+
tools: [
|
|
1690
|
+
{
|
|
1691
|
+
capability: {
|
|
1692
|
+
id: "postgres-query-select",
|
|
1693
|
+
provider: "postgres",
|
|
1694
|
+
connector: "postgres-native",
|
|
1695
|
+
label: "Run scoped SELECT",
|
|
1696
|
+
tool: "postgres.query.select",
|
|
1697
|
+
operation: "read",
|
|
1698
|
+
status: "supported",
|
|
1699
|
+
supportedModes: ["postgres"],
|
|
1700
|
+
requiresActionPass: true,
|
|
1701
|
+
defaultPolicy: "deny_until_scoped",
|
|
1702
|
+
payloadFields: ["statement", "parameters", "maxRows"],
|
|
1703
|
+
approvalTriggers: ["unpredicated table scan"],
|
|
1704
|
+
credentialBoundary: "The DSN stays in the local adapter; only the exact parameterized SELECT and bounded result cross the authorization boundary.",
|
|
1705
|
+
smokeCheck: "current role/database, read-only state, and RLS metadata for configured tables",
|
|
1706
|
+
},
|
|
1707
|
+
normalizeEvidence: (action, payload) => postgresSelectEvidence(action, payload),
|
|
1708
|
+
},
|
|
1709
|
+
],
|
|
1710
|
+
};
|
|
1711
|
+
export const DRIVE_NATIVE_CONNECTOR_GOVERNANCE = {
|
|
1712
|
+
provider: "drive",
|
|
1713
|
+
connector: "google-drive-rest",
|
|
1714
|
+
mode: "rest",
|
|
1715
|
+
configSchema: DriveNativeConnectorConfigSchema,
|
|
1716
|
+
defaultConfig: DEFAULT_DRIVE_NATIVE_CONNECTOR_CONFIG,
|
|
1717
|
+
requiredScopes: ["https://www.googleapis.com/auth/drive.file"],
|
|
1718
|
+
smokeCheck: "GET selected file metadata including isAppAuthorized (no content)",
|
|
1719
|
+
tools: [
|
|
1720
|
+
{
|
|
1721
|
+
capability: {
|
|
1722
|
+
id: "drive-file-read",
|
|
1723
|
+
provider: "drive",
|
|
1724
|
+
connector: "google-drive-rest",
|
|
1725
|
+
label: "Read selected Drive file",
|
|
1726
|
+
tool: "drive.files.read",
|
|
1727
|
+
operation: "read",
|
|
1728
|
+
status: "supported",
|
|
1729
|
+
supportedModes: ["rest"],
|
|
1730
|
+
requiresActionPass: true,
|
|
1731
|
+
defaultPolicy: "deny_until_scoped",
|
|
1732
|
+
payloadFields: ["fileId", "maxBytes"],
|
|
1733
|
+
approvalTriggers: [
|
|
1734
|
+
"file not policy-scoped",
|
|
1735
|
+
"file not selected through Google Picker",
|
|
1736
|
+
"byte limit exceeded",
|
|
1737
|
+
],
|
|
1738
|
+
credentialBoundary: "The OAuth token and file content stay in the local enforcement plane; only a Picker-authorized, policy-scoped file can be returned.",
|
|
1739
|
+
smokeCheck: "selected-file metadata and provider isAppAuthorized state",
|
|
1740
|
+
},
|
|
1741
|
+
normalizeEvidence: (action, payload) => driveFileReadEvidence(action, payload),
|
|
1742
|
+
},
|
|
1743
|
+
],
|
|
1744
|
+
};
|
|
1745
|
+
export const NATIVE_CONNECTOR_GOVERNANCE_REGISTRY = [
|
|
1746
|
+
GITHUB_NATIVE_CONNECTOR_GOVERNANCE,
|
|
1747
|
+
LINEAR_NATIVE_CONNECTOR_GOVERNANCE,
|
|
1748
|
+
JIRA_NATIVE_CONNECTOR_GOVERNANCE,
|
|
1749
|
+
POSTGRES_NATIVE_CONNECTOR_GOVERNANCE,
|
|
1750
|
+
DRIVE_NATIVE_CONNECTOR_GOVERNANCE,
|
|
1751
|
+
];
|
|
1752
|
+
const providerEvidenceNormalizers = new Map([
|
|
1753
|
+
[
|
|
1754
|
+
"slack.chat.postMessage",
|
|
1755
|
+
(action, payload) => slackMessageEvidence(action, payload),
|
|
1756
|
+
],
|
|
1757
|
+
[
|
|
1758
|
+
"docs.documents.search",
|
|
1759
|
+
(action, payload) => docsEvidence(action, payload, "document_search"),
|
|
1760
|
+
],
|
|
1761
|
+
[
|
|
1762
|
+
"docs.documents.read",
|
|
1763
|
+
(action, payload) => docsEvidence(action, payload, "document_read"),
|
|
1764
|
+
],
|
|
1765
|
+
["mcp.tool.call", (action, payload) => mcpEvidence(action, payload)],
|
|
1766
|
+
[
|
|
1767
|
+
"aws.identity.get",
|
|
1768
|
+
(action, payload) => awsEvidence(action, payload, "identity_get"),
|
|
1769
|
+
],
|
|
1770
|
+
[
|
|
1771
|
+
"aws.s3.objects.list",
|
|
1772
|
+
(action, payload) => awsEvidence(action, payload, "s3_objects_list"),
|
|
1773
|
+
],
|
|
1774
|
+
[
|
|
1775
|
+
"gcp.projects.get",
|
|
1776
|
+
(action, payload) => gcpEvidence(action, payload, "project_get"),
|
|
1777
|
+
],
|
|
1778
|
+
[
|
|
1779
|
+
"gcp.storage.objects.list",
|
|
1780
|
+
(action, payload) => gcpEvidence(action, payload, "storage_objects_list"),
|
|
1781
|
+
],
|
|
1782
|
+
...NATIVE_CONNECTOR_GOVERNANCE_REGISTRY.flatMap((descriptor) => descriptor.tools.map((tool) => [
|
|
1783
|
+
tool.capability.tool,
|
|
1784
|
+
tool.normalizeEvidence,
|
|
1785
|
+
])),
|
|
1786
|
+
]);
|
|
597
1787
|
export function providerEvidenceForAction(actionInput) {
|
|
598
1788
|
const action = parseNormalizedAction(actionInput);
|
|
599
|
-
|
|
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
|
-
}
|
|
1789
|
+
return providerEvidenceNormalizers.get(action.capability.tool)?.(action, action.capability.payload);
|
|
640
1790
|
}
|
|
641
1791
|
export function hashJson(value) {
|
|
642
1792
|
return `sha256:${createHash("sha256")
|
|
@@ -659,15 +1809,33 @@ function validatePassBinding(claims, action, payloadHash) {
|
|
|
659
1809
|
if (claims.intent.taskId !== action.intent.taskId) {
|
|
660
1810
|
return "task_id_mismatch";
|
|
661
1811
|
}
|
|
1812
|
+
if (stableStringify(claims.intent) !== stableStringify(action.intent)) {
|
|
1813
|
+
return "intent_mismatch";
|
|
1814
|
+
}
|
|
662
1815
|
if (claims.capability.tool !== action.capability.tool) {
|
|
663
1816
|
return "tool_mismatch";
|
|
664
1817
|
}
|
|
665
1818
|
if (claims.capability.resource !== action.capability.resource) {
|
|
666
1819
|
return "resource_mismatch";
|
|
667
1820
|
}
|
|
1821
|
+
if (stableStringify(claims.capability.constraints) !==
|
|
1822
|
+
stableStringify(action.capability.constraints)) {
|
|
1823
|
+
return "constraints_mismatch";
|
|
1824
|
+
}
|
|
1825
|
+
if (stableStringify(claims.budget) !== stableStringify(action.budget)) {
|
|
1826
|
+
return "budget_mismatch";
|
|
1827
|
+
}
|
|
668
1828
|
if (claims.payloadHash !== payloadHash) {
|
|
669
1829
|
return "payload_hash_mismatch";
|
|
670
1830
|
}
|
|
1831
|
+
const provenanceHash = action.provenance
|
|
1832
|
+
? hashProvenance(action.provenance)
|
|
1833
|
+
: undefined;
|
|
1834
|
+
if (claims.provenanceHash !== provenanceHash) {
|
|
1835
|
+
return action.provenance
|
|
1836
|
+
? "provenance_hash_mismatch"
|
|
1837
|
+
: "unexpected_provenance_hash";
|
|
1838
|
+
}
|
|
671
1839
|
if (claims.approval?.payloadHash && claims.approval.payloadHash !== payloadHash) {
|
|
672
1840
|
return "approval_payload_hash_mismatch";
|
|
673
1841
|
}
|
|
@@ -676,6 +1844,100 @@ function validatePassBinding(claims, action, payloadHash) {
|
|
|
676
1844
|
}
|
|
677
1845
|
return null;
|
|
678
1846
|
}
|
|
1847
|
+
async function validateDpopProof(input, claims) {
|
|
1848
|
+
if (!input.dpopProof)
|
|
1849
|
+
return "dpop_proof_required";
|
|
1850
|
+
if (!input.dpopMethod)
|
|
1851
|
+
return "dpop_method_required";
|
|
1852
|
+
if (!input.dpopUri)
|
|
1853
|
+
return "dpop_uri_required";
|
|
1854
|
+
try {
|
|
1855
|
+
const header = decodeProtectedHeader(input.dpopProof);
|
|
1856
|
+
if (header.typ !== DPOP_PROOF_TYP)
|
|
1857
|
+
return "dpop_type_mismatch";
|
|
1858
|
+
if (!header.jwk || typeof header.jwk !== "object") {
|
|
1859
|
+
return "dpop_public_jwk_required";
|
|
1860
|
+
}
|
|
1861
|
+
const proofJwk = publicOnlyJwk(header.jwk);
|
|
1862
|
+
const algorithm = header.alg;
|
|
1863
|
+
const algorithms = input.dpopAlgorithms ?? [DEFAULT_SIGNING_ALGORITHM];
|
|
1864
|
+
if (!algorithms.includes(algorithm))
|
|
1865
|
+
return "dpop_algorithm_not_allowed";
|
|
1866
|
+
const key = await importJWK(proofJwk, algorithm);
|
|
1867
|
+
const { payload } = await jwtVerify(input.dpopProof, key, {
|
|
1868
|
+
algorithms,
|
|
1869
|
+
currentDate: input.currentDate,
|
|
1870
|
+
});
|
|
1871
|
+
const proof = DpopProofClaimsSchema.parse(payload);
|
|
1872
|
+
const proofJkt = await calculateJwkThumbprint(proofJwk, "sha256");
|
|
1873
|
+
if (proofJkt !== claims.cnf.jkt)
|
|
1874
|
+
return "dpop_key_mismatch";
|
|
1875
|
+
if (proof.htm !== normalizeDpopMethod(input.dpopMethod)) {
|
|
1876
|
+
return "dpop_method_mismatch";
|
|
1877
|
+
}
|
|
1878
|
+
if (proof.htu !== normalizeDpopUri(input.dpopUri)) {
|
|
1879
|
+
return "dpop_uri_mismatch";
|
|
1880
|
+
}
|
|
1881
|
+
if (proof.ath !== computeDpopAccessTokenHash(input.token)) {
|
|
1882
|
+
return "dpop_actionpass_hash_mismatch";
|
|
1883
|
+
}
|
|
1884
|
+
if (input.dpopNonce !== undefined && proof.nonce !== input.dpopNonce) {
|
|
1885
|
+
return "dpop_nonce_mismatch";
|
|
1886
|
+
}
|
|
1887
|
+
const nowSeconds = toNumericDate(input.currentDate ?? new Date());
|
|
1888
|
+
const maxAge = input.dpopMaxAgeSeconds ?? DEFAULT_DPOP_MAX_AGE_SECONDS;
|
|
1889
|
+
const skew = input.dpopClockSkewSeconds ?? DEFAULT_DPOP_CLOCK_SKEW_SECONDS;
|
|
1890
|
+
if (proof.iat > nowSeconds + skew)
|
|
1891
|
+
return "dpop_proof_from_future";
|
|
1892
|
+
if (proof.iat < nowSeconds - maxAge - skew)
|
|
1893
|
+
return "dpop_proof_stale";
|
|
1894
|
+
if (!input.dpopReplayStore)
|
|
1895
|
+
return "dpop_replay_store_required";
|
|
1896
|
+
const accepted = await input.dpopReplayStore.consume(proof.jti, new Date((proof.iat + maxAge + skew) * 1000), input.currentDate);
|
|
1897
|
+
if (!accepted)
|
|
1898
|
+
return "dpop_replay_detected";
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
catch (error) {
|
|
1902
|
+
return error instanceof Error && error.message.startsWith("dpop_")
|
|
1903
|
+
? error.message
|
|
1904
|
+
: "dpop_proof_invalid";
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
export function computeDpopAccessTokenHash(token) {
|
|
1908
|
+
return createHash("sha256").update(token, "ascii").digest("base64url");
|
|
1909
|
+
}
|
|
1910
|
+
export function normalizeDpopMethod(method) {
|
|
1911
|
+
const normalized = method.trim().toUpperCase();
|
|
1912
|
+
if (!normalized)
|
|
1913
|
+
throw new Error("dpop_method_required");
|
|
1914
|
+
return normalized;
|
|
1915
|
+
}
|
|
1916
|
+
export function normalizeDpopUri(uri) {
|
|
1917
|
+
const parsed = new URL(uri);
|
|
1918
|
+
parsed.search = "";
|
|
1919
|
+
parsed.hash = "";
|
|
1920
|
+
return parsed.toString();
|
|
1921
|
+
}
|
|
1922
|
+
export function actionPassDpopTarget(actionInput) {
|
|
1923
|
+
const action = parseNormalizedAction(actionInput);
|
|
1924
|
+
return {
|
|
1925
|
+
method: "POST",
|
|
1926
|
+
uri: `https://local.axtary.dev/actions/${encodeURIComponent(action.capability.tool)}`,
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
function publicOnlyJwk(jwk) {
|
|
1930
|
+
if (!jwk.kty || jwk.kty === "oct") {
|
|
1931
|
+
throw new Error("dpop_public_jwk_invalid");
|
|
1932
|
+
}
|
|
1933
|
+
const publicJwk = { ...jwk };
|
|
1934
|
+
for (const member of ["d", "p", "q", "dp", "dq", "qi", "oth", "k"]) {
|
|
1935
|
+
if (member in publicJwk) {
|
|
1936
|
+
throw new Error("dpop_public_jwk_invalid");
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
return publicJwk;
|
|
1940
|
+
}
|
|
679
1941
|
async function readTrustStore(filePath) {
|
|
680
1942
|
const text = await readTrustStoreFile(filePath);
|
|
681
1943
|
if (!text.trim()) {
|
|
@@ -691,6 +1953,7 @@ async function writeTrustStore(filePath, data) {
|
|
|
691
1953
|
await writeFile(tmpPath, `${JSON.stringify(data, null, 2)}\n`, {
|
|
692
1954
|
encoding: "utf8",
|
|
693
1955
|
flag: "wx",
|
|
1956
|
+
mode: 0o600,
|
|
694
1957
|
});
|
|
695
1958
|
await rename(tmpPath, filePath);
|
|
696
1959
|
}
|
|
@@ -752,7 +2015,13 @@ async function validateRevocation(input, claims) {
|
|
|
752
2015
|
}
|
|
753
2016
|
}
|
|
754
2017
|
if (input.isRevoked) {
|
|
755
|
-
|
|
2018
|
+
let result;
|
|
2019
|
+
try {
|
|
2020
|
+
result = await input.isRevoked(claims);
|
|
2021
|
+
}
|
|
2022
|
+
catch {
|
|
2023
|
+
return "actionpass_revocation_check_failed";
|
|
2024
|
+
}
|
|
756
2025
|
if (result === true) {
|
|
757
2026
|
return "actionpass_revoked";
|
|
758
2027
|
}
|
|
@@ -762,6 +2031,27 @@ async function validateRevocation(input, claims) {
|
|
|
762
2031
|
}
|
|
763
2032
|
return null;
|
|
764
2033
|
}
|
|
2034
|
+
async function validateDistributedStatus(input, claims) {
|
|
2035
|
+
if (!input.getStatus) {
|
|
2036
|
+
return "actionpass_status_check_required";
|
|
2037
|
+
}
|
|
2038
|
+
let status;
|
|
2039
|
+
try {
|
|
2040
|
+
status = await input.getStatus(claims);
|
|
2041
|
+
}
|
|
2042
|
+
catch {
|
|
2043
|
+
return "actionpass_status_check_failed";
|
|
2044
|
+
}
|
|
2045
|
+
if (status === 0)
|
|
2046
|
+
return null;
|
|
2047
|
+
if (status === 1)
|
|
2048
|
+
return "actionpass_revoked";
|
|
2049
|
+
if (status === 2)
|
|
2050
|
+
return "actionpass_suspended";
|
|
2051
|
+
if (typeof status === "string" && status.length > 0)
|
|
2052
|
+
return status;
|
|
2053
|
+
return "actionpass_status_invalid";
|
|
2054
|
+
}
|
|
765
2055
|
function bindApproval(input) {
|
|
766
2056
|
const approval = input.approval
|
|
767
2057
|
? ApprovalSchema.parse(input.approval)
|
|
@@ -803,6 +2093,7 @@ function bindApproval(input) {
|
|
|
803
2093
|
return ApprovalSchema.parse({
|
|
804
2094
|
mode: approval?.mode ?? artifact.mode,
|
|
805
2095
|
approvedBy: approval?.approvedBy ?? artifact.approvedBy,
|
|
2096
|
+
approverRoles: approval?.approverRoles ?? artifact.approverRoles,
|
|
806
2097
|
approvedAt: approval?.approvedAt ?? artifact.approvedAt,
|
|
807
2098
|
approvalArtifact: approval?.approvalArtifact ?? artifact.id,
|
|
808
2099
|
approvalArtifactHash: artifactHash,
|
|
@@ -913,6 +2204,103 @@ function githubBranchEvidence(action, payload) {
|
|
|
913
2204
|
}),
|
|
914
2205
|
});
|
|
915
2206
|
}
|
|
2207
|
+
function githubReviewCommentEvidence(action, payload) {
|
|
2208
|
+
const path = getString(payload.path) ?? "unknown";
|
|
2209
|
+
const pullNumber = getNumber(payload.pullNumber);
|
|
2210
|
+
const line = getNumber(payload.line);
|
|
2211
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2212
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2213
|
+
provider: "github",
|
|
2214
|
+
tool: action.capability.tool,
|
|
2215
|
+
operation: "pull_request_review_comment_create",
|
|
2216
|
+
resource: providerResource("github", "pull_request_line", `${action.capability.resource}:pull:${pullNumber ?? "unknown"}:${path}:${line ?? "unknown"}`),
|
|
2217
|
+
normalized: compactJsonRecord({
|
|
2218
|
+
pullNumber,
|
|
2219
|
+
commitId: getString(payload.commitId),
|
|
2220
|
+
path,
|
|
2221
|
+
line,
|
|
2222
|
+
side: getString(payload.side),
|
|
2223
|
+
startLine: getNumber(payload.startLine),
|
|
2224
|
+
startSide: getString(payload.startSide),
|
|
2225
|
+
body: redactCredentialLikeText(getString(payload.body) ?? ""),
|
|
2226
|
+
}),
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
function githubCheckRunEvidence(action, payload) {
|
|
2230
|
+
const name = getString(payload.name) ?? "unknown";
|
|
2231
|
+
const headSha = getString(payload.headSha) ?? "unknown";
|
|
2232
|
+
const output = isJsonObject(payload.output) ? payload.output : {};
|
|
2233
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2234
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2235
|
+
provider: "github",
|
|
2236
|
+
tool: action.capability.tool,
|
|
2237
|
+
operation: "check_run_create",
|
|
2238
|
+
resource: providerResource("github", "commit", `${action.capability.resource}:${headSha}`, name),
|
|
2239
|
+
normalized: compactJsonRecord({
|
|
2240
|
+
name,
|
|
2241
|
+
headSha,
|
|
2242
|
+
status: getString(payload.status),
|
|
2243
|
+
conclusion: getString(payload.conclusion),
|
|
2244
|
+
detailsUrl: getString(payload.detailsUrl),
|
|
2245
|
+
externalId: getString(payload.externalId),
|
|
2246
|
+
outputTitle: getString(output.title),
|
|
2247
|
+
outputSummary: redactCredentialLikeText(getString(output.summary) ?? ""),
|
|
2248
|
+
}),
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
function githubIssueCommentEvidence(action, payload) {
|
|
2252
|
+
const issueNumber = getNumber(payload.issueNumber);
|
|
2253
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2254
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2255
|
+
provider: "github",
|
|
2256
|
+
tool: action.capability.tool,
|
|
2257
|
+
operation: "issue_comment_create",
|
|
2258
|
+
resource: providerResource("github", "issue", `${action.capability.resource}:issue:${issueNumber ?? "unknown"}`),
|
|
2259
|
+
normalized: compactJsonRecord({
|
|
2260
|
+
issueNumber,
|
|
2261
|
+
body: redactCredentialLikeText(getString(payload.body) ?? ""),
|
|
2262
|
+
}),
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
function postgresSelectEvidence(action, payload) {
|
|
2266
|
+
const statement = typeof payload.statement === "string" ? payload.statement : "";
|
|
2267
|
+
const analysis = analyzePostgresSelect(statement);
|
|
2268
|
+
const parameters = Array.isArray(payload.parameters) ? payload.parameters : [];
|
|
2269
|
+
const maxRows = typeof payload.maxRows === "number" && Number.isInteger(payload.maxRows)
|
|
2270
|
+
? payload.maxRows
|
|
2271
|
+
: null;
|
|
2272
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2273
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2274
|
+
provider: "postgres",
|
|
2275
|
+
tool: action.capability.tool,
|
|
2276
|
+
operation: "query_select",
|
|
2277
|
+
resource: providerResource("postgres", "database", action.capability.resource),
|
|
2278
|
+
normalized: {
|
|
2279
|
+
statementHash: analysis.ok ? analysis.analysis.statementHash : null,
|
|
2280
|
+
predicateHash: analysis.ok ? analysis.analysis.predicateHash : null,
|
|
2281
|
+
tables: analysis.ok ? analysis.analysis.tables : [],
|
|
2282
|
+
functions: analysis.ok ? analysis.analysis.functions : [],
|
|
2283
|
+
parameterCount: analysis.ok ? analysis.analysis.parameterCount : 0,
|
|
2284
|
+
parameterHash: hashPayload(parameters),
|
|
2285
|
+
maxRows,
|
|
2286
|
+
},
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
function driveFileReadEvidence(action, payload) {
|
|
2290
|
+
const fileId = getString(payload.fileId) ?? "unknown";
|
|
2291
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2292
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2293
|
+
provider: "drive",
|
|
2294
|
+
tool: action.capability.tool,
|
|
2295
|
+
operation: "file_read",
|
|
2296
|
+
resource: providerResource("drive", "file", `drive:file:${fileId}`, fileId),
|
|
2297
|
+
normalized: compactJsonRecord({
|
|
2298
|
+
fileId,
|
|
2299
|
+
scope: "https://www.googleapis.com/auth/drive.file",
|
|
2300
|
+
maxBytes: getNumber(payload.maxBytes),
|
|
2301
|
+
}),
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
916
2304
|
function slackMessageEvidence(action, payload) {
|
|
917
2305
|
const channel = getString(payload.channel) ?? "unknown";
|
|
918
2306
|
const message = getString(payload.text) ?? getString(payload.message) ?? "";
|
|
@@ -937,9 +2325,6 @@ function slackMessageEvidence(action, payload) {
|
|
|
937
2325
|
}),
|
|
938
2326
|
});
|
|
939
2327
|
}
|
|
940
|
-
function linearIssueEvidence(action, payload, operation) {
|
|
941
|
-
return issueTrackerEvidence(action, payload, "linear", operation);
|
|
942
|
-
}
|
|
943
2328
|
function issueTrackerEvidence(action, payload, provider, operation) {
|
|
944
2329
|
const issueKey = getString(payload.issueKey) ?? getString(payload.issueId) ?? "unknown";
|
|
945
2330
|
const projectKey = getString(payload.projectKey) ?? issueKey.split("-", 1)[0];
|
|
@@ -956,11 +2341,12 @@ function issueTrackerEvidence(action, payload, provider, operation) {
|
|
|
956
2341
|
status: getString(payload.status),
|
|
957
2342
|
stateId: getString(payload.stateId),
|
|
958
2343
|
priority: getNumber(payload.priority),
|
|
2344
|
+
priorityId: getString(payload.priorityId),
|
|
959
2345
|
commentBody: operation === "comment_create"
|
|
960
2346
|
? redactCredentialLikeText(getString(payload.body) ?? "")
|
|
961
2347
|
: null,
|
|
962
2348
|
}),
|
|
963
|
-
fieldChanges:
|
|
2349
|
+
fieldChanges: issueTrackerFieldChanges(payload),
|
|
964
2350
|
});
|
|
965
2351
|
}
|
|
966
2352
|
function docsEvidence(action, payload, operation) {
|
|
@@ -1089,13 +2475,14 @@ function changedFilesFromPayload(payload) {
|
|
|
1089
2475
|
.filter((path) => Boolean(path));
|
|
1090
2476
|
return [...new Set([...getStringArray(payload.filesChanged), ...changes])];
|
|
1091
2477
|
}
|
|
1092
|
-
function
|
|
2478
|
+
function issueTrackerFieldChanges(payload) {
|
|
1093
2479
|
return [
|
|
1094
2480
|
fieldChange("title", payload.fromTitle, payload.title),
|
|
1095
2481
|
fieldChange("description", payload.fromDescription, payload.description),
|
|
1096
2482
|
fieldChange("status", payload.fromStatus, payload.status),
|
|
1097
2483
|
fieldChange("stateId", payload.fromStateId, payload.stateId),
|
|
1098
2484
|
fieldChange("priority", payload.fromPriority, payload.priority),
|
|
2485
|
+
fieldChange("priorityId", payload.fromPriorityId, payload.priorityId),
|
|
1099
2486
|
].filter((change) => change !== null);
|
|
1100
2487
|
}
|
|
1101
2488
|
function fieldChange(field, before, after) {
|
|
@@ -1187,7 +2574,9 @@ function canonicalize(value) {
|
|
|
1187
2574
|
if (isPlainObject(value)) {
|
|
1188
2575
|
return Object.fromEntries(Object.entries(value)
|
|
1189
2576
|
.filter(([, entryValue]) => entryValue !== undefined)
|
|
1190
|
-
|
|
2577
|
+
// UTF-16 code unit order per RFC 8785 (JCS); locale-sensitive sorts
|
|
2578
|
+
// would make hashes non-portable across environments.
|
|
2579
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
1191
2580
|
.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
|
|
1192
2581
|
}
|
|
1193
2582
|
return value;
|
|
@@ -1201,4 +2590,262 @@ function isPlainObject(value) {
|
|
|
1201
2590
|
function isNodeError(error) {
|
|
1202
2591
|
return error instanceof Error && "code" in error;
|
|
1203
2592
|
}
|
|
2593
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2594
|
+
// SDK facade (PRD §8.2)
|
|
2595
|
+
//
|
|
2596
|
+
// The ergonomic developer entry point. It exposes the five PRD verbs —
|
|
2597
|
+
// `authorize`, `verify`, `record`, `revoke`, `explain` — over a flat request
|
|
2598
|
+
// shape and delegates to the tested core functions above. It adds no new
|
|
2599
|
+
// authorization logic and changes no token/ledger/policy shape; it only maps the
|
|
2600
|
+
// flat request to a NormalizedAction, threads a local hash-linked ledger head,
|
|
2601
|
+
// and promotes the deterministic reason prose (`explainDecision`).
|
|
2602
|
+
//
|
|
2603
|
+
// Keys: if no signing key is configured, the facade resolves a persistent dev
|
|
2604
|
+
// keypair (`devKeypair()` — env-backed or a 0600 local keyring file) so the
|
|
2605
|
+
// quickstart runs with zero key plumbing and its passes verify across process
|
|
2606
|
+
// restarts. Configure { signingKey, verificationKey } (or a hosted issuer) for
|
|
2607
|
+
// production.
|
|
2608
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2609
|
+
export const AXTARY_DEFAULT_ISSUER = "axtary-dev";
|
|
2610
|
+
export const AXTARY_DEFAULT_TENANT = "local";
|
|
2611
|
+
export const AXTARY_DEFAULT_RUNTIME = "sdk";
|
|
2612
|
+
function isNormalizedActionShape(value) {
|
|
2613
|
+
return (typeof value === "object" &&
|
|
2614
|
+
value !== null &&
|
|
2615
|
+
"actor" in value &&
|
|
2616
|
+
"capability" in value &&
|
|
2617
|
+
"intent" in value);
|
|
2618
|
+
}
|
|
2619
|
+
function isAuthorizeResultShape(value) {
|
|
2620
|
+
return (typeof value === "object" &&
|
|
2621
|
+
value !== null &&
|
|
2622
|
+
"action" in value &&
|
|
2623
|
+
"decision" in value &&
|
|
2624
|
+
typeof value.decision === "object");
|
|
2625
|
+
}
|
|
2626
|
+
function isAxtaryPassShape(value) {
|
|
2627
|
+
return (typeof value === "object" &&
|
|
2628
|
+
value !== null &&
|
|
2629
|
+
"token" in value &&
|
|
2630
|
+
"claims" in value);
|
|
2631
|
+
}
|
|
2632
|
+
export class Axtary {
|
|
2633
|
+
issuer;
|
|
2634
|
+
tenant;
|
|
2635
|
+
runtime;
|
|
2636
|
+
defaultPolicy;
|
|
2637
|
+
algorithm;
|
|
2638
|
+
quiet;
|
|
2639
|
+
configuredSigningKey;
|
|
2640
|
+
configuredKeyId;
|
|
2641
|
+
configuredVerificationKey;
|
|
2642
|
+
keyPath;
|
|
2643
|
+
signerPromise;
|
|
2644
|
+
ledgerHead = null;
|
|
2645
|
+
revocations = new Map();
|
|
2646
|
+
constructor(config = {}) {
|
|
2647
|
+
this.issuer = config.issuer ?? AXTARY_DEFAULT_ISSUER;
|
|
2648
|
+
this.tenant = config.tenant ?? AXTARY_DEFAULT_TENANT;
|
|
2649
|
+
this.runtime = config.runtime ?? AXTARY_DEFAULT_RUNTIME;
|
|
2650
|
+
this.defaultPolicy = config.policy;
|
|
2651
|
+
this.algorithm = config.algorithm ?? DEFAULT_SIGNING_ALGORITHM;
|
|
2652
|
+
this.quiet = config.quiet ?? false;
|
|
2653
|
+
this.configuredSigningKey = config.signingKey;
|
|
2654
|
+
this.configuredKeyId = config.signingKeyId;
|
|
2655
|
+
this.configuredVerificationKey = config.verificationKey;
|
|
2656
|
+
this.keyPath = config.keyPath;
|
|
2657
|
+
}
|
|
2658
|
+
/** Current local ledger head (hash of the last record this instance wrote). */
|
|
2659
|
+
get ledgerHash() {
|
|
2660
|
+
return this.ledgerHead;
|
|
2661
|
+
}
|
|
2662
|
+
async resolveSigner() {
|
|
2663
|
+
if (!this.signerPromise) {
|
|
2664
|
+
this.signerPromise = (async () => {
|
|
2665
|
+
if (this.configuredSigningKey) {
|
|
2666
|
+
return {
|
|
2667
|
+
signingKey: this.configuredSigningKey,
|
|
2668
|
+
keyId: this.configuredKeyId,
|
|
2669
|
+
verificationKey: this.configuredVerificationKey,
|
|
2670
|
+
algorithm: this.algorithm,
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
const dev = await devKeypair({ path: this.keyPath });
|
|
2674
|
+
if (!this.quiet) {
|
|
2675
|
+
const origin = dev.source === "env"
|
|
2676
|
+
? `from ${AXTARY_DEV_SIGNING_KEY_ENV}`
|
|
2677
|
+
: `at ${dev.path}`;
|
|
2678
|
+
console.warn("[axtary] No signing key configured — issuing ActionPasses with a " +
|
|
2679
|
+
`persistent local dev key (${origin}). These passes verify across ` +
|
|
2680
|
+
"process restarts on this machine. Pass { signingKey, " +
|
|
2681
|
+
"verificationKey } to createAxtary() (or use a hosted issuer) for " +
|
|
2682
|
+
"production.");
|
|
2683
|
+
}
|
|
2684
|
+
return {
|
|
2685
|
+
signingKey: dev.signingKey,
|
|
2686
|
+
keyId: dev.signingKeyId,
|
|
2687
|
+
verificationKey: dev.verificationKey,
|
|
2688
|
+
algorithm: dev.algorithm,
|
|
2689
|
+
};
|
|
2690
|
+
})();
|
|
2691
|
+
}
|
|
2692
|
+
return this.signerPromise;
|
|
2693
|
+
}
|
|
2694
|
+
toNormalizedAction(request) {
|
|
2695
|
+
if (isNormalizedActionShape(request)) {
|
|
2696
|
+
return parseNormalizedAction(request);
|
|
2697
|
+
}
|
|
2698
|
+
return parseNormalizedAction({
|
|
2699
|
+
schemaVersion: ACTION_SCHEMA_VERSION,
|
|
2700
|
+
actor: {
|
|
2701
|
+
agentId: request.agent,
|
|
2702
|
+
humanOwner: request.human,
|
|
2703
|
+
runtime: request.runtime ?? this.runtime,
|
|
2704
|
+
tenant: request.tenant ?? this.tenant,
|
|
2705
|
+
},
|
|
2706
|
+
intent: {
|
|
2707
|
+
taskId: request.task ?? `task_${randomUUID()}`,
|
|
2708
|
+
declaredGoal: request.intent,
|
|
2709
|
+
maxDelegationDepth: request.maxDelegationDepth,
|
|
2710
|
+
},
|
|
2711
|
+
capability: {
|
|
2712
|
+
tool: request.tool,
|
|
2713
|
+
resource: request.resource,
|
|
2714
|
+
payload: request.payload ?? {},
|
|
2715
|
+
constraints: request.constraints,
|
|
2716
|
+
},
|
|
2717
|
+
toolDefinition: request.toolDefinition,
|
|
2718
|
+
provenance: request.provenance,
|
|
2719
|
+
budget: request.budget,
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Decide an action and, for an allow, issue an ActionPass. Always returns a
|
|
2724
|
+
* decision (the policy evaluation is keyless); a deny/step-up returns
|
|
2725
|
+
* `pass: null`. Threads the local ledger head so successive calls chain.
|
|
2726
|
+
*/
|
|
2727
|
+
async authorize(request, options = {}) {
|
|
2728
|
+
const signer = await this.resolveSigner();
|
|
2729
|
+
const action = this.toNormalizedAction(request);
|
|
2730
|
+
const result = await authorize({
|
|
2731
|
+
action,
|
|
2732
|
+
issuer: this.issuer,
|
|
2733
|
+
tenant: this.tenant,
|
|
2734
|
+
signingKey: signer.signingKey,
|
|
2735
|
+
keyId: signer.keyId,
|
|
2736
|
+
algorithm: signer.algorithm,
|
|
2737
|
+
policy: options.policy ?? this.defaultPolicy,
|
|
2738
|
+
approval: options.approval,
|
|
2739
|
+
approvalArtifact: options.approvalArtifact,
|
|
2740
|
+
previousLedgerHash: this.ledgerHead,
|
|
2741
|
+
now: options.now,
|
|
2742
|
+
});
|
|
2743
|
+
this.ledgerHead = result.ledger.ledgerHash;
|
|
2744
|
+
return {
|
|
2745
|
+
status: result.decision.decision,
|
|
2746
|
+
decision: result.decision,
|
|
2747
|
+
reasons: result.decision.reasons,
|
|
2748
|
+
explanation: explainDecision(result.decision),
|
|
2749
|
+
payloadHash: result.payloadHash,
|
|
2750
|
+
action: result.action,
|
|
2751
|
+
pass: result.actionPass,
|
|
2752
|
+
ledger: result.ledger,
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Verify a previously issued pass against an action. Accepts an authorize
|
|
2757
|
+
* result, a pass object, or a raw token; the action defaults to the result's
|
|
2758
|
+
* action when one is supplied. Fails closed for a pass this instance revoked.
|
|
2759
|
+
*/
|
|
2760
|
+
async verify(pass, action) {
|
|
2761
|
+
let token;
|
|
2762
|
+
let actionInput = action;
|
|
2763
|
+
if (typeof pass === "string") {
|
|
2764
|
+
token = pass;
|
|
2765
|
+
}
|
|
2766
|
+
else if (isAuthorizeResultShape(pass)) {
|
|
2767
|
+
token = pass.pass?.token ?? null;
|
|
2768
|
+
actionInput = actionInput ?? pass.action;
|
|
2769
|
+
}
|
|
2770
|
+
else if (isAxtaryPassShape(pass)) {
|
|
2771
|
+
token = pass.token;
|
|
2772
|
+
}
|
|
2773
|
+
else {
|
|
2774
|
+
token = null;
|
|
2775
|
+
}
|
|
2776
|
+
if (!token) {
|
|
2777
|
+
return { valid: false, reason: "actionpass_not_issued" };
|
|
2778
|
+
}
|
|
2779
|
+
if (!actionInput) {
|
|
2780
|
+
return { valid: false, reason: "verify_requires_action" };
|
|
2781
|
+
}
|
|
2782
|
+
const signer = await this.resolveSigner();
|
|
2783
|
+
if (!signer.verificationKey) {
|
|
2784
|
+
return { valid: false, reason: "verification_key_required" };
|
|
2785
|
+
}
|
|
2786
|
+
return verifyActionPass({
|
|
2787
|
+
token,
|
|
2788
|
+
action: this.toNormalizedAction(actionInput),
|
|
2789
|
+
verificationKey: signer.verificationKey,
|
|
2790
|
+
issuer: this.issuer,
|
|
2791
|
+
algorithms: [signer.algorithm],
|
|
2792
|
+
revocations: this.revocations.values(),
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Append an execution result to the local ledger, chained after the decision
|
|
2797
|
+
* record from `authorize`. Returns the tamper-evident record; durable
|
|
2798
|
+
* persistence is the proxy/CLI's responsibility.
|
|
2799
|
+
*/
|
|
2800
|
+
record(input) {
|
|
2801
|
+
const action = this.toNormalizedAction(input.action);
|
|
2802
|
+
const passId = typeof input.pass === "string"
|
|
2803
|
+
? input.pass
|
|
2804
|
+
: (input.pass?.claims.jti ?? null);
|
|
2805
|
+
const executionOutcome = input.outcome
|
|
2806
|
+
? LedgerExecutionOutcomeSchema.parse({
|
|
2807
|
+
schemaVersion: LEDGER_EXECUTION_OUTCOME_VERSION,
|
|
2808
|
+
...input.outcome,
|
|
2809
|
+
})
|
|
2810
|
+
: undefined;
|
|
2811
|
+
const record = recordDecision({
|
|
2812
|
+
action,
|
|
2813
|
+
decision: input.decision,
|
|
2814
|
+
actionPassId: passId,
|
|
2815
|
+
executionOutcome,
|
|
2816
|
+
previousLedgerHash: input.previousLedgerHash ?? this.ledgerHead,
|
|
2817
|
+
correlationId: input.correlationId ?? passId,
|
|
2818
|
+
});
|
|
2819
|
+
this.ledgerHead = record.ledgerHash;
|
|
2820
|
+
return record;
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Revoke a pass by id. The revocation is held in this instance so a later
|
|
2824
|
+
* `verify` of that pass fails closed. Durable cross-process revocation is the
|
|
2825
|
+
* CLI/trust-store path (`axtary revoke`).
|
|
2826
|
+
*/
|
|
2827
|
+
revoke(passId, options = {}) {
|
|
2828
|
+
const revocation = revokeActionPass({
|
|
2829
|
+
passId,
|
|
2830
|
+
revokedBy: options.revokedBy,
|
|
2831
|
+
reason: options.reason,
|
|
2832
|
+
revokedAt: options.revokedAt,
|
|
2833
|
+
});
|
|
2834
|
+
this.revocations.set(passId, revocation);
|
|
2835
|
+
return revocation;
|
|
2836
|
+
}
|
|
2837
|
+
/** Deterministic, human-readable explanation of a decision (no model call). */
|
|
2838
|
+
explain(decision) {
|
|
2839
|
+
const policyDecision = isAuthorizeResultShape(decision)
|
|
2840
|
+
? decision.decision
|
|
2841
|
+
: decision;
|
|
2842
|
+
return explainDecision(policyDecision);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
/** Create an Axtary SDK instance. */
|
|
2846
|
+
export function createAxtary(config = {}) {
|
|
2847
|
+
return new Axtary(config);
|
|
2848
|
+
}
|
|
2849
|
+
/** Default Axtary SDK instance (PRD §8.2 `import { axtary }`). */
|
|
2850
|
+
export const axtary = createAxtary();
|
|
1204
2851
|
//# sourceMappingURL=index.js.map
|