@axtary/actionpass 0.0.1 → 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/LICENSE +202 -0
- package/README.md +134 -21
- 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 +1670 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2851 -0
- package/dist/index.js.map +1 -0
- 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 +18 -6
- package/src/index.ts +0 -552
package/dist/index.js
ADDED
|
@@ -0,0 +1,2851 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { lock } from "proper-lockfile";
|
|
5
|
+
import { decodeProtectedHeader, calculateJwkThumbprint, importJWK, SignJWT, jwtVerify, } from "jose";
|
|
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";
|
|
18
|
+
export const ACTION_SCHEMA_VERSION = "axtary.action.v0";
|
|
19
|
+
export const ACTIONPASS_V0_VERSION = "axtary.actionpass.v0";
|
|
20
|
+
export const ACTIONPASS_V1_VERSION = "axtary.actionpass.v1";
|
|
21
|
+
export const ACTIONPASS_V2_VERSION = "axtary.actionpass.v2";
|
|
22
|
+
export const ACTIONPASS_VERSION = ACTIONPASS_V0_VERSION;
|
|
23
|
+
export const APPROVAL_ARTIFACT_VERSION = "axtary.approval.v0";
|
|
24
|
+
export const ACTIONPASS_REVOCATION_VERSION = "axtary.actionpass_revocation.v0";
|
|
25
|
+
export const ACTIONPASS_TRUST_STORE_VERSION = "axtary.actionpass_trust_store.v0";
|
|
26
|
+
export const LEDGER_SCHEMA_VERSION = "axtary.ledger.v0";
|
|
27
|
+
export const LEDGER_PROVIDER_EVIDENCE_VERSION = "axtary.ledger_provider_evidence.v0";
|
|
28
|
+
export const LEDGER_EXECUTION_OUTCOME_VERSION = "axtary.ledger_execution_outcome.v0";
|
|
29
|
+
export const DEFAULT_EXPIRES_IN_SECONDS = 600;
|
|
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;
|
|
34
|
+
const JsonValueSchema = z.lazy(() => z.union([
|
|
35
|
+
z.string(),
|
|
36
|
+
z.number().finite(),
|
|
37
|
+
z.boolean(),
|
|
38
|
+
z.null(),
|
|
39
|
+
z.array(JsonValueSchema),
|
|
40
|
+
z.record(z.string(), JsonValueSchema),
|
|
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
|
+
});
|
|
49
|
+
export const AxtaryDecisionSchema = z.enum(["allow", "deny", "step_up"]);
|
|
50
|
+
export const NormalizedActionSchema = z.object({
|
|
51
|
+
schemaVersion: z.literal(ACTION_SCHEMA_VERSION).default(ACTION_SCHEMA_VERSION),
|
|
52
|
+
actor: z.object({
|
|
53
|
+
agentId: z.string().min(1),
|
|
54
|
+
humanOwner: z.string().min(1),
|
|
55
|
+
runtime: z.string().min(1),
|
|
56
|
+
tenant: z.string().min(1).optional(),
|
|
57
|
+
}),
|
|
58
|
+
intent: z.object({
|
|
59
|
+
taskId: z.string().min(1),
|
|
60
|
+
declaredGoal: z.string().min(1),
|
|
61
|
+
maxDelegationDepth: z.number().int().nonnegative().optional(),
|
|
62
|
+
}),
|
|
63
|
+
capability: z.object({
|
|
64
|
+
tool: z.string().min(1),
|
|
65
|
+
resource: z.string().min(1),
|
|
66
|
+
payload: z.record(z.string(), JsonValueSchema),
|
|
67
|
+
constraints: z.record(z.string(), JsonValueSchema).optional(),
|
|
68
|
+
}),
|
|
69
|
+
toolDefinition: z
|
|
70
|
+
.object({
|
|
71
|
+
serverIdentity: z.string().min(1),
|
|
72
|
+
schemaVersion: z.string().min(1),
|
|
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(),
|
|
82
|
+
})
|
|
83
|
+
.optional(),
|
|
84
|
+
provenance: ActionProvenanceSchema.optional(),
|
|
85
|
+
budget: ActionBudgetSchema.optional(),
|
|
86
|
+
});
|
|
87
|
+
export const PolicyDecisionSchema = z.object({
|
|
88
|
+
decision: AxtaryDecisionSchema,
|
|
89
|
+
reasons: z.array(z.string().min(1)),
|
|
90
|
+
policy: z.object({
|
|
91
|
+
nativeRule: z.string().min(1),
|
|
92
|
+
version: z.string().min(1),
|
|
93
|
+
cedarCompatible: z.boolean(),
|
|
94
|
+
opaCompatible: z.boolean(),
|
|
95
|
+
}),
|
|
96
|
+
constraints: z.object({
|
|
97
|
+
expiresInSeconds: z.number().int().positive(),
|
|
98
|
+
maxFilesChanged: z.number().int().nonnegative(),
|
|
99
|
+
blockedPaths: z.array(z.string().min(1)),
|
|
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(),
|
|
127
|
+
});
|
|
128
|
+
export const ApprovalSchema = z.object({
|
|
129
|
+
mode: z.enum(["none", "human", "policy_override"]),
|
|
130
|
+
approvedBy: z.string().min(1).optional(),
|
|
131
|
+
approverRoles: z.array(z.string().min(1)).optional(),
|
|
132
|
+
approvalArtifact: z.string().min(1).optional(),
|
|
133
|
+
approvalArtifactHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
|
|
134
|
+
actionHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
|
|
135
|
+
payloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
|
|
136
|
+
approvedAt: z.string().datetime().optional(),
|
|
137
|
+
});
|
|
138
|
+
export const ApprovalArtifactSchema = z.object({
|
|
139
|
+
schemaVersion: z.literal(APPROVAL_ARTIFACT_VERSION),
|
|
140
|
+
id: z.string().min(1),
|
|
141
|
+
mode: z.enum(["human", "policy_override"]),
|
|
142
|
+
approvedBy: z.string().min(1),
|
|
143
|
+
approverRoles: z.array(z.string().min(1)).optional(),
|
|
144
|
+
approvedAt: z.string().datetime(),
|
|
145
|
+
actionHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
146
|
+
payloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
147
|
+
taskId: z.string().min(1),
|
|
148
|
+
tool: z.string().min(1),
|
|
149
|
+
resource: z.string().min(1),
|
|
150
|
+
reason: z.string().min(1).optional(),
|
|
151
|
+
expiresAt: z.string().datetime().optional(),
|
|
152
|
+
});
|
|
153
|
+
const ActionPassBaseClaimsSchema = z.object({
|
|
154
|
+
iss: z.string().min(1),
|
|
155
|
+
sub: z.string().min(1),
|
|
156
|
+
aud: z.union([z.string().min(1), z.array(z.string().min(1))]),
|
|
157
|
+
exp: z.number().int().positive(),
|
|
158
|
+
nbf: z.number().int().positive(),
|
|
159
|
+
iat: z.number().int().positive(),
|
|
160
|
+
jti: z.string().min(1),
|
|
161
|
+
tenant: z.string().min(1),
|
|
162
|
+
humanOwner: z.string().min(1),
|
|
163
|
+
agentRuntime: z.string().min(1),
|
|
164
|
+
intent: NormalizedActionSchema.shape.intent,
|
|
165
|
+
capability: z.object({
|
|
166
|
+
tool: z.string().min(1),
|
|
167
|
+
resource: z.string().min(1),
|
|
168
|
+
constraints: z.record(z.string(), JsonValueSchema).optional(),
|
|
169
|
+
}),
|
|
170
|
+
decision: AxtaryDecisionSchema,
|
|
171
|
+
reasons: z.array(z.string().min(1)),
|
|
172
|
+
policy: PolicyDecisionSchema.shape.policy,
|
|
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(),
|
|
176
|
+
approval: ApprovalSchema.optional(),
|
|
177
|
+
audit: z.object({
|
|
178
|
+
traceId: z.string().min(1),
|
|
179
|
+
parentPassId: z.string().min(1).nullable(),
|
|
180
|
+
ledgerHash: z.string().min(1).nullable(),
|
|
181
|
+
}),
|
|
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
|
+
});
|
|
227
|
+
export const ActionPassRevocationSchema = z.object({
|
|
228
|
+
schemaVersion: z.literal(ACTIONPASS_REVOCATION_VERSION),
|
|
229
|
+
passId: z.string().min(1),
|
|
230
|
+
revokedAt: z.string().datetime(),
|
|
231
|
+
revokedBy: z.string().min(1).optional(),
|
|
232
|
+
reason: z.string().min(1).optional(),
|
|
233
|
+
});
|
|
234
|
+
export const ActionPassVerificationKeyRecordSchema = z.object({
|
|
235
|
+
kid: z.string().min(1),
|
|
236
|
+
publicJwk: z.record(z.string(), JsonValueSchema),
|
|
237
|
+
algorithm: z.string().min(1).optional(),
|
|
238
|
+
status: z.enum(["active", "retired", "revoked"]).default("active"),
|
|
239
|
+
notBefore: z.string().datetime().optional(),
|
|
240
|
+
expiresAt: z.string().datetime().optional(),
|
|
241
|
+
});
|
|
242
|
+
export const ActionPassTrustStoreSchema = z.object({
|
|
243
|
+
schemaVersion: z.literal(ACTIONPASS_TRUST_STORE_VERSION),
|
|
244
|
+
keys: z.array(ActionPassVerificationKeyRecordSchema).default([]),
|
|
245
|
+
revocations: z.array(ActionPassRevocationSchema).default([]),
|
|
246
|
+
});
|
|
247
|
+
export const LedgerProviderEvidenceDiffLineSchema = z.object({
|
|
248
|
+
type: z.enum(["context", "addition", "deletion"]),
|
|
249
|
+
content: z.string(),
|
|
250
|
+
oldLine: z.number().int().positive().optional(),
|
|
251
|
+
newLine: z.number().int().positive().optional(),
|
|
252
|
+
});
|
|
253
|
+
export const LedgerProviderEvidenceDiffFileSchema = z.object({
|
|
254
|
+
path: z.string().min(1),
|
|
255
|
+
oldPath: z.string().min(1).optional(),
|
|
256
|
+
status: z.enum(["added", "modified", "deleted", "renamed"]),
|
|
257
|
+
additions: z.number().int().nonnegative().default(0),
|
|
258
|
+
deletions: z.number().int().nonnegative().default(0),
|
|
259
|
+
hunks: z
|
|
260
|
+
.array(z.object({
|
|
261
|
+
header: z.string().min(1),
|
|
262
|
+
lines: z.array(LedgerProviderEvidenceDiffLineSchema),
|
|
263
|
+
}))
|
|
264
|
+
.default([]),
|
|
265
|
+
});
|
|
266
|
+
export const LedgerProviderSchema = z.enum([
|
|
267
|
+
"github",
|
|
268
|
+
"slack",
|
|
269
|
+
"linear",
|
|
270
|
+
"docs",
|
|
271
|
+
"drive",
|
|
272
|
+
"mcp",
|
|
273
|
+
"aws",
|
|
274
|
+
"gcp",
|
|
275
|
+
"jira",
|
|
276
|
+
"postgres",
|
|
277
|
+
]);
|
|
278
|
+
export const LedgerProviderEvidenceDiffSchema = z.object({
|
|
279
|
+
provider: LedgerProviderSchema,
|
|
280
|
+
summary: z.string().min(1).optional(),
|
|
281
|
+
files: z.array(LedgerProviderEvidenceDiffFileSchema).default([]),
|
|
282
|
+
});
|
|
283
|
+
export const LedgerProviderEvidenceFieldChangeSchema = z.object({
|
|
284
|
+
field: z.string().min(1),
|
|
285
|
+
before: JsonValueSchema.optional(),
|
|
286
|
+
after: JsonValueSchema.optional(),
|
|
287
|
+
});
|
|
288
|
+
export const LedgerProviderEvidenceSchema = z.object({
|
|
289
|
+
schemaVersion: z.literal(LEDGER_PROVIDER_EVIDENCE_VERSION),
|
|
290
|
+
provider: LedgerProviderSchema,
|
|
291
|
+
tool: z.string().min(1),
|
|
292
|
+
operation: z.string().min(1),
|
|
293
|
+
resource: z
|
|
294
|
+
.object({
|
|
295
|
+
provider: LedgerProviderSchema,
|
|
296
|
+
kind: z.string().min(1),
|
|
297
|
+
id: z.string().min(1),
|
|
298
|
+
label: z.string().min(1).optional(),
|
|
299
|
+
url: z.string().url().optional(),
|
|
300
|
+
})
|
|
301
|
+
.strict(),
|
|
302
|
+
normalized: z.record(z.string(), JsonValueSchema).default({}),
|
|
303
|
+
fieldChanges: z.array(LedgerProviderEvidenceFieldChangeSchema).default([]),
|
|
304
|
+
diff: LedgerProviderEvidenceDiffSchema.optional(),
|
|
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
|
+
};
|
|
417
|
+
export const LedgerExecutionOutcomeSchema = z
|
|
418
|
+
.object({
|
|
419
|
+
schemaVersion: z.literal(LEDGER_EXECUTION_OUTCOME_VERSION),
|
|
420
|
+
status: z.enum(["succeeded", "failed"]),
|
|
421
|
+
provider: LedgerProviderSchema.optional(),
|
|
422
|
+
resultId: z.string().min(1).optional(),
|
|
423
|
+
resultUrl: z.string().url().optional(),
|
|
424
|
+
resourceId: z.string().min(1).optional(),
|
|
425
|
+
resourceLabel: z.string().min(1).optional(),
|
|
426
|
+
failureReason: z.string().min(1).optional(),
|
|
427
|
+
errorClass: z.string().min(1).optional(),
|
|
428
|
+
sideEffectProof: z.record(z.string(), JsonValueSchema).default({}),
|
|
429
|
+
})
|
|
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
|
+
});
|
|
456
|
+
export const LedgerRecordSchema = z.object({
|
|
457
|
+
schemaVersion: z.literal(LEDGER_SCHEMA_VERSION),
|
|
458
|
+
id: z.string().min(1),
|
|
459
|
+
occurredAt: z.string().datetime(),
|
|
460
|
+
auditContext: LedgerAuditContextSchema.optional(),
|
|
461
|
+
actionHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
462
|
+
payloadHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
463
|
+
provenanceHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).optional(),
|
|
464
|
+
decision: AxtaryDecisionSchema,
|
|
465
|
+
reasons: z.array(z.string().min(1)),
|
|
466
|
+
policy: PolicyDecisionSchema.shape.policy,
|
|
467
|
+
providerEvidence: LedgerProviderEvidenceSchema.optional(),
|
|
468
|
+
executionOutcome: LedgerExecutionOutcomeSchema.optional(),
|
|
469
|
+
traceId: z.string().min(1).optional(),
|
|
470
|
+
actionPassId: z.string().min(1).nullable(),
|
|
471
|
+
delegation: DelegationLedgerEdgeSchema.optional(),
|
|
472
|
+
budget: BudgetLedgerEventSchema.optional(),
|
|
473
|
+
correlationId: z.string().min(1).optional(),
|
|
474
|
+
previousLedgerHash: z.string().regex(/^sha256:[a-f0-9]{64}$/).nullable(),
|
|
475
|
+
ledgerHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
476
|
+
});
|
|
477
|
+
export const demoAction = NormalizedActionSchema.parse({
|
|
478
|
+
actor: {
|
|
479
|
+
agentId: "agent:codex-prod",
|
|
480
|
+
humanOwner: "user:asrar@company.com",
|
|
481
|
+
runtime: "codex-cli",
|
|
482
|
+
tenant: "org:company",
|
|
483
|
+
},
|
|
484
|
+
intent: {
|
|
485
|
+
taskId: "AXT-418",
|
|
486
|
+
declaredGoal: "Open a PR that fixes the auth redirect bug",
|
|
487
|
+
},
|
|
488
|
+
capability: {
|
|
489
|
+
tool: "github.pull_requests.create",
|
|
490
|
+
resource: "repo:company/web-app",
|
|
491
|
+
payload: {
|
|
492
|
+
baseBranch: "main",
|
|
493
|
+
filesChanged: ["src/app/login/page.tsx", "src/lib/auth/session.ts"],
|
|
494
|
+
touchesProduction: false,
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
export function parseNormalizedAction(action) {
|
|
499
|
+
return NormalizedActionSchema.parse(action);
|
|
500
|
+
}
|
|
501
|
+
export function evaluateAction(actionInput, options = {}) {
|
|
502
|
+
const action = parseNormalizedAction(actionInput);
|
|
503
|
+
const payload = action.capability.payload;
|
|
504
|
+
const filesChanged = getStringArray(payload.filesChanged);
|
|
505
|
+
const requiredBaseBranch = options.requiredBaseBranch ?? "main";
|
|
506
|
+
const maxFilesChanged = options.maxFilesChanged ?? 12;
|
|
507
|
+
const blockedPathPrefixes = options.blockedPathPrefixes ?? [
|
|
508
|
+
"infra/prod/",
|
|
509
|
+
"billing/",
|
|
510
|
+
".env",
|
|
511
|
+
];
|
|
512
|
+
const policy = {
|
|
513
|
+
nativeRule: "github_pr_coding_agent_v0",
|
|
514
|
+
version: options.policyVersion ?? "2026-05-30",
|
|
515
|
+
cedarCompatible: true,
|
|
516
|
+
opaCompatible: true,
|
|
517
|
+
};
|
|
518
|
+
const constraints = {
|
|
519
|
+
expiresInSeconds: DEFAULT_EXPIRES_IN_SECONDS,
|
|
520
|
+
maxFilesChanged,
|
|
521
|
+
blockedPaths: ["infra/prod/**", "billing/**", ".env*"],
|
|
522
|
+
};
|
|
523
|
+
const stepUpReasons = [];
|
|
524
|
+
if (action.capability.tool !== (options.allowedTool ?? "github.pull_requests.create")) {
|
|
525
|
+
return {
|
|
526
|
+
decision: "deny",
|
|
527
|
+
reasons: [
|
|
528
|
+
`unsupported_tool: ${action.capability.tool} is not enabled by this policy`,
|
|
529
|
+
],
|
|
530
|
+
policy,
|
|
531
|
+
constraints,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (payload.baseBranch !== requiredBaseBranch) {
|
|
535
|
+
stepUpReasons.push(`base_branch_mismatch: expected ${requiredBaseBranch}`);
|
|
536
|
+
}
|
|
537
|
+
if (filesChanged.length > maxFilesChanged) {
|
|
538
|
+
stepUpReasons.push("file_count_exceeds_policy_limit");
|
|
539
|
+
}
|
|
540
|
+
if (filesChanged.some((path) => blockedPathPrefixes.some((prefix) => path.startsWith(prefix)))) {
|
|
541
|
+
stepUpReasons.push("payload_touches_protected_path");
|
|
542
|
+
}
|
|
543
|
+
if (payload.touchesProduction === true) {
|
|
544
|
+
stepUpReasons.push("payload_declares_production_impact");
|
|
545
|
+
}
|
|
546
|
+
if (stepUpReasons.length > 0) {
|
|
547
|
+
return {
|
|
548
|
+
decision: "step_up",
|
|
549
|
+
reasons: stepUpReasons,
|
|
550
|
+
policy,
|
|
551
|
+
constraints,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
decision: "allow",
|
|
556
|
+
reasons: ["payload_is_inside_current_actionpass_constraints"],
|
|
557
|
+
policy,
|
|
558
|
+
constraints,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
export async function authorize(input) {
|
|
562
|
+
const action = parseNormalizedAction(input.action);
|
|
563
|
+
const decision = evaluateAction(action, input.policy);
|
|
564
|
+
const payloadHash = hashPayload(action.capability.payload);
|
|
565
|
+
const traceId = input.traceId ?? `trace_${randomUUID()}`;
|
|
566
|
+
const actionPass = decision.decision === "allow"
|
|
567
|
+
? await issueActionPass({
|
|
568
|
+
action,
|
|
569
|
+
decision,
|
|
570
|
+
issuer: input.issuer,
|
|
571
|
+
tenant: input.tenant,
|
|
572
|
+
signingKey: input.signingKey,
|
|
573
|
+
keyId: input.keyId,
|
|
574
|
+
algorithm: input.algorithm,
|
|
575
|
+
now: input.now,
|
|
576
|
+
approval: input.approval,
|
|
577
|
+
approvalArtifact: input.approvalArtifact,
|
|
578
|
+
traceId,
|
|
579
|
+
})
|
|
580
|
+
: null;
|
|
581
|
+
const ledger = recordDecision({
|
|
582
|
+
action,
|
|
583
|
+
decision,
|
|
584
|
+
actionPassId: actionPass?.claims.jti ?? null,
|
|
585
|
+
previousLedgerHash: input.previousLedgerHash ?? null,
|
|
586
|
+
occurredAt: input.now,
|
|
587
|
+
traceId,
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
action,
|
|
591
|
+
decision,
|
|
592
|
+
payloadHash,
|
|
593
|
+
actionPass,
|
|
594
|
+
ledger,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
export function createApprovalArtifact(input) {
|
|
598
|
+
const action = parseNormalizedAction(input.action);
|
|
599
|
+
const artifact = ApprovalArtifactSchema.parse({
|
|
600
|
+
schemaVersion: APPROVAL_ARTIFACT_VERSION,
|
|
601
|
+
id: input.id ?? `approval_${randomUUID()}`,
|
|
602
|
+
mode: input.mode,
|
|
603
|
+
approvedBy: input.approvedBy,
|
|
604
|
+
approverRoles: input.approverRoles,
|
|
605
|
+
approvedAt: (input.approvedAt ?? new Date()).toISOString(),
|
|
606
|
+
actionHash: hashAction(action),
|
|
607
|
+
payloadHash: hashPayload(action.capability.payload),
|
|
608
|
+
taskId: action.intent.taskId,
|
|
609
|
+
tool: action.capability.tool,
|
|
610
|
+
resource: action.capability.resource,
|
|
611
|
+
reason: input.reason,
|
|
612
|
+
expiresAt: input.expiresAt?.toISOString(),
|
|
613
|
+
});
|
|
614
|
+
const artifactHash = hashJson(artifact);
|
|
615
|
+
return {
|
|
616
|
+
artifact,
|
|
617
|
+
artifactHash,
|
|
618
|
+
approval: {
|
|
619
|
+
mode: artifact.mode,
|
|
620
|
+
approvedBy: artifact.approvedBy,
|
|
621
|
+
approverRoles: artifact.approverRoles,
|
|
622
|
+
approvedAt: artifact.approvedAt,
|
|
623
|
+
approvalArtifact: artifact.id,
|
|
624
|
+
approvalArtifactHash: artifactHash,
|
|
625
|
+
actionHash: artifact.actionHash,
|
|
626
|
+
payloadHash: artifact.payloadHash,
|
|
627
|
+
},
|
|
628
|
+
};
|
|
629
|
+
}
|
|
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) {
|
|
747
|
+
const action = parseNormalizedAction(input.action);
|
|
748
|
+
const decision = PolicyDecisionSchema.parse(input.decision);
|
|
749
|
+
if (decision.decision !== "allow") {
|
|
750
|
+
throw new Error("ActionPass can only be issued for an allow decision.");
|
|
751
|
+
}
|
|
752
|
+
const now = input.now ?? new Date();
|
|
753
|
+
const issuedAt = toNumericDate(now);
|
|
754
|
+
const expiresInSeconds = input.expiresInSeconds ?? decision.constraints.expiresInSeconds;
|
|
755
|
+
const expiresAt = issuedAt + expiresInSeconds;
|
|
756
|
+
const passId = input.passId ?? `ap_${randomUUID()}`;
|
|
757
|
+
const actionHash = hashAction(action);
|
|
758
|
+
const payloadHash = hashPayload(action.capability.payload);
|
|
759
|
+
const approval = bindApproval({
|
|
760
|
+
action,
|
|
761
|
+
actionHash,
|
|
762
|
+
payloadHash,
|
|
763
|
+
approval: input.approval,
|
|
764
|
+
approvalArtifact: input.approvalArtifact,
|
|
765
|
+
now,
|
|
766
|
+
});
|
|
767
|
+
const claims = ActionPassClaimsSchema.parse({
|
|
768
|
+
apv: version,
|
|
769
|
+
iss: input.issuer,
|
|
770
|
+
sub: action.actor.agentId,
|
|
771
|
+
aud: action.capability.resource,
|
|
772
|
+
exp: expiresAt,
|
|
773
|
+
nbf: issuedAt,
|
|
774
|
+
iat: issuedAt,
|
|
775
|
+
jti: passId,
|
|
776
|
+
tenant: input.tenant,
|
|
777
|
+
humanOwner: action.actor.humanOwner,
|
|
778
|
+
agentRuntime: action.actor.runtime,
|
|
779
|
+
intent: action.intent,
|
|
780
|
+
capability: {
|
|
781
|
+
tool: action.capability.tool,
|
|
782
|
+
resource: action.capability.resource,
|
|
783
|
+
constraints: action.capability.constraints,
|
|
784
|
+
},
|
|
785
|
+
decision: decision.decision,
|
|
786
|
+
reasons: decision.reasons,
|
|
787
|
+
policy: decision.policy,
|
|
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,
|
|
802
|
+
approval,
|
|
803
|
+
audit: {
|
|
804
|
+
traceId: input.traceId ?? `trace_${randomUUID()}`,
|
|
805
|
+
parentPassId: input.parentPassId ?? null,
|
|
806
|
+
ledgerHash: input.ledgerHash ?? null,
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
const token = await new SignJWT(claims)
|
|
810
|
+
.setProtectedHeader({
|
|
811
|
+
alg: input.algorithm ?? DEFAULT_SIGNING_ALGORITHM,
|
|
812
|
+
typ: "axtary-actionpass+jwt",
|
|
813
|
+
kid: input.keyId,
|
|
814
|
+
})
|
|
815
|
+
.sign(input.signingKey);
|
|
816
|
+
return { token, claims };
|
|
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
|
+
}
|
|
1002
|
+
export async function verifyActionPass(input) {
|
|
1003
|
+
return verifyActionPassInternal(input, true);
|
|
1004
|
+
}
|
|
1005
|
+
async function verifyActionPassInternal(input, requireDpop) {
|
|
1006
|
+
try {
|
|
1007
|
+
const action = parseNormalizedAction(input.action);
|
|
1008
|
+
const verificationKey = await resolveVerificationKey(input);
|
|
1009
|
+
const { payload } = await jwtVerify(input.token, verificationKey, {
|
|
1010
|
+
issuer: input.issuer,
|
|
1011
|
+
audience: input.audience ?? action.capability.resource,
|
|
1012
|
+
algorithms: input.algorithms ?? [DEFAULT_SIGNING_ALGORITHM],
|
|
1013
|
+
clockTolerance: input.clockTolerance,
|
|
1014
|
+
currentDate: input.currentDate,
|
|
1015
|
+
});
|
|
1016
|
+
const claims = ActionPassClaimsSchema.parse(payload);
|
|
1017
|
+
const payloadHash = hashPayload(action.capability.payload);
|
|
1018
|
+
const bindingError = validatePassBinding(claims, action, payloadHash);
|
|
1019
|
+
if (bindingError) {
|
|
1020
|
+
return {
|
|
1021
|
+
valid: false,
|
|
1022
|
+
reason: bindingError,
|
|
1023
|
+
};
|
|
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
|
+
}
|
|
1033
|
+
const revocationError = await validateRevocation(input, claims);
|
|
1034
|
+
if (revocationError) {
|
|
1035
|
+
return {
|
|
1036
|
+
valid: false,
|
|
1037
|
+
reason: revocationError,
|
|
1038
|
+
};
|
|
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
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
valid: true,
|
|
1048
|
+
claims,
|
|
1049
|
+
payloadHash,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
return {
|
|
1054
|
+
valid: false,
|
|
1055
|
+
reason: error instanceof Error ? error.message : "verification_failed",
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
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
|
+
}
|
|
1114
|
+
export function revokeActionPass(input) {
|
|
1115
|
+
return ActionPassRevocationSchema.parse({
|
|
1116
|
+
schemaVersion: ACTIONPASS_REVOCATION_VERSION,
|
|
1117
|
+
passId: input.passId,
|
|
1118
|
+
revokedAt: (input.revokedAt ?? new Date()).toISOString(),
|
|
1119
|
+
revokedBy: input.revokedBy,
|
|
1120
|
+
reason: input.reason,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
export class LocalActionPassTrustStore {
|
|
1124
|
+
filePath;
|
|
1125
|
+
constructor(filePath) {
|
|
1126
|
+
this.filePath = filePath;
|
|
1127
|
+
}
|
|
1128
|
+
async read() {
|
|
1129
|
+
return readTrustStore(this.filePath);
|
|
1130
|
+
}
|
|
1131
|
+
async write(dataInput) {
|
|
1132
|
+
const data = ActionPassTrustStoreSchema.parse({
|
|
1133
|
+
schemaVersion: ACTIONPASS_TRUST_STORE_VERSION,
|
|
1134
|
+
keys: dataInput.keys ?? [],
|
|
1135
|
+
revocations: dataInput.revocations ?? [],
|
|
1136
|
+
});
|
|
1137
|
+
await writeTrustStore(this.filePath, data);
|
|
1138
|
+
return data;
|
|
1139
|
+
}
|
|
1140
|
+
async putVerificationKey(keyInput) {
|
|
1141
|
+
const key = ActionPassVerificationKeyRecordSchema.parse(keyInput);
|
|
1142
|
+
const data = await this.read();
|
|
1143
|
+
const nextKeys = [
|
|
1144
|
+
...data.keys.filter((entry) => entry.kid !== key.kid),
|
|
1145
|
+
key,
|
|
1146
|
+
].sort((left, right) => left.kid.localeCompare(right.kid));
|
|
1147
|
+
await this.write({
|
|
1148
|
+
...data,
|
|
1149
|
+
keys: nextKeys,
|
|
1150
|
+
});
|
|
1151
|
+
return key;
|
|
1152
|
+
}
|
|
1153
|
+
async revokeActionPass(input) {
|
|
1154
|
+
const revocation = "schemaVersion" in input
|
|
1155
|
+
? ActionPassRevocationSchema.parse(input)
|
|
1156
|
+
: revokeActionPass(input);
|
|
1157
|
+
const data = await this.read();
|
|
1158
|
+
const nextRevocations = [
|
|
1159
|
+
...data.revocations.filter((entry) => entry.passId !== revocation.passId),
|
|
1160
|
+
revocation,
|
|
1161
|
+
].sort((left, right) => left.passId.localeCompare(right.passId));
|
|
1162
|
+
await this.write({
|
|
1163
|
+
...data,
|
|
1164
|
+
revocations: nextRevocations,
|
|
1165
|
+
});
|
|
1166
|
+
return revocation;
|
|
1167
|
+
}
|
|
1168
|
+
async getVerificationKey(keyId, now = new Date()) {
|
|
1169
|
+
if (!keyId) {
|
|
1170
|
+
return undefined;
|
|
1171
|
+
}
|
|
1172
|
+
const data = await this.read();
|
|
1173
|
+
const key = data.keys.find((entry) => entry.kid === keyId);
|
|
1174
|
+
if (!key || !isTrustedVerificationKey(key, now)) {
|
|
1175
|
+
return undefined;
|
|
1176
|
+
}
|
|
1177
|
+
return key.publicJwk;
|
|
1178
|
+
}
|
|
1179
|
+
async isPassRevoked(passId) {
|
|
1180
|
+
const data = await this.read();
|
|
1181
|
+
return data.revocations.some((entry) => entry.passId === passId);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
export async function verifyActionPassWithTrustStore(input) {
|
|
1185
|
+
const store = typeof input.trustStore === "string"
|
|
1186
|
+
? new LocalActionPassTrustStore(input.trustStore)
|
|
1187
|
+
: input.trustStore;
|
|
1188
|
+
return verifyActionPass({
|
|
1189
|
+
...input,
|
|
1190
|
+
verificationKeys: (keyId) => store.getVerificationKey(keyId),
|
|
1191
|
+
isRevoked: (claims) => store.isPassRevoked(claims.jti),
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
export function recordDecision(input) {
|
|
1195
|
+
const action = parseNormalizedAction(input.action);
|
|
1196
|
+
const decision = PolicyDecisionSchema.parse(input.decision);
|
|
1197
|
+
const payloadHash = hashPayload(action.capability.payload);
|
|
1198
|
+
const actionHash = hashAction(action);
|
|
1199
|
+
const traceId = input.traceId ?? `trace_${randomUUID()}`;
|
|
1200
|
+
const recordWithoutHash = {
|
|
1201
|
+
schemaVersion: LEDGER_SCHEMA_VERSION,
|
|
1202
|
+
id: `ledger_${randomUUID()}`,
|
|
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
|
+
},
|
|
1212
|
+
actionHash,
|
|
1213
|
+
payloadHash,
|
|
1214
|
+
provenanceHash: action.provenance
|
|
1215
|
+
? hashProvenance(action.provenance)
|
|
1216
|
+
: undefined,
|
|
1217
|
+
decision: decision.decision,
|
|
1218
|
+
reasons: decision.reasons,
|
|
1219
|
+
policy: decision.policy,
|
|
1220
|
+
providerEvidence: input.providerEvidence ?? providerEvidenceForAction(action),
|
|
1221
|
+
executionOutcome: input.executionOutcome,
|
|
1222
|
+
traceId,
|
|
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,
|
|
1230
|
+
correlationId: input.correlationId ?? undefined,
|
|
1231
|
+
previousLedgerHash: input.previousLedgerHash ?? null,
|
|
1232
|
+
};
|
|
1233
|
+
return LedgerRecordSchema.parse({
|
|
1234
|
+
...recordWithoutHash,
|
|
1235
|
+
ledgerHash: hashJson(recordWithoutHash),
|
|
1236
|
+
});
|
|
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
|
+
}
|
|
1284
|
+
export function hashPayload(payload) {
|
|
1285
|
+
return hashJson(JsonValueSchema.parse(payload));
|
|
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
|
+
}
|
|
1348
|
+
export function hashAction(action) {
|
|
1349
|
+
return hashJson(parseNormalizedAction(action));
|
|
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
|
+
]);
|
|
1787
|
+
export function providerEvidenceForAction(actionInput) {
|
|
1788
|
+
const action = parseNormalizedAction(actionInput);
|
|
1789
|
+
return providerEvidenceNormalizers.get(action.capability.tool)?.(action, action.capability.payload);
|
|
1790
|
+
}
|
|
1791
|
+
export function hashJson(value) {
|
|
1792
|
+
return `sha256:${createHash("sha256")
|
|
1793
|
+
.update(stableStringify(value))
|
|
1794
|
+
.digest("hex")}`;
|
|
1795
|
+
}
|
|
1796
|
+
export function stableStringify(value) {
|
|
1797
|
+
return JSON.stringify(canonicalize(value));
|
|
1798
|
+
}
|
|
1799
|
+
function validatePassBinding(claims, action, payloadHash) {
|
|
1800
|
+
if (claims.sub !== action.actor.agentId) {
|
|
1801
|
+
return "agent_id_mismatch";
|
|
1802
|
+
}
|
|
1803
|
+
if (claims.humanOwner !== action.actor.humanOwner) {
|
|
1804
|
+
return "human_owner_mismatch";
|
|
1805
|
+
}
|
|
1806
|
+
if (claims.agentRuntime !== action.actor.runtime) {
|
|
1807
|
+
return "agent_runtime_mismatch";
|
|
1808
|
+
}
|
|
1809
|
+
if (claims.intent.taskId !== action.intent.taskId) {
|
|
1810
|
+
return "task_id_mismatch";
|
|
1811
|
+
}
|
|
1812
|
+
if (stableStringify(claims.intent) !== stableStringify(action.intent)) {
|
|
1813
|
+
return "intent_mismatch";
|
|
1814
|
+
}
|
|
1815
|
+
if (claims.capability.tool !== action.capability.tool) {
|
|
1816
|
+
return "tool_mismatch";
|
|
1817
|
+
}
|
|
1818
|
+
if (claims.capability.resource !== action.capability.resource) {
|
|
1819
|
+
return "resource_mismatch";
|
|
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
|
+
}
|
|
1828
|
+
if (claims.payloadHash !== payloadHash) {
|
|
1829
|
+
return "payload_hash_mismatch";
|
|
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
|
+
}
|
|
1839
|
+
if (claims.approval?.payloadHash && claims.approval.payloadHash !== payloadHash) {
|
|
1840
|
+
return "approval_payload_hash_mismatch";
|
|
1841
|
+
}
|
|
1842
|
+
if (claims.approval?.actionHash && claims.approval.actionHash !== hashAction(action)) {
|
|
1843
|
+
return "approval_action_hash_mismatch";
|
|
1844
|
+
}
|
|
1845
|
+
return null;
|
|
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
|
+
}
|
|
1941
|
+
async function readTrustStore(filePath) {
|
|
1942
|
+
const text = await readTrustStoreFile(filePath);
|
|
1943
|
+
if (!text.trim()) {
|
|
1944
|
+
return ActionPassTrustStoreSchema.parse({
|
|
1945
|
+
schemaVersion: ACTIONPASS_TRUST_STORE_VERSION,
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
return ActionPassTrustStoreSchema.parse(JSON.parse(text));
|
|
1949
|
+
}
|
|
1950
|
+
async function writeTrustStore(filePath, data) {
|
|
1951
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
1952
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1953
|
+
await writeFile(tmpPath, `${JSON.stringify(data, null, 2)}\n`, {
|
|
1954
|
+
encoding: "utf8",
|
|
1955
|
+
flag: "wx",
|
|
1956
|
+
mode: 0o600,
|
|
1957
|
+
});
|
|
1958
|
+
await rename(tmpPath, filePath);
|
|
1959
|
+
}
|
|
1960
|
+
async function readTrustStoreFile(filePath) {
|
|
1961
|
+
try {
|
|
1962
|
+
return await readFile(filePath, "utf8");
|
|
1963
|
+
}
|
|
1964
|
+
catch (error) {
|
|
1965
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1966
|
+
return "";
|
|
1967
|
+
}
|
|
1968
|
+
throw error;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function isTrustedVerificationKey(key, now) {
|
|
1972
|
+
if (key.status === "revoked") {
|
|
1973
|
+
return false;
|
|
1974
|
+
}
|
|
1975
|
+
if (key.notBefore && Date.parse(key.notBefore) > now.getTime()) {
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
if (key.expiresAt && Date.parse(key.expiresAt) < now.getTime()) {
|
|
1979
|
+
return false;
|
|
1980
|
+
}
|
|
1981
|
+
return true;
|
|
1982
|
+
}
|
|
1983
|
+
async function resolveVerificationKey(input) {
|
|
1984
|
+
if (!input.verificationKeys) {
|
|
1985
|
+
if (!input.verificationKey) {
|
|
1986
|
+
throw new Error("verification_key_required");
|
|
1987
|
+
}
|
|
1988
|
+
return input.verificationKey;
|
|
1989
|
+
}
|
|
1990
|
+
const { kid } = decodeProtectedHeader(input.token);
|
|
1991
|
+
const key = typeof input.verificationKeys === "function"
|
|
1992
|
+
? await input.verificationKeys(kid)
|
|
1993
|
+
: kid
|
|
1994
|
+
? input.verificationKeys[kid]
|
|
1995
|
+
: undefined;
|
|
1996
|
+
if (!key) {
|
|
1997
|
+
throw new Error(kid ? `verification_key_not_found:${kid}` : "verification_key_id_required");
|
|
1998
|
+
}
|
|
1999
|
+
return key;
|
|
2000
|
+
}
|
|
2001
|
+
async function validateRevocation(input, claims) {
|
|
2002
|
+
if (input.revokedPassIds) {
|
|
2003
|
+
for (const passId of input.revokedPassIds) {
|
|
2004
|
+
if (passId === claims.jti) {
|
|
2005
|
+
return "actionpass_revoked";
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
if (input.revocations) {
|
|
2010
|
+
for (const revocationInput of input.revocations) {
|
|
2011
|
+
const revocation = ActionPassRevocationSchema.parse(revocationInput);
|
|
2012
|
+
if (revocation.passId === claims.jti) {
|
|
2013
|
+
return "actionpass_revoked";
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (input.isRevoked) {
|
|
2018
|
+
let result;
|
|
2019
|
+
try {
|
|
2020
|
+
result = await input.isRevoked(claims);
|
|
2021
|
+
}
|
|
2022
|
+
catch {
|
|
2023
|
+
return "actionpass_revocation_check_failed";
|
|
2024
|
+
}
|
|
2025
|
+
if (result === true) {
|
|
2026
|
+
return "actionpass_revoked";
|
|
2027
|
+
}
|
|
2028
|
+
if (typeof result === "string" && result.length > 0) {
|
|
2029
|
+
return result;
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return null;
|
|
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
|
+
}
|
|
2055
|
+
function bindApproval(input) {
|
|
2056
|
+
const approval = input.approval
|
|
2057
|
+
? ApprovalSchema.parse(input.approval)
|
|
2058
|
+
: undefined;
|
|
2059
|
+
const artifact = input.approvalArtifact
|
|
2060
|
+
? ApprovalArtifactSchema.parse(input.approvalArtifact)
|
|
2061
|
+
: undefined;
|
|
2062
|
+
if (!approval && !artifact) {
|
|
2063
|
+
return undefined;
|
|
2064
|
+
}
|
|
2065
|
+
if (approval && approval.mode !== "none" && !approval.approvedBy && !artifact) {
|
|
2066
|
+
throw new Error("approval_approver_required");
|
|
2067
|
+
}
|
|
2068
|
+
if (approval?.payloadHash && approval.payloadHash !== input.payloadHash) {
|
|
2069
|
+
throw new Error("approval_payload_hash_mismatch");
|
|
2070
|
+
}
|
|
2071
|
+
if (approval?.actionHash && approval.actionHash !== input.actionHash) {
|
|
2072
|
+
throw new Error("approval_action_hash_mismatch");
|
|
2073
|
+
}
|
|
2074
|
+
if (approval &&
|
|
2075
|
+
approval.mode !== "none" &&
|
|
2076
|
+
!approval.payloadHash &&
|
|
2077
|
+
!approval.actionHash &&
|
|
2078
|
+
!artifact) {
|
|
2079
|
+
throw new Error("approval_binding_required");
|
|
2080
|
+
}
|
|
2081
|
+
if (!artifact) {
|
|
2082
|
+
return approval;
|
|
2083
|
+
}
|
|
2084
|
+
const artifactError = validateApprovalArtifact(artifact, input.action, input.now);
|
|
2085
|
+
if (artifactError) {
|
|
2086
|
+
throw new Error(artifactError);
|
|
2087
|
+
}
|
|
2088
|
+
const artifactHash = hashJson(artifact);
|
|
2089
|
+
if (approval?.approvalArtifactHash &&
|
|
2090
|
+
approval.approvalArtifactHash !== artifactHash) {
|
|
2091
|
+
throw new Error("approval_artifact_hash_mismatch");
|
|
2092
|
+
}
|
|
2093
|
+
return ApprovalSchema.parse({
|
|
2094
|
+
mode: approval?.mode ?? artifact.mode,
|
|
2095
|
+
approvedBy: approval?.approvedBy ?? artifact.approvedBy,
|
|
2096
|
+
approverRoles: approval?.approverRoles ?? artifact.approverRoles,
|
|
2097
|
+
approvedAt: approval?.approvedAt ?? artifact.approvedAt,
|
|
2098
|
+
approvalArtifact: approval?.approvalArtifact ?? artifact.id,
|
|
2099
|
+
approvalArtifactHash: artifactHash,
|
|
2100
|
+
actionHash: artifact.actionHash,
|
|
2101
|
+
payloadHash: artifact.payloadHash,
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
export function validateApprovalArtifact(artifactInput, actionInput, now = new Date()) {
|
|
2105
|
+
const artifact = ApprovalArtifactSchema.parse(artifactInput);
|
|
2106
|
+
const action = parseNormalizedAction(actionInput);
|
|
2107
|
+
if (artifact.actionHash !== hashAction(action)) {
|
|
2108
|
+
return "approval_action_hash_mismatch";
|
|
2109
|
+
}
|
|
2110
|
+
if (artifact.payloadHash !== hashPayload(action.capability.payload)) {
|
|
2111
|
+
return "approval_payload_hash_mismatch";
|
|
2112
|
+
}
|
|
2113
|
+
if (artifact.taskId !== action.intent.taskId) {
|
|
2114
|
+
return "approval_task_id_mismatch";
|
|
2115
|
+
}
|
|
2116
|
+
if (artifact.tool !== action.capability.tool) {
|
|
2117
|
+
return "approval_tool_mismatch";
|
|
2118
|
+
}
|
|
2119
|
+
if (artifact.resource !== action.capability.resource) {
|
|
2120
|
+
return "approval_resource_mismatch";
|
|
2121
|
+
}
|
|
2122
|
+
if (artifact.expiresAt && Date.parse(artifact.expiresAt) < now.getTime()) {
|
|
2123
|
+
return "approval_artifact_expired";
|
|
2124
|
+
}
|
|
2125
|
+
return null;
|
|
2126
|
+
}
|
|
2127
|
+
function githubPullRequestEvidence(action, payload) {
|
|
2128
|
+
const files = changedFilesFromPayload(payload);
|
|
2129
|
+
const diff = providerDiffFromPayload("github", payload) ?? {
|
|
2130
|
+
provider: "github",
|
|
2131
|
+
summary: `${files.length} changed ${files.length === 1 ? "file" : "files"}`,
|
|
2132
|
+
files: files.map((path) => ({
|
|
2133
|
+
path,
|
|
2134
|
+
status: "modified",
|
|
2135
|
+
additions: 0,
|
|
2136
|
+
deletions: 0,
|
|
2137
|
+
hunks: [],
|
|
2138
|
+
})),
|
|
2139
|
+
};
|
|
2140
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2141
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2142
|
+
provider: "github",
|
|
2143
|
+
tool: action.capability.tool,
|
|
2144
|
+
operation: "pull_request_create",
|
|
2145
|
+
resource: providerResource("github", "repo", action.capability.resource),
|
|
2146
|
+
normalized: compactJsonRecord({
|
|
2147
|
+
title: getString(payload.title) ?? action.intent.declaredGoal,
|
|
2148
|
+
baseBranch: getString(payload.baseBranch),
|
|
2149
|
+
headBranch: getString(payload.headBranch) ?? getString(payload.branch),
|
|
2150
|
+
filesChanged: files,
|
|
2151
|
+
touchesProduction: getBoolean(payload.touchesProduction),
|
|
2152
|
+
testsPassed: getBoolean(payload.testsPassed),
|
|
2153
|
+
draft: getBoolean(payload.draft),
|
|
2154
|
+
}),
|
|
2155
|
+
diff,
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
function githubContentEvidence(action, payload, operation) {
|
|
2159
|
+
const path = getString(payload.path) ?? "unknown";
|
|
2160
|
+
const diff = operation === "content_write"
|
|
2161
|
+
? providerDiffFromPayload("github", payload) ?? {
|
|
2162
|
+
provider: "github",
|
|
2163
|
+
summary: "1 changed file",
|
|
2164
|
+
files: [
|
|
2165
|
+
{
|
|
2166
|
+
path,
|
|
2167
|
+
status: "modified",
|
|
2168
|
+
additions: 0,
|
|
2169
|
+
deletions: 0,
|
|
2170
|
+
hunks: [],
|
|
2171
|
+
},
|
|
2172
|
+
],
|
|
2173
|
+
}
|
|
2174
|
+
: undefined;
|
|
2175
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2176
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2177
|
+
provider: "github",
|
|
2178
|
+
tool: action.capability.tool,
|
|
2179
|
+
operation,
|
|
2180
|
+
resource: providerResource("github", "file", `${action.capability.resource}:${path}`),
|
|
2181
|
+
normalized: compactJsonRecord({
|
|
2182
|
+
path,
|
|
2183
|
+
branch: getString(payload.branch) ?? getString(payload.ref),
|
|
2184
|
+
message: getString(payload.message),
|
|
2185
|
+
contentLength: operation === "content_write"
|
|
2186
|
+
? getString(payload.content)?.length ?? null
|
|
2187
|
+
: null,
|
|
2188
|
+
}),
|
|
2189
|
+
diff,
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
function githubBranchEvidence(action, payload) {
|
|
2193
|
+
const branch = getString(payload.branch) ?? "unknown";
|
|
2194
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2195
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2196
|
+
provider: "github",
|
|
2197
|
+
tool: action.capability.tool,
|
|
2198
|
+
operation: "branch_create",
|
|
2199
|
+
resource: providerResource("github", "branch", `${action.capability.resource}:${branch}`),
|
|
2200
|
+
normalized: compactJsonRecord({
|
|
2201
|
+
branch,
|
|
2202
|
+
baseBranch: getString(payload.baseBranch),
|
|
2203
|
+
sha: getString(payload.sha),
|
|
2204
|
+
}),
|
|
2205
|
+
});
|
|
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
|
+
}
|
|
2304
|
+
function slackMessageEvidence(action, payload) {
|
|
2305
|
+
const channel = getString(payload.channel) ?? "unknown";
|
|
2306
|
+
const message = getString(payload.text) ?? getString(payload.message) ?? "";
|
|
2307
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2308
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2309
|
+
provider: "slack",
|
|
2310
|
+
tool: action.capability.tool,
|
|
2311
|
+
operation: "message_post",
|
|
2312
|
+
resource: providerResource("slack", "channel", `${action.capability.resource}:${channel}`, channel),
|
|
2313
|
+
normalized: compactJsonRecord({
|
|
2314
|
+
channel,
|
|
2315
|
+
message: redactCredentialLikeText(message),
|
|
2316
|
+
messageLength: message.length,
|
|
2317
|
+
threadTs: getString(payload.threadTs),
|
|
2318
|
+
externalRecipients: getStringArray(payload.externalRecipients),
|
|
2319
|
+
mentionedUsers: getStringArray(payload.mentionedUsers),
|
|
2320
|
+
mentionedGroups: getStringArray(payload.mentionedGroups),
|
|
2321
|
+
links: getStringArray(payload.links),
|
|
2322
|
+
attachmentCount: getArrayLength(payload.attachments),
|
|
2323
|
+
unfurlLinks: getBoolean(payload.unfurlLinks),
|
|
2324
|
+
unfurlMedia: getBoolean(payload.unfurlMedia),
|
|
2325
|
+
}),
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
function issueTrackerEvidence(action, payload, provider, operation) {
|
|
2329
|
+
const issueKey = getString(payload.issueKey) ?? getString(payload.issueId) ?? "unknown";
|
|
2330
|
+
const projectKey = getString(payload.projectKey) ?? issueKey.split("-", 1)[0];
|
|
2331
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2332
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2333
|
+
provider,
|
|
2334
|
+
tool: action.capability.tool,
|
|
2335
|
+
operation,
|
|
2336
|
+
resource: providerResource(provider, "issue", `${action.capability.resource}:${issueKey}`, issueKey),
|
|
2337
|
+
normalized: compactJsonRecord({
|
|
2338
|
+
issueKey,
|
|
2339
|
+
projectKey,
|
|
2340
|
+
title: getString(payload.title),
|
|
2341
|
+
status: getString(payload.status),
|
|
2342
|
+
stateId: getString(payload.stateId),
|
|
2343
|
+
priority: getNumber(payload.priority),
|
|
2344
|
+
priorityId: getString(payload.priorityId),
|
|
2345
|
+
commentBody: operation === "comment_create"
|
|
2346
|
+
? redactCredentialLikeText(getString(payload.body) ?? "")
|
|
2347
|
+
: null,
|
|
2348
|
+
}),
|
|
2349
|
+
fieldChanges: issueTrackerFieldChanges(payload),
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
function docsEvidence(action, payload, operation) {
|
|
2353
|
+
const workspace = getString(payload.workspace) ?? "local";
|
|
2354
|
+
const path = getString(payload.path);
|
|
2355
|
+
const query = getString(payload.query);
|
|
2356
|
+
const resourceId = path
|
|
2357
|
+
? `${action.capability.resource}:${workspace}:${path}`
|
|
2358
|
+
: `${action.capability.resource}:${workspace}:search`;
|
|
2359
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2360
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2361
|
+
provider: "docs",
|
|
2362
|
+
tool: action.capability.tool,
|
|
2363
|
+
operation,
|
|
2364
|
+
resource: providerResource("docs", path ? "document" : "workspace", resourceId, path ?? workspace),
|
|
2365
|
+
normalized: compactJsonRecord({
|
|
2366
|
+
workspace,
|
|
2367
|
+
path,
|
|
2368
|
+
query,
|
|
2369
|
+
maxResults: getNumber(payload.maxResults),
|
|
2370
|
+
maxBytes: getNumber(payload.maxBytes),
|
|
2371
|
+
}),
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
function mcpEvidence(action, payload) {
|
|
2375
|
+
const serverIdentity = action.toolDefinition?.serverIdentity ??
|
|
2376
|
+
getString(payload.serverIdentity) ??
|
|
2377
|
+
"mcp:unknown";
|
|
2378
|
+
const toolName = getString(payload.toolName) ?? action.capability.tool;
|
|
2379
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2380
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2381
|
+
provider: "mcp",
|
|
2382
|
+
tool: action.capability.tool,
|
|
2383
|
+
operation: "tool_call",
|
|
2384
|
+
resource: providerResource("mcp", "tool", `${serverIdentity}:${toolName}`, toolName),
|
|
2385
|
+
normalized: compactJsonRecord({
|
|
2386
|
+
serverIdentity,
|
|
2387
|
+
schemaVersion: action.toolDefinition?.schemaVersion,
|
|
2388
|
+
definitionHash: action.toolDefinition?.definitionHash,
|
|
2389
|
+
toolName,
|
|
2390
|
+
}),
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
function awsEvidence(action, payload, operation) {
|
|
2394
|
+
const bucket = getString(payload.bucket);
|
|
2395
|
+
const accountId = getString(payload.accountId);
|
|
2396
|
+
const region = getString(payload.region);
|
|
2397
|
+
const resourceId = bucket
|
|
2398
|
+
? `aws:s3:${region ?? "unknown"}:${bucket}`
|
|
2399
|
+
: accountId
|
|
2400
|
+
? `aws:account:${accountId}`
|
|
2401
|
+
: action.capability.resource;
|
|
2402
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2403
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2404
|
+
provider: "aws",
|
|
2405
|
+
tool: action.capability.tool,
|
|
2406
|
+
operation,
|
|
2407
|
+
resource: providerResource("aws", bucket ? "s3_bucket" : "identity", resourceId, bucket ?? accountId),
|
|
2408
|
+
normalized: compactJsonRecord({
|
|
2409
|
+
accountId,
|
|
2410
|
+
region,
|
|
2411
|
+
bucket,
|
|
2412
|
+
prefix: getString(payload.prefix),
|
|
2413
|
+
maxKeys: getNumber(payload.maxKeys),
|
|
2414
|
+
}),
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
function gcpEvidence(action, payload, operation) {
|
|
2418
|
+
const projectId = getString(payload.projectId);
|
|
2419
|
+
const bucket = getString(payload.bucket);
|
|
2420
|
+
const resourceId = bucket
|
|
2421
|
+
? `gcp:storage:${bucket}`
|
|
2422
|
+
: projectId
|
|
2423
|
+
? `gcp:project:${projectId}`
|
|
2424
|
+
: action.capability.resource;
|
|
2425
|
+
return LedgerProviderEvidenceSchema.parse({
|
|
2426
|
+
schemaVersion: LEDGER_PROVIDER_EVIDENCE_VERSION,
|
|
2427
|
+
provider: "gcp",
|
|
2428
|
+
tool: action.capability.tool,
|
|
2429
|
+
operation,
|
|
2430
|
+
resource: providerResource("gcp", bucket ? "storage_bucket" : "project", resourceId, bucket ?? projectId),
|
|
2431
|
+
normalized: compactJsonRecord({
|
|
2432
|
+
projectId,
|
|
2433
|
+
bucket,
|
|
2434
|
+
prefix: getString(payload.prefix),
|
|
2435
|
+
maxResults: getNumber(payload.maxResults),
|
|
2436
|
+
}),
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
function providerResource(provider, kind, id, label) {
|
|
2440
|
+
return {
|
|
2441
|
+
provider,
|
|
2442
|
+
kind,
|
|
2443
|
+
id,
|
|
2444
|
+
...(label ? { label } : {}),
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
function providerDiffFromPayload(provider, payload) {
|
|
2448
|
+
const diff = isJsonObject(payload.diff) ? payload.diff : null;
|
|
2449
|
+
if (!diff)
|
|
2450
|
+
return undefined;
|
|
2451
|
+
return LedgerProviderEvidenceDiffSchema.parse({
|
|
2452
|
+
provider,
|
|
2453
|
+
summary: getString(diff.summary) ?? undefined,
|
|
2454
|
+
files: getObjectArray(diff.files).map((file) => ({
|
|
2455
|
+
path: getString(file.path) ?? "unknown",
|
|
2456
|
+
oldPath: getString(file.oldPath) ?? undefined,
|
|
2457
|
+
status: parseDiffStatus(getString(file.status)),
|
|
2458
|
+
additions: getNumber(file.additions) ?? 0,
|
|
2459
|
+
deletions: getNumber(file.deletions) ?? 0,
|
|
2460
|
+
hunks: getObjectArray(file.hunks).map((hunk) => ({
|
|
2461
|
+
header: getString(hunk.header) ?? "@@",
|
|
2462
|
+
lines: getObjectArray(hunk.lines).map((line) => ({
|
|
2463
|
+
type: parseDiffLineType(getString(line.type)),
|
|
2464
|
+
content: redactCredentialLikeText(getString(line.content) ?? ""),
|
|
2465
|
+
oldLine: getNumber(line.oldLine) ?? undefined,
|
|
2466
|
+
newLine: getNumber(line.newLine) ?? undefined,
|
|
2467
|
+
})),
|
|
2468
|
+
})),
|
|
2469
|
+
})),
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
function changedFilesFromPayload(payload) {
|
|
2473
|
+
const changes = getObjectArray(payload.changes)
|
|
2474
|
+
.map((change) => getString(change.path))
|
|
2475
|
+
.filter((path) => Boolean(path));
|
|
2476
|
+
return [...new Set([...getStringArray(payload.filesChanged), ...changes])];
|
|
2477
|
+
}
|
|
2478
|
+
function issueTrackerFieldChanges(payload) {
|
|
2479
|
+
return [
|
|
2480
|
+
fieldChange("title", payload.fromTitle, payload.title),
|
|
2481
|
+
fieldChange("description", payload.fromDescription, payload.description),
|
|
2482
|
+
fieldChange("status", payload.fromStatus, payload.status),
|
|
2483
|
+
fieldChange("stateId", payload.fromStateId, payload.stateId),
|
|
2484
|
+
fieldChange("priority", payload.fromPriority, payload.priority),
|
|
2485
|
+
fieldChange("priorityId", payload.fromPriorityId, payload.priorityId),
|
|
2486
|
+
].filter((change) => change !== null);
|
|
2487
|
+
}
|
|
2488
|
+
function fieldChange(field, before, after) {
|
|
2489
|
+
if (before === undefined && after === undefined)
|
|
2490
|
+
return null;
|
|
2491
|
+
return LedgerProviderEvidenceFieldChangeSchema.parse({
|
|
2492
|
+
field,
|
|
2493
|
+
before: sanitizeJsonValue(before),
|
|
2494
|
+
after: sanitizeJsonValue(after),
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
function compactJsonRecord(input) {
|
|
2498
|
+
return Object.fromEntries(Object.entries(input)
|
|
2499
|
+
.map(([key, value]) => [key, sanitizeJsonValue(value)])
|
|
2500
|
+
.filter((entry) => entry[1] !== undefined));
|
|
2501
|
+
}
|
|
2502
|
+
function sanitizeJsonValue(value) {
|
|
2503
|
+
if (value === undefined || value === null)
|
|
2504
|
+
return undefined;
|
|
2505
|
+
if (typeof value === "string")
|
|
2506
|
+
return redactCredentialLikeText(value);
|
|
2507
|
+
if (Array.isArray(value)) {
|
|
2508
|
+
return value
|
|
2509
|
+
.map((item) => sanitizeJsonValue(item))
|
|
2510
|
+
.filter((item) => item !== undefined);
|
|
2511
|
+
}
|
|
2512
|
+
if (isJsonObject(value)) {
|
|
2513
|
+
return compactJsonRecord(value);
|
|
2514
|
+
}
|
|
2515
|
+
return value;
|
|
2516
|
+
}
|
|
2517
|
+
function redactCredentialLikeText(value) {
|
|
2518
|
+
return value
|
|
2519
|
+
.replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[redacted-github-token]")
|
|
2520
|
+
.replace(/\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g, "[redacted-slack-token]")
|
|
2521
|
+
.replace(/\bsk-[A-Za-z0-9_-]{20,}\b/g, "[redacted-api-key]")
|
|
2522
|
+
.replace(/\bAKIA[0-9A-Z]{16}\b/g, "[redacted-aws-access-key]")
|
|
2523
|
+
.replace(/\b[A-Za-z0-9+/]{40}\b/g, "[redacted-token-like-value]");
|
|
2524
|
+
}
|
|
2525
|
+
function parseDiffStatus(value) {
|
|
2526
|
+
if (value === "added" ||
|
|
2527
|
+
value === "modified" ||
|
|
2528
|
+
value === "deleted" ||
|
|
2529
|
+
value === "renamed") {
|
|
2530
|
+
return value;
|
|
2531
|
+
}
|
|
2532
|
+
return "modified";
|
|
2533
|
+
}
|
|
2534
|
+
function parseDiffLineType(value) {
|
|
2535
|
+
if (value === "addition" || value === "deletion" || value === "context") {
|
|
2536
|
+
return value;
|
|
2537
|
+
}
|
|
2538
|
+
return "context";
|
|
2539
|
+
}
|
|
2540
|
+
function getString(value) {
|
|
2541
|
+
return typeof value === "string" ? value : null;
|
|
2542
|
+
}
|
|
2543
|
+
function getStringArray(value) {
|
|
2544
|
+
if (!Array.isArray(value)) {
|
|
2545
|
+
return [];
|
|
2546
|
+
}
|
|
2547
|
+
return value.filter((item) => typeof item === "string");
|
|
2548
|
+
}
|
|
2549
|
+
function getNumber(value) {
|
|
2550
|
+
return typeof value === "number" ? value : null;
|
|
2551
|
+
}
|
|
2552
|
+
function getBoolean(value) {
|
|
2553
|
+
return typeof value === "boolean" ? value : null;
|
|
2554
|
+
}
|
|
2555
|
+
function getArrayLength(value) {
|
|
2556
|
+
return Array.isArray(value) ? value.length : null;
|
|
2557
|
+
}
|
|
2558
|
+
function getObjectArray(value) {
|
|
2559
|
+
if (!Array.isArray(value)) {
|
|
2560
|
+
return [];
|
|
2561
|
+
}
|
|
2562
|
+
return value.filter((item) => isJsonObject(item));
|
|
2563
|
+
}
|
|
2564
|
+
function isJsonObject(value) {
|
|
2565
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2566
|
+
}
|
|
2567
|
+
function toNumericDate(date) {
|
|
2568
|
+
return Math.floor(date.getTime() / 1000);
|
|
2569
|
+
}
|
|
2570
|
+
function canonicalize(value) {
|
|
2571
|
+
if (Array.isArray(value)) {
|
|
2572
|
+
return value.map((item) => canonicalize(item));
|
|
2573
|
+
}
|
|
2574
|
+
if (isPlainObject(value)) {
|
|
2575
|
+
return Object.fromEntries(Object.entries(value)
|
|
2576
|
+
.filter(([, entryValue]) => entryValue !== undefined)
|
|
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))
|
|
2580
|
+
.map(([key, entryValue]) => [key, canonicalize(entryValue)]));
|
|
2581
|
+
}
|
|
2582
|
+
return value;
|
|
2583
|
+
}
|
|
2584
|
+
function isPlainObject(value) {
|
|
2585
|
+
return (typeof value === "object" &&
|
|
2586
|
+
value !== null &&
|
|
2587
|
+
!Array.isArray(value) &&
|
|
2588
|
+
Object.getPrototypeOf(value) === Object.prototype);
|
|
2589
|
+
}
|
|
2590
|
+
function isNodeError(error) {
|
|
2591
|
+
return error instanceof Error && "code" in error;
|
|
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();
|
|
2851
|
+
//# sourceMappingURL=index.js.map
|