@axtary/policy 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/index.d.ts +241 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1218 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
import { DEFAULT_EXPIRES_IN_SECONDS, NormalizedActionSchema, } from "@axtary/actionpass";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const POLICY_SCHEMA_VERSION = "axtary.policy.v0";
|
|
4
|
+
const DEFAULT_GITHUB_PULL_REQUEST_POLICY = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
requiredBaseBranch: "main",
|
|
7
|
+
maxFilesChanged: 12,
|
|
8
|
+
denyPathPrefixes: [".env", "secrets/"],
|
|
9
|
+
stepUpPathPrefixes: ["infra/prod/", "billing/", "auth/"],
|
|
10
|
+
requiresTests: true,
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_GITHUB_CONTENTS_POLICY = {
|
|
13
|
+
enabled: true,
|
|
14
|
+
denyReadPathPrefixes: [".env", "secrets/", "infra/prod/"],
|
|
15
|
+
};
|
|
16
|
+
const DEFAULT_SLACK_MESSAGES_POLICY = {
|
|
17
|
+
enabled: true,
|
|
18
|
+
allowedChannels: ["#axtary-dev"],
|
|
19
|
+
stepUpExternalRecipients: true,
|
|
20
|
+
};
|
|
21
|
+
const DEFAULT_ISSUE_TRACKER_POLICY = {
|
|
22
|
+
enabled: true,
|
|
23
|
+
allowedProjectKeys: ["AXT"],
|
|
24
|
+
stepUpStatuses: ["Done", "Closed"],
|
|
25
|
+
};
|
|
26
|
+
const DEFAULT_AWS_READ_POLICY = {
|
|
27
|
+
enabled: true,
|
|
28
|
+
allowedAccountIds: [],
|
|
29
|
+
allowedRegions: ["us-east-1"],
|
|
30
|
+
allowedS3Buckets: [],
|
|
31
|
+
denyObjectKeyPrefixes: ["secrets/", ".env"],
|
|
32
|
+
};
|
|
33
|
+
const DEFAULT_GCP_READ_POLICY = {
|
|
34
|
+
enabled: true,
|
|
35
|
+
allowedProjectIds: [],
|
|
36
|
+
allowedStorageBuckets: [],
|
|
37
|
+
denyObjectNamePrefixes: ["secrets/", ".env"],
|
|
38
|
+
};
|
|
39
|
+
const DEFAULT_MCP_POLICY = {
|
|
40
|
+
enabled: true,
|
|
41
|
+
allowedTools: [],
|
|
42
|
+
};
|
|
43
|
+
const DEFAULT_DOCS_POLICY = {
|
|
44
|
+
enabled: true,
|
|
45
|
+
allowedWorkspaces: ["local"],
|
|
46
|
+
allowedPathPrefixes: ["README.md", "docs/", "product/"],
|
|
47
|
+
denyPathPrefixes: [".env", "secrets/", "private/"],
|
|
48
|
+
maxSearchResults: 10,
|
|
49
|
+
maxReadBytes: 20_000,
|
|
50
|
+
};
|
|
51
|
+
export const PolicyConfigSchema = z.object({
|
|
52
|
+
version: z.string().min(1).default("2026-05-30"),
|
|
53
|
+
github: z
|
|
54
|
+
.object({
|
|
55
|
+
pullRequests: z
|
|
56
|
+
.object({
|
|
57
|
+
enabled: z.boolean().default(true),
|
|
58
|
+
requiredBaseBranch: z.string().min(1).default("main"),
|
|
59
|
+
maxFilesChanged: z.number().int().nonnegative().default(12),
|
|
60
|
+
denyPathPrefixes: z.array(z.string().min(1)).default([".env", "secrets/"]),
|
|
61
|
+
stepUpPathPrefixes: z
|
|
62
|
+
.array(z.string().min(1))
|
|
63
|
+
.default(DEFAULT_GITHUB_PULL_REQUEST_POLICY.stepUpPathPrefixes),
|
|
64
|
+
requiresTests: z.boolean().default(true),
|
|
65
|
+
})
|
|
66
|
+
.default(DEFAULT_GITHUB_PULL_REQUEST_POLICY),
|
|
67
|
+
contents: z
|
|
68
|
+
.object({
|
|
69
|
+
enabled: z.boolean().default(true),
|
|
70
|
+
denyReadPathPrefixes: z
|
|
71
|
+
.array(z.string().min(1))
|
|
72
|
+
.default(DEFAULT_GITHUB_CONTENTS_POLICY.denyReadPathPrefixes),
|
|
73
|
+
})
|
|
74
|
+
.default(DEFAULT_GITHUB_CONTENTS_POLICY),
|
|
75
|
+
})
|
|
76
|
+
.default({
|
|
77
|
+
pullRequests: DEFAULT_GITHUB_PULL_REQUEST_POLICY,
|
|
78
|
+
contents: DEFAULT_GITHUB_CONTENTS_POLICY,
|
|
79
|
+
}),
|
|
80
|
+
slack: z
|
|
81
|
+
.object({
|
|
82
|
+
messages: z
|
|
83
|
+
.object({
|
|
84
|
+
enabled: z.boolean().default(true),
|
|
85
|
+
allowedChannels: z.array(z.string().min(1)).default(["#axtary-dev"]),
|
|
86
|
+
stepUpExternalRecipients: z.boolean().default(true),
|
|
87
|
+
})
|
|
88
|
+
.default(DEFAULT_SLACK_MESSAGES_POLICY),
|
|
89
|
+
})
|
|
90
|
+
.default({
|
|
91
|
+
messages: DEFAULT_SLACK_MESSAGES_POLICY,
|
|
92
|
+
}),
|
|
93
|
+
issueTrackers: z
|
|
94
|
+
.object({
|
|
95
|
+
linear: z
|
|
96
|
+
.object({
|
|
97
|
+
enabled: z.boolean().default(true),
|
|
98
|
+
allowedProjectKeys: z
|
|
99
|
+
.array(z.string().min(1))
|
|
100
|
+
.default(DEFAULT_ISSUE_TRACKER_POLICY.allowedProjectKeys),
|
|
101
|
+
stepUpStatuses: z
|
|
102
|
+
.array(z.string().min(1))
|
|
103
|
+
.default(DEFAULT_ISSUE_TRACKER_POLICY.stepUpStatuses),
|
|
104
|
+
})
|
|
105
|
+
.default(DEFAULT_ISSUE_TRACKER_POLICY),
|
|
106
|
+
jira: z
|
|
107
|
+
.object({
|
|
108
|
+
enabled: z.boolean().default(true),
|
|
109
|
+
allowedProjectKeys: z
|
|
110
|
+
.array(z.string().min(1))
|
|
111
|
+
.default(DEFAULT_ISSUE_TRACKER_POLICY.allowedProjectKeys),
|
|
112
|
+
stepUpStatuses: z
|
|
113
|
+
.array(z.string().min(1))
|
|
114
|
+
.default(DEFAULT_ISSUE_TRACKER_POLICY.stepUpStatuses),
|
|
115
|
+
})
|
|
116
|
+
.default(DEFAULT_ISSUE_TRACKER_POLICY),
|
|
117
|
+
})
|
|
118
|
+
.default({
|
|
119
|
+
linear: DEFAULT_ISSUE_TRACKER_POLICY,
|
|
120
|
+
jira: DEFAULT_ISSUE_TRACKER_POLICY,
|
|
121
|
+
}),
|
|
122
|
+
cloud: z
|
|
123
|
+
.object({
|
|
124
|
+
aws: z
|
|
125
|
+
.object({
|
|
126
|
+
enabled: z.boolean().default(true),
|
|
127
|
+
allowedAccountIds: z.array(z.string().min(1)).default([]),
|
|
128
|
+
allowedRegions: z.array(z.string().min(1)).default(["us-east-1"]),
|
|
129
|
+
allowedS3Buckets: z.array(z.string().min(1)).default([]),
|
|
130
|
+
denyObjectKeyPrefixes: z.array(z.string().min(1)).default(["secrets/", ".env"]),
|
|
131
|
+
})
|
|
132
|
+
.default(DEFAULT_AWS_READ_POLICY),
|
|
133
|
+
gcp: z
|
|
134
|
+
.object({
|
|
135
|
+
enabled: z.boolean().default(true),
|
|
136
|
+
allowedProjectIds: z.array(z.string().min(1)).default([]),
|
|
137
|
+
allowedStorageBuckets: z.array(z.string().min(1)).default([]),
|
|
138
|
+
denyObjectNamePrefixes: z
|
|
139
|
+
.array(z.string().min(1))
|
|
140
|
+
.default(["secrets/", ".env"]),
|
|
141
|
+
})
|
|
142
|
+
.default(DEFAULT_GCP_READ_POLICY),
|
|
143
|
+
})
|
|
144
|
+
.default({
|
|
145
|
+
aws: DEFAULT_AWS_READ_POLICY,
|
|
146
|
+
gcp: DEFAULT_GCP_READ_POLICY,
|
|
147
|
+
}),
|
|
148
|
+
mcp: z
|
|
149
|
+
.object({
|
|
150
|
+
enabled: z.boolean().default(true),
|
|
151
|
+
allowedTools: z
|
|
152
|
+
.array(z.object({
|
|
153
|
+
serverIdentity: z.string().min(1),
|
|
154
|
+
toolName: z.string().min(1),
|
|
155
|
+
definitionHash: z
|
|
156
|
+
.string()
|
|
157
|
+
.regex(/^sha256:[a-f0-9]{64}$/),
|
|
158
|
+
}))
|
|
159
|
+
.default([]),
|
|
160
|
+
})
|
|
161
|
+
.default(DEFAULT_MCP_POLICY),
|
|
162
|
+
docs: z
|
|
163
|
+
.object({
|
|
164
|
+
documents: z
|
|
165
|
+
.object({
|
|
166
|
+
enabled: z.boolean().default(true),
|
|
167
|
+
allowedWorkspaces: z
|
|
168
|
+
.array(z.string().min(1))
|
|
169
|
+
.default(DEFAULT_DOCS_POLICY.allowedWorkspaces),
|
|
170
|
+
allowedPathPrefixes: z
|
|
171
|
+
.array(z.string().min(1))
|
|
172
|
+
.default(DEFAULT_DOCS_POLICY.allowedPathPrefixes),
|
|
173
|
+
denyPathPrefixes: z
|
|
174
|
+
.array(z.string().min(1))
|
|
175
|
+
.default(DEFAULT_DOCS_POLICY.denyPathPrefixes),
|
|
176
|
+
maxSearchResults: z.number().int().positive().default(10),
|
|
177
|
+
maxReadBytes: z.number().int().positive().default(20_000),
|
|
178
|
+
})
|
|
179
|
+
.default(DEFAULT_DOCS_POLICY),
|
|
180
|
+
})
|
|
181
|
+
.default({
|
|
182
|
+
documents: DEFAULT_DOCS_POLICY,
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
export const PolicyFixtureInputSchema = z.object({
|
|
186
|
+
id: z.string().min(1).optional(),
|
|
187
|
+
name: z.string().min(1).optional(),
|
|
188
|
+
expectedDecision: z.enum(["allow", "deny", "step_up"]),
|
|
189
|
+
expectedReasons: z.array(z.string().min(1)).optional(),
|
|
190
|
+
action: NormalizedActionSchema,
|
|
191
|
+
sourcePath: z.string().min(1).optional(),
|
|
192
|
+
});
|
|
193
|
+
export const DEFAULT_POLICY_CONFIG = PolicyConfigSchema.parse({});
|
|
194
|
+
export const DEFAULT_POLICY_FIXTURE_SOURCE = {
|
|
195
|
+
kind: "package_defaults",
|
|
196
|
+
label: "package defaults from @axtary/policy",
|
|
197
|
+
path: null,
|
|
198
|
+
};
|
|
199
|
+
export const POLICY_TEMPLATES = [
|
|
200
|
+
{
|
|
201
|
+
id: "coding-agent-repo-guardrails",
|
|
202
|
+
name: "Coding agent repo guardrails",
|
|
203
|
+
category: "coding_agent",
|
|
204
|
+
summary: "Allow routine PR work while denying secret reads and stepping up auth, billing, and production paths.",
|
|
205
|
+
tools: [
|
|
206
|
+
"github.pull_requests.create",
|
|
207
|
+
"github.branches.create",
|
|
208
|
+
"github.contents.read",
|
|
209
|
+
"github.contents.write",
|
|
210
|
+
],
|
|
211
|
+
decisionModel: "step_up",
|
|
212
|
+
config: {
|
|
213
|
+
github: {
|
|
214
|
+
pullRequests: {
|
|
215
|
+
requiredBaseBranch: "main",
|
|
216
|
+
maxFilesChanged: 12,
|
|
217
|
+
denyPathPrefixes: [".env", "secrets/"],
|
|
218
|
+
stepUpPathPrefixes: ["infra/prod/", "billing/", "auth/"],
|
|
219
|
+
requiresTests: true,
|
|
220
|
+
},
|
|
221
|
+
contents: {
|
|
222
|
+
denyReadPathPrefixes: [".env", "secrets/", "infra/prod/"],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
evidence: ["filesChanged", "baseBranch", "testsPassed", "payloadHash"],
|
|
227
|
+
notes: [
|
|
228
|
+
"Matches the current coding-agent wedge.",
|
|
229
|
+
"Protected path changes require exact approval evidence before execution.",
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "staging-cloud-read-only",
|
|
234
|
+
name: "Staging cloud read-only",
|
|
235
|
+
category: "cloud_read",
|
|
236
|
+
summary: "Permit bounded AWS/GCP inventory and object listing only after account, project, region, and bucket scopes are pinned.",
|
|
237
|
+
tools: [
|
|
238
|
+
"aws.identity.get",
|
|
239
|
+
"aws.s3.objects.list",
|
|
240
|
+
"gcp.projects.get",
|
|
241
|
+
"gcp.storage.objects.list",
|
|
242
|
+
],
|
|
243
|
+
decisionModel: "deny_until_scoped",
|
|
244
|
+
config: {
|
|
245
|
+
cloud: {
|
|
246
|
+
aws: {
|
|
247
|
+
enabled: true,
|
|
248
|
+
allowedAccountIds: [],
|
|
249
|
+
allowedRegions: ["us-east-1"],
|
|
250
|
+
allowedS3Buckets: [],
|
|
251
|
+
denyObjectKeyPrefixes: ["secrets/", ".env"],
|
|
252
|
+
},
|
|
253
|
+
gcp: {
|
|
254
|
+
enabled: true,
|
|
255
|
+
allowedProjectIds: [],
|
|
256
|
+
allowedStorageBuckets: [],
|
|
257
|
+
denyObjectNamePrefixes: ["secrets/", ".env"],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
evidence: ["accountId", "region", "projectId", "bucket", "prefix"],
|
|
262
|
+
notes: [
|
|
263
|
+
"Empty allowlists intentionally fail closed until customer scopes are configured.",
|
|
264
|
+
"Smoke checks stay read/auth-only.",
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
id: "production-impact-step-up",
|
|
269
|
+
name: "Production impact step-up",
|
|
270
|
+
category: "production_change",
|
|
271
|
+
summary: "Route production-impact declarations and protected infrastructure paths through approval.",
|
|
272
|
+
tools: ["github.pull_requests.create", "github.contents.write"],
|
|
273
|
+
decisionModel: "step_up",
|
|
274
|
+
config: {
|
|
275
|
+
github: {
|
|
276
|
+
pullRequests: {
|
|
277
|
+
stepUpPathPrefixes: ["infra/prod/", "deploy/", "auth/", "billing/"],
|
|
278
|
+
requiresTests: true,
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
evidence: ["touchesProduction", "filesChanged", "diff", "payloadHash"],
|
|
283
|
+
notes: [
|
|
284
|
+
"The native evaluator already steps up PR payloads with touchesProduction=true.",
|
|
285
|
+
"Future cloud write tools should attach the same production-impact evidence before execution.",
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: "iam-mutation-deny",
|
|
290
|
+
name: "IAM mutation deny",
|
|
291
|
+
category: "identity_mutation",
|
|
292
|
+
summary: "Keep identity and credential mutations denied until a scoped connector and reviewer workflow exist.",
|
|
293
|
+
tools: [
|
|
294
|
+
"aws.iam.roles.update",
|
|
295
|
+
"aws.iam.policies.attach",
|
|
296
|
+
"gcp.iam.bindings.set",
|
|
297
|
+
"gcp.serviceAccounts.keys.create",
|
|
298
|
+
],
|
|
299
|
+
decisionModel: "deny",
|
|
300
|
+
config: {},
|
|
301
|
+
evidence: ["resource", "role", "principal", "payloadHash"],
|
|
302
|
+
notes: [
|
|
303
|
+
"These tools are not registered executable handlers yet; unsupported tools fail closed.",
|
|
304
|
+
"When added, they should require reviewer permissions and short-lived/federated credentials.",
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "external-slack-step-up",
|
|
309
|
+
name: "External Slack step-up",
|
|
310
|
+
category: "external_communication",
|
|
311
|
+
summary: "Allow approved internal channels and require approval when a message reaches external recipients.",
|
|
312
|
+
tools: ["slack.chat.postMessage"],
|
|
313
|
+
decisionModel: "step_up",
|
|
314
|
+
config: {
|
|
315
|
+
slack: {
|
|
316
|
+
messages: {
|
|
317
|
+
allowedChannels: ["#axtary-dev"],
|
|
318
|
+
stepUpExternalRecipients: true,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
evidence: ["channel", "message", "externalRecipients", "payloadHash"],
|
|
323
|
+
notes: [
|
|
324
|
+
"Exact message payload is approval evidence.",
|
|
325
|
+
"Unapproved channels are denied rather than stepped up.",
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: "local-docs-read-scoped",
|
|
330
|
+
name: "Local docs read scoped",
|
|
331
|
+
category: "docs_read",
|
|
332
|
+
summary: "Permit local docs search and read actions only inside configured workspaces, paths, result caps, and byte limits.",
|
|
333
|
+
tools: ["docs.documents.search", "docs.documents.read"],
|
|
334
|
+
decisionModel: "deny_until_scoped",
|
|
335
|
+
config: {
|
|
336
|
+
docs: {
|
|
337
|
+
documents: {
|
|
338
|
+
enabled: true,
|
|
339
|
+
allowedWorkspaces: ["local"],
|
|
340
|
+
allowedPathPrefixes: ["README.md", "docs/", "product/"],
|
|
341
|
+
denyPathPrefixes: [".env", "secrets/", "private/"],
|
|
342
|
+
maxSearchResults: 10,
|
|
343
|
+
maxReadBytes: 20_000,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
evidence: ["workspace", "path", "query", "maxResults", "maxBytes"],
|
|
348
|
+
notes: [
|
|
349
|
+
"Local document roots are configured adapter-side, while policy controls normalized action scope.",
|
|
350
|
+
"Returned content should be bounded and redacted by the connector before the agent sees it.",
|
|
351
|
+
],
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
export const DEFAULT_POLICY_REVIEW_FIXTURES = [
|
|
355
|
+
PolicyFixtureInputSchema.parse({
|
|
356
|
+
id: "allow-github-pr",
|
|
357
|
+
name: "allow GitHub PR inside configured branch, paths, and test policy",
|
|
358
|
+
expectedDecision: "allow",
|
|
359
|
+
expectedReasons: ["github_pr_inside_policy"],
|
|
360
|
+
action: {
|
|
361
|
+
schemaVersion: "axtary.action.v0",
|
|
362
|
+
actor: {
|
|
363
|
+
agentId: "agent:codex-prod",
|
|
364
|
+
humanOwner: "user:asrar@company.com",
|
|
365
|
+
runtime: "codex-cli",
|
|
366
|
+
tenant: "org:company",
|
|
367
|
+
},
|
|
368
|
+
intent: {
|
|
369
|
+
taskId: "AXT-418",
|
|
370
|
+
declaredGoal: "Open a PR that fixes the auth redirect bug",
|
|
371
|
+
},
|
|
372
|
+
capability: {
|
|
373
|
+
tool: "github.pull_requests.create",
|
|
374
|
+
resource: "repo:company/web-app",
|
|
375
|
+
payload: {
|
|
376
|
+
baseBranch: "main",
|
|
377
|
+
filesChanged: ["src/app/login/page.tsx", "src/lib/auth/session.ts"],
|
|
378
|
+
testsPassed: true,
|
|
379
|
+
touchesProduction: false,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
384
|
+
PolicyFixtureInputSchema.parse({
|
|
385
|
+
id: "deny-secret-read",
|
|
386
|
+
name: "deny GitHub content read of secret path",
|
|
387
|
+
expectedDecision: "deny",
|
|
388
|
+
expectedReasons: ["content_read_denied_path"],
|
|
389
|
+
action: {
|
|
390
|
+
schemaVersion: "axtary.action.v0",
|
|
391
|
+
actor: {
|
|
392
|
+
agentId: "agent:codex-prod",
|
|
393
|
+
humanOwner: "user:asrar@company.com",
|
|
394
|
+
runtime: "codex-cli",
|
|
395
|
+
tenant: "org:company",
|
|
396
|
+
},
|
|
397
|
+
intent: {
|
|
398
|
+
taskId: "AXT-418",
|
|
399
|
+
declaredGoal: "Read files required for the auth redirect bug",
|
|
400
|
+
},
|
|
401
|
+
capability: {
|
|
402
|
+
tool: "github.contents.read",
|
|
403
|
+
resource: "repo:company/web-app",
|
|
404
|
+
payload: {
|
|
405
|
+
path: ".env.production",
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
}),
|
|
410
|
+
PolicyFixtureInputSchema.parse({
|
|
411
|
+
id: "step-up-linear-done",
|
|
412
|
+
name: "step up Linear issue completion",
|
|
413
|
+
expectedDecision: "step_up",
|
|
414
|
+
expectedReasons: ["linear_status_requires_step_up"],
|
|
415
|
+
action: {
|
|
416
|
+
schemaVersion: "axtary.action.v0",
|
|
417
|
+
actor: {
|
|
418
|
+
agentId: "agent:codex-prod",
|
|
419
|
+
humanOwner: "user:asrar@company.com",
|
|
420
|
+
runtime: "codex-cli",
|
|
421
|
+
tenant: "org:company",
|
|
422
|
+
},
|
|
423
|
+
intent: {
|
|
424
|
+
taskId: "AXT-418",
|
|
425
|
+
declaredGoal: "Mark the source Linear issue as done after the PR",
|
|
426
|
+
},
|
|
427
|
+
capability: {
|
|
428
|
+
tool: "linear.issues.update",
|
|
429
|
+
resource: "linear:workspace/company",
|
|
430
|
+
payload: {
|
|
431
|
+
issueKey: "AXT-418",
|
|
432
|
+
projectKey: "AXT",
|
|
433
|
+
status: "Done",
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
}),
|
|
438
|
+
];
|
|
439
|
+
export function getPolicyTemplates() {
|
|
440
|
+
return POLICY_TEMPLATES;
|
|
441
|
+
}
|
|
442
|
+
export function getPolicyTemplate(id) {
|
|
443
|
+
return POLICY_TEMPLATES.find((template) => template.id === id) ?? null;
|
|
444
|
+
}
|
|
445
|
+
export function mergePolicyConfig(baseInput = {}, patchInput = {}) {
|
|
446
|
+
const base = PolicyConfigSchema.parse(baseInput);
|
|
447
|
+
const patch = deepMerge(base, patchInput);
|
|
448
|
+
return PolicyConfigSchema.parse(patch);
|
|
449
|
+
}
|
|
450
|
+
export function applyPolicyTemplate(baseInput, templateId) {
|
|
451
|
+
const template = getPolicyTemplate(templateId);
|
|
452
|
+
if (!template) {
|
|
453
|
+
throw new Error(`policy_template_not_found:${templateId}`);
|
|
454
|
+
}
|
|
455
|
+
return mergePolicyConfig(baseInput, template.config);
|
|
456
|
+
}
|
|
457
|
+
export function explainPolicyDecision(decisionInput) {
|
|
458
|
+
const reviewerAction = decisionInput.decision === "allow"
|
|
459
|
+
? "none"
|
|
460
|
+
: decisionInput.decision === "step_up"
|
|
461
|
+
? "approval_required"
|
|
462
|
+
: "block";
|
|
463
|
+
const label = decisionInput.decision === "allow"
|
|
464
|
+
? "Allowed"
|
|
465
|
+
: decisionInput.decision === "step_up"
|
|
466
|
+
? "Step-up required"
|
|
467
|
+
: "Denied";
|
|
468
|
+
return {
|
|
469
|
+
label,
|
|
470
|
+
reviewerAction,
|
|
471
|
+
summary: decisionSummary(decisionInput),
|
|
472
|
+
reasonDetails: decisionInput.reasons.map((reason) => ({
|
|
473
|
+
reason,
|
|
474
|
+
detail: reasonDetail(reason),
|
|
475
|
+
})),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
export function runPolicyFixtures(configInput = {}, fixtureInputs = DEFAULT_POLICY_REVIEW_FIXTURES) {
|
|
479
|
+
const config = PolicyConfigSchema.parse(configInput);
|
|
480
|
+
return fixtureInputs.map((fixtureInput, index) => {
|
|
481
|
+
const fixture = PolicyFixtureInputSchema.parse(fixtureInput);
|
|
482
|
+
const action = NormalizedActionSchema.parse(fixture.action);
|
|
483
|
+
const decisionResult = evaluatePolicy(action, config);
|
|
484
|
+
const expectedReasons = fixture.expectedReasons ?? null;
|
|
485
|
+
const reasonsPassed = !expectedReasons || arraysEqual(expectedReasons, decisionResult.reasons);
|
|
486
|
+
return {
|
|
487
|
+
id: fixture.id ?? `fixture-${index + 1}`,
|
|
488
|
+
name: fixture.name ?? `Policy fixture ${index + 1}`,
|
|
489
|
+
tool: action.capability.tool,
|
|
490
|
+
resource: action.capability.resource,
|
|
491
|
+
expectedDecision: fixture.expectedDecision,
|
|
492
|
+
actualDecision: decisionResult.decision,
|
|
493
|
+
expectedReasons,
|
|
494
|
+
actualReasons: decisionResult.reasons,
|
|
495
|
+
sourcePath: fixture.sourcePath ?? null,
|
|
496
|
+
passed: decisionResult.decision === fixture.expectedDecision && reasonsPassed,
|
|
497
|
+
decision: decisionResult,
|
|
498
|
+
explanation: explainPolicyDecision(decisionResult),
|
|
499
|
+
cedar: toCedarRequest(action),
|
|
500
|
+
opa: toOpaInput(action),
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
export function buildPolicyReview(configInput = {}, fixtureInputs = DEFAULT_POLICY_REVIEW_FIXTURES, source = DEFAULT_POLICY_FIXTURE_SOURCE) {
|
|
505
|
+
const config = PolicyConfigSchema.parse(configInput);
|
|
506
|
+
const fixtures = runPolicyFixtures(config, fixtureInputs);
|
|
507
|
+
return {
|
|
508
|
+
schemaVersion: POLICY_SCHEMA_VERSION,
|
|
509
|
+
policyVersion: config.version,
|
|
510
|
+
nativeRule: "axtary_policy_v0",
|
|
511
|
+
source,
|
|
512
|
+
summary: {
|
|
513
|
+
totalFixtures: fixtures.length,
|
|
514
|
+
passedFixtures: fixtures.filter((fixture) => fixture.passed).length,
|
|
515
|
+
failedFixtures: fixtures.filter((fixture) => !fixture.passed).length,
|
|
516
|
+
allow: fixtures.filter((fixture) => fixture.actualDecision === "allow").length,
|
|
517
|
+
deny: fixtures.filter((fixture) => fixture.actualDecision === "deny").length,
|
|
518
|
+
stepUp: fixtures.filter((fixture) => fixture.actualDecision === "step_up").length,
|
|
519
|
+
},
|
|
520
|
+
fixtures,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
export function previewPolicyTemplates(configInput = {}, templates = getPolicyTemplates(), fixtureInputs = DEFAULT_POLICY_REVIEW_FIXTURES) {
|
|
524
|
+
const config = PolicyConfigSchema.parse(configInput);
|
|
525
|
+
const beforeFixtures = runPolicyFixtures(config, fixtureInputs);
|
|
526
|
+
return templates.map((template) => {
|
|
527
|
+
const resultingConfig = mergePolicyConfig(config, template.config);
|
|
528
|
+
const afterFixtures = runPolicyFixtures(resultingConfig, fixtureInputs);
|
|
529
|
+
const fixtureOutcomeChanges = afterFixtures.flatMap((afterFixture) => {
|
|
530
|
+
const beforeFixture = beforeFixtures.find((fixture) => fixture.id === afterFixture.id);
|
|
531
|
+
if (!beforeFixture || !fixtureOutcomeChanged(beforeFixture, afterFixture)) {
|
|
532
|
+
return [];
|
|
533
|
+
}
|
|
534
|
+
return [
|
|
535
|
+
{
|
|
536
|
+
fixtureId: afterFixture.id,
|
|
537
|
+
name: afterFixture.name,
|
|
538
|
+
tool: afterFixture.tool,
|
|
539
|
+
beforeDecision: beforeFixture.actualDecision,
|
|
540
|
+
afterDecision: afterFixture.actualDecision,
|
|
541
|
+
beforePassed: beforeFixture.passed,
|
|
542
|
+
afterPassed: afterFixture.passed,
|
|
543
|
+
beforeReasons: beforeFixture.actualReasons,
|
|
544
|
+
afterReasons: afterFixture.actualReasons,
|
|
545
|
+
},
|
|
546
|
+
];
|
|
547
|
+
});
|
|
548
|
+
return {
|
|
549
|
+
templateId: template.id,
|
|
550
|
+
name: template.name,
|
|
551
|
+
category: template.category,
|
|
552
|
+
decisionModel: template.decisionModel,
|
|
553
|
+
changedSections: collectPatchPaths(template.config),
|
|
554
|
+
evidence: template.evidence,
|
|
555
|
+
fixtureOutcomeSummary: {
|
|
556
|
+
totalFixtures: beforeFixtures.length,
|
|
557
|
+
changedFixtures: fixtureOutcomeChanges.length,
|
|
558
|
+
newlyFailingFixtures: fixtureOutcomeChanges.filter((change) => change.beforePassed && !change.afterPassed).length,
|
|
559
|
+
newlyPassingFixtures: fixtureOutcomeChanges.filter((change) => !change.beforePassed && change.afterPassed).length,
|
|
560
|
+
},
|
|
561
|
+
fixtureOutcomeChanges,
|
|
562
|
+
resultingConfig,
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
export function evaluatePolicy(actionInput, configInput = {}) {
|
|
567
|
+
const action = NormalizedActionSchema.parse(actionInput);
|
|
568
|
+
const config = PolicyConfigSchema.parse(configInput);
|
|
569
|
+
switch (action.capability.tool) {
|
|
570
|
+
case "github.pull_requests.create":
|
|
571
|
+
return evaluateGitHubPullRequest(action, config);
|
|
572
|
+
case "github.contents.read":
|
|
573
|
+
return evaluateGitHubContentRead(action, config);
|
|
574
|
+
case "github.contents.write":
|
|
575
|
+
return evaluateGitHubContentWrite(action, config);
|
|
576
|
+
case "github.branches.create":
|
|
577
|
+
return evaluateGitHubBranchCreate(action, config);
|
|
578
|
+
case "slack.chat.postMessage":
|
|
579
|
+
return evaluateSlackMessage(action, config);
|
|
580
|
+
case "linear.issues.read":
|
|
581
|
+
case "linear.comments.create":
|
|
582
|
+
case "linear.issues.update":
|
|
583
|
+
return evaluateIssueTrackerAction(action, config, "linear");
|
|
584
|
+
case "jira.issues.read":
|
|
585
|
+
case "jira.comments.create":
|
|
586
|
+
case "jira.issues.update":
|
|
587
|
+
return evaluateIssueTrackerAction(action, config, "jira");
|
|
588
|
+
case "aws.identity.get":
|
|
589
|
+
case "aws.s3.objects.list":
|
|
590
|
+
return evaluateAwsRead(action, config);
|
|
591
|
+
case "gcp.projects.get":
|
|
592
|
+
case "gcp.storage.objects.list":
|
|
593
|
+
return evaluateGcpRead(action, config);
|
|
594
|
+
case "docs.documents.search":
|
|
595
|
+
case "docs.documents.read":
|
|
596
|
+
return evaluateDocsDocument(action, config);
|
|
597
|
+
case "mcp.tool.call":
|
|
598
|
+
return evaluateMcpToolCall(action, config);
|
|
599
|
+
default:
|
|
600
|
+
return decision("deny", [`unsupported_tool:${action.capability.tool}`], config, {
|
|
601
|
+
maxFilesChanged: 0,
|
|
602
|
+
blockedPaths: [],
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
function evaluateGitHubContentWrite(action, config) {
|
|
607
|
+
const policy = config.github.pullRequests;
|
|
608
|
+
const path = getString(action.capability.payload.path);
|
|
609
|
+
const branch = getString(action.capability.payload.branch);
|
|
610
|
+
if (!path) {
|
|
611
|
+
return decision("deny", ["github_content_write_missing_path"], config, {
|
|
612
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
613
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
if (!branch) {
|
|
617
|
+
return decision("deny", ["github_content_write_missing_branch"], config, {
|
|
618
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
619
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
if (pathsMatch([path], policy.denyPathPrefixes)) {
|
|
623
|
+
return decision("deny", ["payload_touches_denied_path"], config, {
|
|
624
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
625
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
if (pathsMatch([path], policy.stepUpPathPrefixes)) {
|
|
629
|
+
return decision("step_up", ["payload_touches_step_up_path"], config, {
|
|
630
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
631
|
+
blockedPaths: [...policy.denyPathPrefixes, ...policy.stepUpPathPrefixes],
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return decision("allow", ["github_content_write_inside_policy"], config, {
|
|
635
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
636
|
+
blockedPaths: [...policy.denyPathPrefixes, ...policy.stepUpPathPrefixes],
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function evaluateGitHubBranchCreate(action, config) {
|
|
640
|
+
const policy = config.github.pullRequests;
|
|
641
|
+
const branch = getString(action.capability.payload.branch);
|
|
642
|
+
const baseBranch = getString(action.capability.payload.baseBranch);
|
|
643
|
+
if (!branch) {
|
|
644
|
+
return decision("deny", ["github_branch_missing_name"], config, {
|
|
645
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
646
|
+
blockedPaths: [],
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (baseBranch && baseBranch !== policy.requiredBaseBranch) {
|
|
650
|
+
return decision("step_up", [`base_branch_mismatch:${policy.requiredBaseBranch}`], config, {
|
|
651
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
652
|
+
blockedPaths: [],
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return decision("allow", ["github_branch_inside_policy"], config, {
|
|
656
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
657
|
+
blockedPaths: [],
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
export function toCedarRequest(actionInput) {
|
|
661
|
+
const action = NormalizedActionSchema.parse(actionInput);
|
|
662
|
+
return {
|
|
663
|
+
principal: {
|
|
664
|
+
type: "Agent",
|
|
665
|
+
id: action.actor.agentId,
|
|
666
|
+
},
|
|
667
|
+
action: {
|
|
668
|
+
type: "Action",
|
|
669
|
+
id: action.capability.tool,
|
|
670
|
+
},
|
|
671
|
+
resource: resourceToCedar(action.capability.resource),
|
|
672
|
+
context: {
|
|
673
|
+
human_owner: action.actor.humanOwner,
|
|
674
|
+
runtime: action.actor.runtime,
|
|
675
|
+
tenant: action.actor.tenant ?? null,
|
|
676
|
+
task_id: action.intent.taskId,
|
|
677
|
+
declared_goal: action.intent.declaredGoal,
|
|
678
|
+
payload: action.capability.payload,
|
|
679
|
+
constraints: action.capability.constraints ?? null,
|
|
680
|
+
tool_definition: action.toolDefinition ?? null,
|
|
681
|
+
budget: action.budget ?? null,
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
export function toOpaInput(actionInput) {
|
|
686
|
+
const action = NormalizedActionSchema.parse(actionInput);
|
|
687
|
+
return {
|
|
688
|
+
input: {
|
|
689
|
+
schemaVersion: POLICY_SCHEMA_VERSION,
|
|
690
|
+
action,
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function evaluateGitHubPullRequest(action, config) {
|
|
695
|
+
const policy = config.github.pullRequests;
|
|
696
|
+
if (!policy.enabled) {
|
|
697
|
+
return decision("deny", ["github_pr_policy_disabled"], config, {
|
|
698
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
699
|
+
blockedPaths: [...policy.denyPathPrefixes, ...policy.stepUpPathPrefixes],
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
const payload = action.capability.payload;
|
|
703
|
+
const filesChanged = getStringArray(payload.filesChanged);
|
|
704
|
+
const denyReasons = [];
|
|
705
|
+
const stepUpReasons = [];
|
|
706
|
+
if (payload.baseBranch !== policy.requiredBaseBranch) {
|
|
707
|
+
stepUpReasons.push(`base_branch_mismatch:${policy.requiredBaseBranch}`);
|
|
708
|
+
}
|
|
709
|
+
if (filesChanged.length > policy.maxFilesChanged) {
|
|
710
|
+
stepUpReasons.push("file_count_exceeds_policy_limit");
|
|
711
|
+
}
|
|
712
|
+
if (pathsMatch(filesChanged, policy.denyPathPrefixes)) {
|
|
713
|
+
denyReasons.push("payload_touches_denied_path");
|
|
714
|
+
}
|
|
715
|
+
if (pathsMatch(filesChanged, policy.stepUpPathPrefixes)) {
|
|
716
|
+
stepUpReasons.push("payload_touches_step_up_path");
|
|
717
|
+
}
|
|
718
|
+
if (policy.requiresTests && payload.testsPassed !== true) {
|
|
719
|
+
stepUpReasons.push("tests_required_before_pr");
|
|
720
|
+
}
|
|
721
|
+
if (payload.touchesProduction === true) {
|
|
722
|
+
stepUpReasons.push("payload_declares_production_impact");
|
|
723
|
+
}
|
|
724
|
+
if (denyReasons.length > 0) {
|
|
725
|
+
return decision("deny", denyReasons, config, {
|
|
726
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
727
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (stepUpReasons.length > 0) {
|
|
731
|
+
return decision("step_up", stepUpReasons, config, {
|
|
732
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
733
|
+
blockedPaths: [...policy.denyPathPrefixes, ...policy.stepUpPathPrefixes],
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return decision("allow", ["github_pr_inside_policy"], config, {
|
|
737
|
+
maxFilesChanged: policy.maxFilesChanged,
|
|
738
|
+
blockedPaths: [...policy.denyPathPrefixes, ...policy.stepUpPathPrefixes],
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
function evaluateGitHubContentRead(action, config) {
|
|
742
|
+
const policy = config.github.contents;
|
|
743
|
+
const path = getString(action.capability.payload.path);
|
|
744
|
+
if (!policy.enabled) {
|
|
745
|
+
return decision("deny", ["github_contents_policy_disabled"], config, {
|
|
746
|
+
maxFilesChanged: 0,
|
|
747
|
+
blockedPaths: policy.denyReadPathPrefixes,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
if (!path) {
|
|
751
|
+
return decision("deny", ["missing_content_path"], config, {
|
|
752
|
+
maxFilesChanged: 0,
|
|
753
|
+
blockedPaths: policy.denyReadPathPrefixes,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
if (pathsMatch([path], policy.denyReadPathPrefixes)) {
|
|
757
|
+
return decision("deny", ["content_read_denied_path"], config, {
|
|
758
|
+
maxFilesChanged: 0,
|
|
759
|
+
blockedPaths: policy.denyReadPathPrefixes,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return decision("allow", ["content_read_inside_policy"], config, {
|
|
763
|
+
maxFilesChanged: 0,
|
|
764
|
+
blockedPaths: policy.denyReadPathPrefixes,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
function evaluateSlackMessage(action, config) {
|
|
768
|
+
const policy = config.slack.messages;
|
|
769
|
+
const channel = getString(action.capability.payload.channel);
|
|
770
|
+
const externalRecipients = getStringArray(action.capability.payload.externalRecipients);
|
|
771
|
+
if (!policy.enabled) {
|
|
772
|
+
return decision("deny", ["slack_message_policy_disabled"], config, {
|
|
773
|
+
maxFilesChanged: 0,
|
|
774
|
+
blockedPaths: [],
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (!channel || !policy.allowedChannels.includes(channel)) {
|
|
778
|
+
return decision("deny", ["slack_channel_not_allowed"], config, {
|
|
779
|
+
maxFilesChanged: 0,
|
|
780
|
+
blockedPaths: [],
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
if (policy.stepUpExternalRecipients && externalRecipients.length > 0) {
|
|
784
|
+
return decision("step_up", ["slack_external_recipient_step_up"], config, {
|
|
785
|
+
maxFilesChanged: 0,
|
|
786
|
+
blockedPaths: [],
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
return decision("allow", ["slack_message_inside_policy"], config, {
|
|
790
|
+
maxFilesChanged: 0,
|
|
791
|
+
blockedPaths: [],
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
function evaluateIssueTrackerAction(action, config, provider) {
|
|
795
|
+
const policy = config.issueTrackers[provider];
|
|
796
|
+
const payload = action.capability.payload;
|
|
797
|
+
const issueKey = getString(payload.issueKey);
|
|
798
|
+
const projectKey = getString(payload.projectKey) ?? issueKey?.split("-", 1)[0] ?? null;
|
|
799
|
+
if (!policy.enabled) {
|
|
800
|
+
return decision("deny", [`${provider}_issue_tracker_policy_disabled`], config, {
|
|
801
|
+
maxFilesChanged: 0,
|
|
802
|
+
blockedPaths: [],
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
if (!issueKey || !projectKey) {
|
|
806
|
+
return decision("deny", [`${provider}_issue_missing_key`], config, {
|
|
807
|
+
maxFilesChanged: 0,
|
|
808
|
+
blockedPaths: [],
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
if (!policy.allowedProjectKeys.includes(projectKey)) {
|
|
812
|
+
return decision("deny", [`${provider}_project_not_allowed`], config, {
|
|
813
|
+
maxFilesChanged: 0,
|
|
814
|
+
blockedPaths: [],
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (action.capability.tool.endsWith(".comments.create")) {
|
|
818
|
+
const body = getString(payload.body);
|
|
819
|
+
if (!body) {
|
|
820
|
+
return decision("deny", [`${provider}_comment_missing_body`], config, {
|
|
821
|
+
maxFilesChanged: 0,
|
|
822
|
+
blockedPaths: [],
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (action.capability.tool.endsWith(".issues.update")) {
|
|
827
|
+
const status = getString(payload.status);
|
|
828
|
+
if (status && policy.stepUpStatuses.includes(status)) {
|
|
829
|
+
return decision("step_up", [`${provider}_status_requires_step_up`], config, {
|
|
830
|
+
maxFilesChanged: 0,
|
|
831
|
+
blockedPaths: [],
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return decision("allow", [`${provider}_issue_inside_policy`], config, {
|
|
836
|
+
maxFilesChanged: 0,
|
|
837
|
+
blockedPaths: [],
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
function evaluateAwsRead(action, config) {
|
|
841
|
+
const policy = config.cloud.aws;
|
|
842
|
+
if (!policy.enabled) {
|
|
843
|
+
return decision("deny", ["aws_read_policy_disabled"], config, {
|
|
844
|
+
maxFilesChanged: 0,
|
|
845
|
+
blockedPaths: policy.denyObjectKeyPrefixes,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
if (action.capability.tool === "aws.identity.get") {
|
|
849
|
+
return decision("allow", ["aws_identity_read_inside_policy"], config, {
|
|
850
|
+
maxFilesChanged: 0,
|
|
851
|
+
blockedPaths: [],
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
const bucket = getString(action.capability.payload.bucket);
|
|
855
|
+
const prefix = getString(action.capability.payload.prefix) ?? "";
|
|
856
|
+
const region = getString(action.capability.payload.region) ?? "us-east-1";
|
|
857
|
+
const accountId = getString(action.capability.payload.accountId);
|
|
858
|
+
if (!bucket) {
|
|
859
|
+
return decision("deny", ["aws_s3_missing_bucket"], config, {
|
|
860
|
+
maxFilesChanged: 0,
|
|
861
|
+
blockedPaths: policy.denyObjectKeyPrefixes,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
if (policy.allowedAccountIds.length > 0 &&
|
|
865
|
+
(!accountId || !policy.allowedAccountIds.includes(accountId))) {
|
|
866
|
+
return decision("deny", ["aws_account_not_allowed"], config, {
|
|
867
|
+
maxFilesChanged: 0,
|
|
868
|
+
blockedPaths: policy.denyObjectKeyPrefixes,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
if (!policy.allowedRegions.includes(region)) {
|
|
872
|
+
return decision("deny", ["aws_region_not_allowed"], config, {
|
|
873
|
+
maxFilesChanged: 0,
|
|
874
|
+
blockedPaths: policy.denyObjectKeyPrefixes,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
if (!policy.allowedS3Buckets.includes(bucket)) {
|
|
878
|
+
return decision("deny", ["aws_s3_bucket_not_allowed"], config, {
|
|
879
|
+
maxFilesChanged: 0,
|
|
880
|
+
blockedPaths: policy.denyObjectKeyPrefixes,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
if (pathsMatch([prefix], policy.denyObjectKeyPrefixes)) {
|
|
884
|
+
return decision("deny", ["aws_s3_prefix_denied"], config, {
|
|
885
|
+
maxFilesChanged: 0,
|
|
886
|
+
blockedPaths: policy.denyObjectKeyPrefixes,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
return decision("allow", ["aws_s3_read_inside_policy"], config, {
|
|
890
|
+
maxFilesChanged: 0,
|
|
891
|
+
blockedPaths: policy.denyObjectKeyPrefixes,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
function evaluateGcpRead(action, config) {
|
|
895
|
+
const policy = config.cloud.gcp;
|
|
896
|
+
if (!policy.enabled) {
|
|
897
|
+
return decision("deny", ["gcp_read_policy_disabled"], config, {
|
|
898
|
+
maxFilesChanged: 0,
|
|
899
|
+
blockedPaths: policy.denyObjectNamePrefixes,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (action.capability.tool === "gcp.projects.get") {
|
|
903
|
+
const projectId = getString(action.capability.payload.projectId);
|
|
904
|
+
if (!projectId) {
|
|
905
|
+
return decision("deny", ["gcp_missing_project_id"], config, {
|
|
906
|
+
maxFilesChanged: 0,
|
|
907
|
+
blockedPaths: [],
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
if (!policy.allowedProjectIds.includes(projectId)) {
|
|
911
|
+
return decision("deny", ["gcp_project_not_allowed"], config, {
|
|
912
|
+
maxFilesChanged: 0,
|
|
913
|
+
blockedPaths: [],
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
return decision("allow", ["gcp_project_read_inside_policy"], config, {
|
|
917
|
+
maxFilesChanged: 0,
|
|
918
|
+
blockedPaths: [],
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
const bucket = getString(action.capability.payload.bucket);
|
|
922
|
+
const prefix = getString(action.capability.payload.prefix) ?? "";
|
|
923
|
+
if (!bucket) {
|
|
924
|
+
return decision("deny", ["gcp_storage_missing_bucket"], config, {
|
|
925
|
+
maxFilesChanged: 0,
|
|
926
|
+
blockedPaths: policy.denyObjectNamePrefixes,
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
if (!policy.allowedStorageBuckets.includes(bucket)) {
|
|
930
|
+
return decision("deny", ["gcp_storage_bucket_not_allowed"], config, {
|
|
931
|
+
maxFilesChanged: 0,
|
|
932
|
+
blockedPaths: policy.denyObjectNamePrefixes,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
if (pathsMatch([prefix], policy.denyObjectNamePrefixes)) {
|
|
936
|
+
return decision("deny", ["gcp_storage_prefix_denied"], config, {
|
|
937
|
+
maxFilesChanged: 0,
|
|
938
|
+
blockedPaths: policy.denyObjectNamePrefixes,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return decision("allow", ["gcp_storage_read_inside_policy"], config, {
|
|
942
|
+
maxFilesChanged: 0,
|
|
943
|
+
blockedPaths: policy.denyObjectNamePrefixes,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
function evaluateDocsDocument(action, config) {
|
|
947
|
+
const policy = config.docs.documents;
|
|
948
|
+
const workspace = getString(action.capability.payload.workspace) ?? "local";
|
|
949
|
+
const path = getString(action.capability.payload.path);
|
|
950
|
+
const query = getString(action.capability.payload.query);
|
|
951
|
+
const maxResults = getNumber(action.capability.payload.maxResults) ?? 5;
|
|
952
|
+
const maxBytes = getNumber(action.capability.payload.maxBytes) ?? policy.maxReadBytes;
|
|
953
|
+
if (!policy.enabled) {
|
|
954
|
+
return decision("deny", ["docs_policy_disabled"], config, {
|
|
955
|
+
maxFilesChanged: 0,
|
|
956
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
if (!policy.allowedWorkspaces.includes(workspace)) {
|
|
960
|
+
return decision("deny", ["docs_workspace_not_allowed"], config, {
|
|
961
|
+
maxFilesChanged: 0,
|
|
962
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
if (action.capability.tool === "docs.documents.search") {
|
|
966
|
+
if (!query) {
|
|
967
|
+
return decision("deny", ["docs_search_query_required"], config, {
|
|
968
|
+
maxFilesChanged: 0,
|
|
969
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
if (maxResults > policy.maxSearchResults) {
|
|
973
|
+
return decision("deny", ["docs_result_limit_exceeds_policy"], config, {
|
|
974
|
+
maxFilesChanged: 0,
|
|
975
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
return decision("allow", ["docs_search_inside_policy"], config, {
|
|
979
|
+
maxFilesChanged: 0,
|
|
980
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
if (!path) {
|
|
984
|
+
return decision("deny", ["docs_path_required"], config, {
|
|
985
|
+
maxFilesChanged: 0,
|
|
986
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
if (isUnsafeRelativePath(path) || pathsMatch([path], policy.denyPathPrefixes)) {
|
|
990
|
+
return decision("deny", ["docs_path_denied"], config, {
|
|
991
|
+
maxFilesChanged: 0,
|
|
992
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
if (!pathsMatch([path], policy.allowedPathPrefixes)) {
|
|
996
|
+
return decision("deny", ["docs_path_not_allowed"], config, {
|
|
997
|
+
maxFilesChanged: 0,
|
|
998
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
if (maxBytes > policy.maxReadBytes) {
|
|
1002
|
+
return decision("deny", ["docs_read_bytes_exceeds_policy"], config, {
|
|
1003
|
+
maxFilesChanged: 0,
|
|
1004
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
return decision("allow", ["docs_read_inside_policy"], config, {
|
|
1008
|
+
maxFilesChanged: 0,
|
|
1009
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function evaluateMcpToolCall(action, config) {
|
|
1013
|
+
const policy = config.mcp;
|
|
1014
|
+
if (!policy.enabled) {
|
|
1015
|
+
return decision("deny", ["mcp_policy_disabled"], config, {
|
|
1016
|
+
maxFilesChanged: 0,
|
|
1017
|
+
blockedPaths: [],
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
const toolName = getString(action.capability.payload.toolName);
|
|
1021
|
+
const toolDefinition = action.toolDefinition;
|
|
1022
|
+
if (!toolName) {
|
|
1023
|
+
return decision("deny", ["mcp_tool_name_required"], config, {
|
|
1024
|
+
maxFilesChanged: 0,
|
|
1025
|
+
blockedPaths: [],
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
if (!toolDefinition) {
|
|
1029
|
+
return decision("deny", ["mcp_tool_definition_required"], config, {
|
|
1030
|
+
maxFilesChanged: 0,
|
|
1031
|
+
blockedPaths: [],
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
const allowedTool = policy.allowedTools.find((entry) => entry.serverIdentity === toolDefinition.serverIdentity &&
|
|
1035
|
+
entry.toolName === toolName);
|
|
1036
|
+
if (!allowedTool) {
|
|
1037
|
+
return decision("deny", ["mcp_tool_not_allowed"], config, {
|
|
1038
|
+
maxFilesChanged: 0,
|
|
1039
|
+
blockedPaths: [],
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (allowedTool.definitionHash !== toolDefinition.definitionHash) {
|
|
1043
|
+
return decision("deny", ["mcp_definition_hash_not_allowed"], config, {
|
|
1044
|
+
maxFilesChanged: 0,
|
|
1045
|
+
blockedPaths: [],
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
return decision("allow", ["mcp_tool_inside_policy"], config, {
|
|
1049
|
+
maxFilesChanged: 0,
|
|
1050
|
+
blockedPaths: [],
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
function decision(value, reasons, config, constraints) {
|
|
1054
|
+
return {
|
|
1055
|
+
decision: value,
|
|
1056
|
+
reasons,
|
|
1057
|
+
policy: {
|
|
1058
|
+
nativeRule: "axtary_policy_v0",
|
|
1059
|
+
version: config.version,
|
|
1060
|
+
cedarCompatible: true,
|
|
1061
|
+
opaCompatible: true,
|
|
1062
|
+
},
|
|
1063
|
+
constraints: {
|
|
1064
|
+
expiresInSeconds: DEFAULT_EXPIRES_IN_SECONDS,
|
|
1065
|
+
maxFilesChanged: constraints.maxFilesChanged,
|
|
1066
|
+
blockedPaths: constraints.blockedPaths,
|
|
1067
|
+
},
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
function resourceToCedar(resource) {
|
|
1071
|
+
const [kind, id] = resource.split(":", 2);
|
|
1072
|
+
return {
|
|
1073
|
+
type: kind ? titleCase(kind) : "Resource",
|
|
1074
|
+
id: id ?? resource,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
function pathsMatch(paths, prefixes) {
|
|
1078
|
+
return paths.some((path) => prefixes.some((prefix) => path.startsWith(prefix)));
|
|
1079
|
+
}
|
|
1080
|
+
function isUnsafeRelativePath(path) {
|
|
1081
|
+
if (path.startsWith("/") || path.startsWith("\\") || path.includes("\0")) {
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
return path.split(/[\\/]+/).some((segment) => segment === ".." || segment === "");
|
|
1085
|
+
}
|
|
1086
|
+
function decisionSummary(decisionInput) {
|
|
1087
|
+
if (decisionInput.decision === "allow") {
|
|
1088
|
+
return "The normalized action is inside the active policy and can proceed through ActionPass issuance.";
|
|
1089
|
+
}
|
|
1090
|
+
if (decisionInput.decision === "step_up") {
|
|
1091
|
+
return "The normalized action is in scope only after exact payload-bound reviewer approval.";
|
|
1092
|
+
}
|
|
1093
|
+
return "The normalized action is outside the active policy and must not execute.";
|
|
1094
|
+
}
|
|
1095
|
+
function reasonDetail(reason) {
|
|
1096
|
+
if (reason === "github_pr_inside_policy") {
|
|
1097
|
+
return "Base branch, changed paths, file count, and test evidence satisfy the GitHub PR rule.";
|
|
1098
|
+
}
|
|
1099
|
+
if (reason === "github_content_write_inside_policy") {
|
|
1100
|
+
return "The write path and branch are inside the configured repository guardrails.";
|
|
1101
|
+
}
|
|
1102
|
+
if (reason === "github_branch_inside_policy") {
|
|
1103
|
+
return "The branch creation request is tied to the configured base branch.";
|
|
1104
|
+
}
|
|
1105
|
+
if (reason === "content_read_denied_path") {
|
|
1106
|
+
return "The requested file path matches a protected content-read prefix.";
|
|
1107
|
+
}
|
|
1108
|
+
if (reason === "payload_touches_denied_path") {
|
|
1109
|
+
return "The payload touches a path prefix that policy blocks outright.";
|
|
1110
|
+
}
|
|
1111
|
+
if (reason === "payload_touches_step_up_path") {
|
|
1112
|
+
return "The payload touches a protected path prefix that requires exact review.";
|
|
1113
|
+
}
|
|
1114
|
+
if (reason === "file_count_exceeds_policy_limit") {
|
|
1115
|
+
return "The PR changes more files than the active policy allows without review.";
|
|
1116
|
+
}
|
|
1117
|
+
if (reason === "tests_required_before_pr") {
|
|
1118
|
+
return "The PR payload does not include passing test evidence required by policy.";
|
|
1119
|
+
}
|
|
1120
|
+
if (reason === "payload_declares_production_impact") {
|
|
1121
|
+
return "The action declares production impact, so reviewer approval is required.";
|
|
1122
|
+
}
|
|
1123
|
+
if (reason === "slack_channel_not_allowed") {
|
|
1124
|
+
return "The Slack channel is not in the configured allowlist.";
|
|
1125
|
+
}
|
|
1126
|
+
if (reason === "slack_external_recipient_step_up") {
|
|
1127
|
+
return "The message reaches external recipients and must be reviewed exactly.";
|
|
1128
|
+
}
|
|
1129
|
+
if (reason === "linear_status_requires_step_up") {
|
|
1130
|
+
return "The Linear status transition targets a configured protected status.";
|
|
1131
|
+
}
|
|
1132
|
+
if (reason === "jira_status_requires_step_up") {
|
|
1133
|
+
return "The Jira status transition targets a configured protected status.";
|
|
1134
|
+
}
|
|
1135
|
+
if (reason.endsWith("_project_not_allowed")) {
|
|
1136
|
+
return "The issue project key is outside the configured tracker allowlist.";
|
|
1137
|
+
}
|
|
1138
|
+
if (reason === "docs_search_inside_policy") {
|
|
1139
|
+
return "The document search request stays inside the configured workspace and result limit.";
|
|
1140
|
+
}
|
|
1141
|
+
if (reason === "docs_read_inside_policy") {
|
|
1142
|
+
return "The document path and requested byte limit are inside the configured documentation scope.";
|
|
1143
|
+
}
|
|
1144
|
+
if (reason === "docs_workspace_not_allowed") {
|
|
1145
|
+
return "The document workspace is outside the configured allowlist.";
|
|
1146
|
+
}
|
|
1147
|
+
if (reason === "docs_path_denied") {
|
|
1148
|
+
return "The requested document path matches a protected documentation prefix.";
|
|
1149
|
+
}
|
|
1150
|
+
if (reason === "docs_path_not_allowed") {
|
|
1151
|
+
return "The requested document path is outside the configured documentation prefixes.";
|
|
1152
|
+
}
|
|
1153
|
+
if (reason === "docs_result_limit_exceeds_policy") {
|
|
1154
|
+
return "The search request asks for more document results than policy allows.";
|
|
1155
|
+
}
|
|
1156
|
+
if (reason === "docs_read_bytes_exceeds_policy") {
|
|
1157
|
+
return "The read request asks for more document bytes than policy allows.";
|
|
1158
|
+
}
|
|
1159
|
+
if (reason.startsWith("unsupported_tool:")) {
|
|
1160
|
+
return "The tool is not supported by the native policy evaluator and fails closed.";
|
|
1161
|
+
}
|
|
1162
|
+
if (reason.startsWith("base_branch_mismatch:")) {
|
|
1163
|
+
return "The requested base branch differs from the configured branch and needs review.";
|
|
1164
|
+
}
|
|
1165
|
+
return "The active native policy emitted this reason for the normalized action.";
|
|
1166
|
+
}
|
|
1167
|
+
function collectPatchPaths(input, prefix = "") {
|
|
1168
|
+
if (!isPlainObject(input)) {
|
|
1169
|
+
return prefix ? [prefix] : [];
|
|
1170
|
+
}
|
|
1171
|
+
return Object.entries(input).flatMap(([key, value]) => {
|
|
1172
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
1173
|
+
if (isPlainObject(value)) {
|
|
1174
|
+
return collectPatchPaths(value, path);
|
|
1175
|
+
}
|
|
1176
|
+
return [path];
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
function arraysEqual(left, right) {
|
|
1180
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1181
|
+
}
|
|
1182
|
+
function fixtureOutcomeChanged(beforeFixture, afterFixture) {
|
|
1183
|
+
return (beforeFixture.actualDecision !== afterFixture.actualDecision ||
|
|
1184
|
+
beforeFixture.passed !== afterFixture.passed ||
|
|
1185
|
+
!arraysEqual(beforeFixture.actualReasons, afterFixture.actualReasons));
|
|
1186
|
+
}
|
|
1187
|
+
function getStringArray(value) {
|
|
1188
|
+
if (!Array.isArray(value)) {
|
|
1189
|
+
return [];
|
|
1190
|
+
}
|
|
1191
|
+
return value.filter((item) => typeof item === "string");
|
|
1192
|
+
}
|
|
1193
|
+
function getString(value) {
|
|
1194
|
+
return typeof value === "string" ? value : null;
|
|
1195
|
+
}
|
|
1196
|
+
function getNumber(value) {
|
|
1197
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1198
|
+
}
|
|
1199
|
+
function deepMerge(base, patch) {
|
|
1200
|
+
if (Array.isArray(base) || Array.isArray(patch)) {
|
|
1201
|
+
return (patch ?? base);
|
|
1202
|
+
}
|
|
1203
|
+
if (!isPlainObject(base) || !isPlainObject(patch)) {
|
|
1204
|
+
return (patch ?? base);
|
|
1205
|
+
}
|
|
1206
|
+
const merged = { ...base };
|
|
1207
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
1208
|
+
merged[key] = key in merged ? deepMerge(merged[key], value) : value;
|
|
1209
|
+
}
|
|
1210
|
+
return merged;
|
|
1211
|
+
}
|
|
1212
|
+
function isPlainObject(value) {
|
|
1213
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1214
|
+
}
|
|
1215
|
+
function titleCase(value) {
|
|
1216
|
+
return `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
|
|
1217
|
+
}
|
|
1218
|
+
//# sourceMappingURL=index.js.map
|