@axtary/policy 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -2
- package/dist/acs.d.ts +103 -0
- package/dist/acs.d.ts.map +1 -0
- package/dist/acs.js +207 -0
- package/dist/acs.js.map +1 -0
- package/dist/index.d.ts +208 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1033 -162
- package/dist/index.js.map +1 -1
- package/dist/parity.d.ts +150 -0
- package/dist/parity.d.ts.map +1 -0
- package/dist/parity.js +348 -0
- package/dist/parity.js.map +1 -0
- package/package.json +10 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import { DEFAULT_EXPIRES_IN_SECONDS, NormalizedActionSchema, } from "@axtary/actionpass";
|
|
1
|
+
import { analyzePostgresSelect, DEFAULT_EXPIRES_IN_SECONDS, explainDecision, NormalizedActionSchema, } from "@axtary/actionpass";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
// The deterministic reason→prose explanation now lives in @axtary/actionpass so
|
|
4
|
+
// the SDK facade and the policy/CLI surfaces share one source of truth. Re-export
|
|
5
|
+
// it under the historical name for existing importers.
|
|
6
|
+
export { explainDecision as explainPolicyDecision } from "@axtary/actionpass";
|
|
3
7
|
export const POLICY_SCHEMA_VERSION = "axtary.policy.v0";
|
|
4
8
|
const DEFAULT_GITHUB_PULL_REQUEST_POLICY = {
|
|
5
9
|
enabled: true,
|
|
@@ -48,8 +52,98 @@ const DEFAULT_DOCS_POLICY = {
|
|
|
48
52
|
maxSearchResults: 10,
|
|
49
53
|
maxReadBytes: 20_000,
|
|
50
54
|
};
|
|
55
|
+
const DEFAULT_POSTGRES_READ_POLICY = {
|
|
56
|
+
enabled: true,
|
|
57
|
+
allowedDatabases: [],
|
|
58
|
+
allowedTables: [],
|
|
59
|
+
allowedFunctions: [],
|
|
60
|
+
maxRows: 100,
|
|
61
|
+
requirePredicate: true,
|
|
62
|
+
};
|
|
63
|
+
const DEFAULT_DRIVE_READ_POLICY = {
|
|
64
|
+
enabled: true,
|
|
65
|
+
allowedFileIds: [],
|
|
66
|
+
maxReadBytes: 100_000,
|
|
67
|
+
};
|
|
68
|
+
export const PROVENANCE_COVERAGE = {
|
|
69
|
+
"github.pull_requests.create": ["title", "body"],
|
|
70
|
+
"github.contents.write": ["content", "message"],
|
|
71
|
+
"github.pull_request_review_comments.create": ["body"],
|
|
72
|
+
"github.check_runs.create": ["output"],
|
|
73
|
+
"github.issue_comments.create": ["body"],
|
|
74
|
+
"slack.chat.postMessage": ["message", "text"],
|
|
75
|
+
"linear.comments.create": ["body"],
|
|
76
|
+
"linear.issues.update": ["title", "description", "status"],
|
|
77
|
+
"jira.comments.create": ["body"],
|
|
78
|
+
"jira.issues.update": ["title", "description", "status"],
|
|
79
|
+
"mcp.tool.call": ["arguments"],
|
|
80
|
+
};
|
|
81
|
+
const DEFAULT_PROVENANCE_POLICY = {
|
|
82
|
+
enabled: false,
|
|
83
|
+
unknownDecision: "deny",
|
|
84
|
+
untrustedDecision: "step_up",
|
|
85
|
+
};
|
|
86
|
+
// Behavioral novelty/sequence step-up (M5.6, risk-graph v0). Opt-in and
|
|
87
|
+
// escalate-only: these signals may raise an `allow` to `step_up`, and may never
|
|
88
|
+
// produce an `allow` or weaken a `deny`/`step_up` (PRD §4 non-goals fence).
|
|
89
|
+
const DEFAULT_BEHAVIOR_POLICY = {
|
|
90
|
+
enabled: false,
|
|
91
|
+
firstSeen: true,
|
|
92
|
+
abnormalSequence: true,
|
|
93
|
+
};
|
|
94
|
+
const StringMatcherSchema = z.union([
|
|
95
|
+
z.string().min(1),
|
|
96
|
+
z.array(z.string().min(1)).min(1),
|
|
97
|
+
]);
|
|
98
|
+
export const PolicyRuleSchema = z.object({
|
|
99
|
+
id: z.string().min(1),
|
|
100
|
+
description: z.string().min(1).optional(),
|
|
101
|
+
enabled: z.boolean().default(true),
|
|
102
|
+
match: z
|
|
103
|
+
.object({
|
|
104
|
+
tools: StringMatcherSchema.optional(),
|
|
105
|
+
resources: StringMatcherSchema.optional(),
|
|
106
|
+
actors: z
|
|
107
|
+
.object({
|
|
108
|
+
agentIds: StringMatcherSchema.optional(),
|
|
109
|
+
humanOwners: StringMatcherSchema.optional(),
|
|
110
|
+
runtimes: StringMatcherSchema.optional(),
|
|
111
|
+
tenants: StringMatcherSchema.optional(),
|
|
112
|
+
})
|
|
113
|
+
.optional(),
|
|
114
|
+
})
|
|
115
|
+
.default({}),
|
|
116
|
+
effect: z.enum(["allow", "deny", "step_up"]),
|
|
117
|
+
reason: z.string().min(1).optional(),
|
|
118
|
+
obligations: z
|
|
119
|
+
.object({
|
|
120
|
+
expiresInSeconds: z.number().int().positive().optional(),
|
|
121
|
+
requiredApproverRoles: z.array(z.string().min(1)).default([]),
|
|
122
|
+
blockedPathPrefixes: z.array(z.string().min(1)).default([]),
|
|
123
|
+
timeWindow: z
|
|
124
|
+
.object({
|
|
125
|
+
startHourUtc: z.number().int().min(0).max(23),
|
|
126
|
+
endHourUtc: z.number().int().min(0).max(23),
|
|
127
|
+
daysOfWeek: z.array(z.number().int().min(0).max(6)).default([]),
|
|
128
|
+
})
|
|
129
|
+
.optional(),
|
|
130
|
+
rateLimit: z
|
|
131
|
+
.object({
|
|
132
|
+
maxActions: z.number().int().positive(),
|
|
133
|
+
windowSeconds: z.number().int().positive(),
|
|
134
|
+
scope: z.enum(["actor", "tool", "resource", "tenant"]).default("actor"),
|
|
135
|
+
})
|
|
136
|
+
.optional(),
|
|
137
|
+
})
|
|
138
|
+
.default({
|
|
139
|
+
requiredApproverRoles: [],
|
|
140
|
+
blockedPathPrefixes: [],
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
51
143
|
export const PolicyConfigSchema = z.object({
|
|
52
144
|
version: z.string().min(1).default("2026-05-30"),
|
|
145
|
+
defaultDecision: z.literal("deny").default("deny"),
|
|
146
|
+
rules: z.array(PolicyRuleSchema).default([]),
|
|
53
147
|
github: z
|
|
54
148
|
.object({
|
|
55
149
|
pullRequests: z
|
|
@@ -181,6 +275,54 @@ export const PolicyConfigSchema = z.object({
|
|
|
181
275
|
.default({
|
|
182
276
|
documents: DEFAULT_DOCS_POLICY,
|
|
183
277
|
}),
|
|
278
|
+
postgres: z
|
|
279
|
+
.object({
|
|
280
|
+
reads: z
|
|
281
|
+
.object({
|
|
282
|
+
enabled: z.boolean().default(true),
|
|
283
|
+
allowedDatabases: z.array(z.string().min(1)).default([]),
|
|
284
|
+
allowedTables: z.array(z.string().min(1)).default([]),
|
|
285
|
+
allowedFunctions: z.array(z.string().min(1)).default([]),
|
|
286
|
+
maxRows: z.number().int().positive().max(10_000).default(100),
|
|
287
|
+
requirePredicate: z.boolean().default(true),
|
|
288
|
+
})
|
|
289
|
+
.default(DEFAULT_POSTGRES_READ_POLICY),
|
|
290
|
+
})
|
|
291
|
+
.default({
|
|
292
|
+
reads: DEFAULT_POSTGRES_READ_POLICY,
|
|
293
|
+
}),
|
|
294
|
+
drive: z
|
|
295
|
+
.object({
|
|
296
|
+
reads: z
|
|
297
|
+
.object({
|
|
298
|
+
enabled: z.boolean().default(true),
|
|
299
|
+
allowedFileIds: z.array(z.string().min(1)).default([]),
|
|
300
|
+
maxReadBytes: z
|
|
301
|
+
.number()
|
|
302
|
+
.int()
|
|
303
|
+
.positive()
|
|
304
|
+
.max(10_000_000)
|
|
305
|
+
.default(100_000),
|
|
306
|
+
})
|
|
307
|
+
.default(DEFAULT_DRIVE_READ_POLICY),
|
|
308
|
+
})
|
|
309
|
+
.default({
|
|
310
|
+
reads: DEFAULT_DRIVE_READ_POLICY,
|
|
311
|
+
}),
|
|
312
|
+
provenance: z
|
|
313
|
+
.object({
|
|
314
|
+
enabled: z.boolean().default(false),
|
|
315
|
+
unknownDecision: z.literal("deny").default("deny"),
|
|
316
|
+
untrustedDecision: z.enum(["deny", "step_up"]).default("step_up"),
|
|
317
|
+
})
|
|
318
|
+
.default(DEFAULT_PROVENANCE_POLICY),
|
|
319
|
+
behavior: z
|
|
320
|
+
.object({
|
|
321
|
+
enabled: z.boolean().default(false),
|
|
322
|
+
firstSeen: z.boolean().default(true),
|
|
323
|
+
abnormalSequence: z.boolean().default(true),
|
|
324
|
+
})
|
|
325
|
+
.default(DEFAULT_BEHAVIOR_POLICY),
|
|
184
326
|
});
|
|
185
327
|
export const PolicyFixtureInputSchema = z.object({
|
|
186
328
|
id: z.string().min(1).optional(),
|
|
@@ -196,20 +338,101 @@ export const DEFAULT_POLICY_FIXTURE_SOURCE = {
|
|
|
196
338
|
label: "package defaults from @axtary/policy",
|
|
197
339
|
path: null,
|
|
198
340
|
};
|
|
341
|
+
const TEMPLATE_ACTOR = {
|
|
342
|
+
agentId: "agent:codex-prod",
|
|
343
|
+
humanOwner: "user:asrar@company.com",
|
|
344
|
+
runtime: "codex-cli",
|
|
345
|
+
tenant: "org:company",
|
|
346
|
+
};
|
|
347
|
+
function templateAction(taskId, declaredGoal, tool, resource, payload) {
|
|
348
|
+
return NormalizedActionSchema.parse({
|
|
349
|
+
schemaVersion: "axtary.action.v0",
|
|
350
|
+
actor: TEMPLATE_ACTOR,
|
|
351
|
+
intent: { taskId, declaredGoal },
|
|
352
|
+
capability: { tool, resource, payload },
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
const DISABLE_NON_REPO_POLICY = {
|
|
356
|
+
slack: { messages: { enabled: false } },
|
|
357
|
+
issueTrackers: {
|
|
358
|
+
linear: { enabled: false },
|
|
359
|
+
jira: { enabled: false },
|
|
360
|
+
},
|
|
361
|
+
cloud: {
|
|
362
|
+
aws: { enabled: false },
|
|
363
|
+
gcp: { enabled: false },
|
|
364
|
+
},
|
|
365
|
+
docs: { documents: { enabled: false } },
|
|
366
|
+
postgres: { reads: { enabled: false } },
|
|
367
|
+
drive: { reads: { enabled: false } },
|
|
368
|
+
mcp: { enabled: false },
|
|
369
|
+
};
|
|
370
|
+
const DISABLE_NON_CLOUD_POLICY = {
|
|
371
|
+
github: {
|
|
372
|
+
pullRequests: { enabled: false },
|
|
373
|
+
contents: { enabled: false },
|
|
374
|
+
},
|
|
375
|
+
slack: { messages: { enabled: false } },
|
|
376
|
+
issueTrackers: {
|
|
377
|
+
linear: { enabled: false },
|
|
378
|
+
jira: { enabled: false },
|
|
379
|
+
},
|
|
380
|
+
docs: { documents: { enabled: false } },
|
|
381
|
+
postgres: { reads: { enabled: false } },
|
|
382
|
+
drive: { reads: { enabled: false } },
|
|
383
|
+
mcp: { enabled: false },
|
|
384
|
+
};
|
|
385
|
+
const DISABLE_NON_TICKET_POLICY = {
|
|
386
|
+
github: {
|
|
387
|
+
pullRequests: { enabled: false },
|
|
388
|
+
contents: { enabled: false },
|
|
389
|
+
},
|
|
390
|
+
slack: { messages: { enabled: false } },
|
|
391
|
+
cloud: {
|
|
392
|
+
aws: { enabled: false },
|
|
393
|
+
gcp: { enabled: false },
|
|
394
|
+
},
|
|
395
|
+
docs: { documents: { enabled: false } },
|
|
396
|
+
postgres: { reads: { enabled: false } },
|
|
397
|
+
drive: { reads: { enabled: false } },
|
|
398
|
+
mcp: { enabled: false },
|
|
399
|
+
};
|
|
400
|
+
const DISABLE_NON_DOCS_POLICY = {
|
|
401
|
+
github: {
|
|
402
|
+
pullRequests: { enabled: false },
|
|
403
|
+
contents: { enabled: false },
|
|
404
|
+
},
|
|
405
|
+
slack: { messages: { enabled: false } },
|
|
406
|
+
issueTrackers: {
|
|
407
|
+
linear: { enabled: false },
|
|
408
|
+
jira: { enabled: false },
|
|
409
|
+
},
|
|
410
|
+
cloud: {
|
|
411
|
+
aws: { enabled: false },
|
|
412
|
+
gcp: { enabled: false },
|
|
413
|
+
},
|
|
414
|
+
postgres: { reads: { enabled: false } },
|
|
415
|
+
drive: { reads: { enabled: false } },
|
|
416
|
+
mcp: { enabled: false },
|
|
417
|
+
};
|
|
199
418
|
export const POLICY_TEMPLATES = [
|
|
200
419
|
{
|
|
201
|
-
id: "
|
|
202
|
-
name: "
|
|
420
|
+
id: "repo-only-coding",
|
|
421
|
+
name: "Repo-only coding",
|
|
203
422
|
category: "coding_agent",
|
|
204
|
-
summary: "Allow routine
|
|
423
|
+
summary: "Allow routine repository work while denying secret reads and stepping up auth, billing, and production paths.",
|
|
205
424
|
tools: [
|
|
206
425
|
"github.pull_requests.create",
|
|
207
426
|
"github.branches.create",
|
|
208
427
|
"github.contents.read",
|
|
209
428
|
"github.contents.write",
|
|
429
|
+
"github.pull_request_review_comments.create",
|
|
430
|
+
"github.check_runs.create",
|
|
431
|
+
"github.issue_comments.create",
|
|
210
432
|
],
|
|
211
433
|
decisionModel: "step_up",
|
|
212
434
|
config: {
|
|
435
|
+
...DISABLE_NON_REPO_POLICY,
|
|
213
436
|
github: {
|
|
214
437
|
pullRequests: {
|
|
215
438
|
requiredBaseBranch: "main",
|
|
@@ -228,12 +451,48 @@ export const POLICY_TEMPLATES = [
|
|
|
228
451
|
"Matches the current coding-agent wedge.",
|
|
229
452
|
"Protected path changes require exact approval evidence before execution.",
|
|
230
453
|
],
|
|
454
|
+
fixtures: [
|
|
455
|
+
PolicyFixtureInputSchema.parse({
|
|
456
|
+
id: "repo-only-coding-allow-pr",
|
|
457
|
+
name: "allow a routine PR inside repo coding scope",
|
|
458
|
+
expectedDecision: "allow",
|
|
459
|
+
expectedReasons: ["github_pr_inside_policy"],
|
|
460
|
+
action: templateAction("AXT-TEMPLATE-1", "Open a routine source PR", "github.pull_requests.create", "repo:company/web-app", {
|
|
461
|
+
baseBranch: "main",
|
|
462
|
+
filesChanged: ["src/app/page.tsx"],
|
|
463
|
+
testsPassed: true,
|
|
464
|
+
touchesProduction: false,
|
|
465
|
+
}),
|
|
466
|
+
}),
|
|
467
|
+
PolicyFixtureInputSchema.parse({
|
|
468
|
+
id: "repo-only-coding-deny-secret-read",
|
|
469
|
+
name: "deny a repository secret read",
|
|
470
|
+
expectedDecision: "deny",
|
|
471
|
+
expectedReasons: ["content_read_denied_path"],
|
|
472
|
+
action: templateAction("AXT-TEMPLATE-2", "Read files for a coding task", "github.contents.read", "repo:company/web-app", { path: ".env.production" }),
|
|
473
|
+
}),
|
|
474
|
+
PolicyFixtureInputSchema.parse({
|
|
475
|
+
id: "repo-only-coding-step-up-prod",
|
|
476
|
+
name: "step up protected production paths",
|
|
477
|
+
expectedDecision: "step_up",
|
|
478
|
+
expectedReasons: [
|
|
479
|
+
"payload_touches_step_up_path",
|
|
480
|
+
"payload_declares_production_impact",
|
|
481
|
+
],
|
|
482
|
+
action: templateAction("AXT-TEMPLATE-3", "Touch production infrastructure", "github.pull_requests.create", "repo:company/web-app", {
|
|
483
|
+
baseBranch: "main",
|
|
484
|
+
filesChanged: ["infra/prod/main.tf"],
|
|
485
|
+
testsPassed: true,
|
|
486
|
+
touchesProduction: true,
|
|
487
|
+
}),
|
|
488
|
+
}),
|
|
489
|
+
],
|
|
231
490
|
},
|
|
232
491
|
{
|
|
233
|
-
id: "staging-
|
|
234
|
-
name: "Staging
|
|
492
|
+
id: "staging-reads",
|
|
493
|
+
name: "Staging reads",
|
|
235
494
|
category: "cloud_read",
|
|
236
|
-
summary: "Permit bounded AWS/GCP inventory and object listing only
|
|
495
|
+
summary: "Permit bounded AWS/GCP staging inventory and object listing only for pinned accounts, projects, regions, and buckets.",
|
|
237
496
|
tools: [
|
|
238
497
|
"aws.identity.get",
|
|
239
498
|
"aws.s3.objects.list",
|
|
@@ -242,97 +501,202 @@ export const POLICY_TEMPLATES = [
|
|
|
242
501
|
],
|
|
243
502
|
decisionModel: "deny_until_scoped",
|
|
244
503
|
config: {
|
|
504
|
+
...DISABLE_NON_CLOUD_POLICY,
|
|
245
505
|
cloud: {
|
|
246
506
|
aws: {
|
|
247
507
|
enabled: true,
|
|
248
|
-
allowedAccountIds: [],
|
|
508
|
+
allowedAccountIds: ["111122223333"],
|
|
249
509
|
allowedRegions: ["us-east-1"],
|
|
250
|
-
allowedS3Buckets: [],
|
|
510
|
+
allowedS3Buckets: ["axtary-staging-logs"],
|
|
251
511
|
denyObjectKeyPrefixes: ["secrets/", ".env"],
|
|
252
512
|
},
|
|
253
513
|
gcp: {
|
|
254
514
|
enabled: true,
|
|
255
|
-
allowedProjectIds: [],
|
|
256
|
-
allowedStorageBuckets: [],
|
|
515
|
+
allowedProjectIds: ["axtary-staging"],
|
|
516
|
+
allowedStorageBuckets: ["axtary-staging-logs"],
|
|
257
517
|
denyObjectNamePrefixes: ["secrets/", ".env"],
|
|
258
518
|
},
|
|
259
519
|
},
|
|
260
520
|
},
|
|
261
521
|
evidence: ["accountId", "region", "projectId", "bucket", "prefix"],
|
|
262
522
|
notes: [
|
|
263
|
-
"
|
|
523
|
+
"The scaffold uses sample staging scopes so policy tests pass locally; replace them with your own staging account, project, and bucket identifiers before real mode.",
|
|
264
524
|
"Smoke checks stay read/auth-only.",
|
|
265
525
|
],
|
|
526
|
+
fixtures: [
|
|
527
|
+
PolicyFixtureInputSchema.parse({
|
|
528
|
+
id: "staging-reads-allow-aws-s3",
|
|
529
|
+
name: "allow a scoped staging S3 list",
|
|
530
|
+
expectedDecision: "allow",
|
|
531
|
+
expectedReasons: ["aws_s3_read_inside_policy"],
|
|
532
|
+
action: templateAction("AXT-TEMPLATE-4", "Inspect staging logs", "aws.s3.objects.list", "aws:s3:::axtary-staging-logs", {
|
|
533
|
+
accountId: "111122223333",
|
|
534
|
+
region: "us-east-1",
|
|
535
|
+
bucket: "axtary-staging-logs",
|
|
536
|
+
prefix: "logs/",
|
|
537
|
+
}),
|
|
538
|
+
}),
|
|
539
|
+
PolicyFixtureInputSchema.parse({
|
|
540
|
+
id: "staging-reads-deny-secret-prefix",
|
|
541
|
+
name: "deny a secret prefix in staging storage",
|
|
542
|
+
expectedDecision: "deny",
|
|
543
|
+
expectedReasons: ["aws_s3_prefix_denied"],
|
|
544
|
+
action: templateAction("AXT-TEMPLATE-5", "Inspect staging secrets", "aws.s3.objects.list", "aws:s3:::axtary-staging-logs", {
|
|
545
|
+
accountId: "111122223333",
|
|
546
|
+
region: "us-east-1",
|
|
547
|
+
bucket: "axtary-staging-logs",
|
|
548
|
+
prefix: "secrets/",
|
|
549
|
+
}),
|
|
550
|
+
}),
|
|
551
|
+
],
|
|
266
552
|
},
|
|
267
553
|
{
|
|
268
|
-
id: "
|
|
269
|
-
name: "
|
|
270
|
-
category: "
|
|
271
|
-
summary: "
|
|
272
|
-
tools: [
|
|
273
|
-
|
|
554
|
+
id: "incident-investigation",
|
|
555
|
+
name: "Incident investigation",
|
|
556
|
+
category: "docs_read",
|
|
557
|
+
summary: "Allow read-only investigation across repository content, tickets, and local docs while denying secret paths.",
|
|
558
|
+
tools: [
|
|
559
|
+
"github.contents.read",
|
|
560
|
+
"linear.issues.read",
|
|
561
|
+
"jira.issues.read",
|
|
562
|
+
"docs.documents.search",
|
|
563
|
+
"docs.documents.read",
|
|
564
|
+
],
|
|
565
|
+
decisionModel: "deny_until_scoped",
|
|
274
566
|
config: {
|
|
567
|
+
slack: { messages: { enabled: false } },
|
|
568
|
+
cloud: {
|
|
569
|
+
aws: { enabled: false },
|
|
570
|
+
gcp: { enabled: false },
|
|
571
|
+
},
|
|
572
|
+
postgres: { reads: { enabled: false } },
|
|
573
|
+
drive: { reads: { enabled: false } },
|
|
574
|
+
mcp: { enabled: false },
|
|
275
575
|
github: {
|
|
276
576
|
pullRequests: {
|
|
277
|
-
|
|
278
|
-
|
|
577
|
+
enabled: false,
|
|
578
|
+
},
|
|
579
|
+
contents: {
|
|
580
|
+
enabled: true,
|
|
581
|
+
denyReadPathPrefixes: [".env", "secrets/", "infra/prod/"],
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
issueTrackers: {
|
|
585
|
+
linear: {
|
|
586
|
+
enabled: true,
|
|
587
|
+
allowedProjectKeys: ["AXT", "INC"],
|
|
588
|
+
stepUpStatuses: ["Done", "Closed"],
|
|
589
|
+
},
|
|
590
|
+
jira: {
|
|
591
|
+
enabled: true,
|
|
592
|
+
allowedProjectKeys: ["AXT", "INC"],
|
|
593
|
+
stepUpStatuses: ["Done", "Closed"],
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
docs: {
|
|
597
|
+
documents: {
|
|
598
|
+
enabled: true,
|
|
599
|
+
allowedWorkspaces: ["local"],
|
|
600
|
+
allowedPathPrefixes: ["README.md", "docs/", "product/"],
|
|
601
|
+
denyPathPrefixes: [".env", "secrets/", "private/"],
|
|
602
|
+
maxSearchResults: 10,
|
|
603
|
+
maxReadBytes: 20_000,
|
|
279
604
|
},
|
|
280
605
|
},
|
|
281
606
|
},
|
|
282
|
-
evidence: ["
|
|
607
|
+
evidence: ["issueKey", "projectKey", "path", "query", "payloadHash"],
|
|
283
608
|
notes: [
|
|
284
|
-
"
|
|
285
|
-
"
|
|
609
|
+
"Investigation scope is read-oriented; ticket completion and production changes use separate templates.",
|
|
610
|
+
"Repository and docs secret paths remain deny-only.",
|
|
286
611
|
],
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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.",
|
|
612
|
+
fixtures: [
|
|
613
|
+
PolicyFixtureInputSchema.parse({
|
|
614
|
+
id: "incident-investigation-allow-ticket-read",
|
|
615
|
+
name: "allow an incident ticket read",
|
|
616
|
+
expectedDecision: "allow",
|
|
617
|
+
expectedReasons: ["jira_issue_inside_policy"],
|
|
618
|
+
action: templateAction("AXT-TEMPLATE-6", "Read the incident ticket", "jira.issues.read", "jira:site/company", { issueKey: "INC-104", projectKey: "INC" }),
|
|
619
|
+
}),
|
|
620
|
+
PolicyFixtureInputSchema.parse({
|
|
621
|
+
id: "incident-investigation-deny-secret-read",
|
|
622
|
+
name: "deny secret repository content during investigation",
|
|
623
|
+
expectedDecision: "deny",
|
|
624
|
+
expectedReasons: ["content_read_denied_path"],
|
|
625
|
+
action: templateAction("AXT-TEMPLATE-7", "Read incident environment details", "github.contents.read", "repo:company/web-app", { path: "secrets/prod.env" }),
|
|
626
|
+
}),
|
|
305
627
|
],
|
|
306
628
|
},
|
|
307
629
|
{
|
|
308
|
-
id: "
|
|
309
|
-
name: "
|
|
630
|
+
id: "ticket-updates",
|
|
631
|
+
name: "Ticket updates",
|
|
310
632
|
category: "external_communication",
|
|
311
|
-
summary: "Allow
|
|
312
|
-
tools: [
|
|
633
|
+
summary: "Allow scoped ticket reads and comments while denying out-of-project updates and stepping up terminal status changes.",
|
|
634
|
+
tools: [
|
|
635
|
+
"linear.issues.read",
|
|
636
|
+
"linear.comments.create",
|
|
637
|
+
"linear.issues.update",
|
|
638
|
+
"jira.issues.read",
|
|
639
|
+
"jira.comments.create",
|
|
640
|
+
"jira.issues.update",
|
|
641
|
+
],
|
|
313
642
|
decisionModel: "step_up",
|
|
314
643
|
config: {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
644
|
+
...DISABLE_NON_TICKET_POLICY,
|
|
645
|
+
issueTrackers: {
|
|
646
|
+
linear: {
|
|
647
|
+
enabled: true,
|
|
648
|
+
allowedProjectKeys: ["AXT"],
|
|
649
|
+
stepUpStatuses: ["Done", "Closed"],
|
|
650
|
+
},
|
|
651
|
+
jira: {
|
|
652
|
+
enabled: true,
|
|
653
|
+
allowedProjectKeys: ["AXT"],
|
|
654
|
+
stepUpStatuses: ["Done", "Closed"],
|
|
319
655
|
},
|
|
320
656
|
},
|
|
321
657
|
},
|
|
322
|
-
evidence: ["
|
|
658
|
+
evidence: ["issueKey", "projectKey", "status", "body", "payloadHash"],
|
|
323
659
|
notes: [
|
|
324
|
-
"
|
|
325
|
-
"
|
|
660
|
+
"Comments and non-terminal edits inside the configured project are allowed.",
|
|
661
|
+
"Terminal statuses require reviewer approval; other projects are denied.",
|
|
662
|
+
],
|
|
663
|
+
fixtures: [
|
|
664
|
+
PolicyFixtureInputSchema.parse({
|
|
665
|
+
id: "ticket-updates-allow-comment",
|
|
666
|
+
name: "allow a scoped Linear comment",
|
|
667
|
+
expectedDecision: "allow",
|
|
668
|
+
expectedReasons: ["linear_issue_inside_policy"],
|
|
669
|
+
action: templateAction("AXT-TEMPLATE-8", "Comment on the source ticket", "linear.comments.create", "linear:workspace/company", {
|
|
670
|
+
issueKey: "AXT-418",
|
|
671
|
+
projectKey: "AXT",
|
|
672
|
+
body: "Investigation complete; PR is linked.",
|
|
673
|
+
}),
|
|
674
|
+
}),
|
|
675
|
+
PolicyFixtureInputSchema.parse({
|
|
676
|
+
id: "ticket-updates-deny-other-project",
|
|
677
|
+
name: "deny a ticket update outside the configured project",
|
|
678
|
+
expectedDecision: "deny",
|
|
679
|
+
expectedReasons: ["jira_project_not_allowed"],
|
|
680
|
+
action: templateAction("AXT-TEMPLATE-9", "Update an unrelated ticket", "jira.issues.update", "jira:site/company", { issueKey: "SEC-9", projectKey: "SEC", status: "In Progress" }),
|
|
681
|
+
}),
|
|
682
|
+
PolicyFixtureInputSchema.parse({
|
|
683
|
+
id: "ticket-updates-step-up-done",
|
|
684
|
+
name: "step up terminal ticket status changes",
|
|
685
|
+
expectedDecision: "step_up",
|
|
686
|
+
expectedReasons: ["linear_status_requires_step_up"],
|
|
687
|
+
action: templateAction("AXT-TEMPLATE-10", "Mark the source ticket done", "linear.issues.update", "linear:workspace/company", { issueKey: "AXT-418", projectKey: "AXT", status: "Done" }),
|
|
688
|
+
}),
|
|
326
689
|
],
|
|
327
690
|
},
|
|
328
691
|
{
|
|
329
|
-
id: "
|
|
330
|
-
name: "
|
|
692
|
+
id: "doc-search",
|
|
693
|
+
name: "Doc search",
|
|
331
694
|
category: "docs_read",
|
|
332
|
-
summary: "Permit local
|
|
695
|
+
summary: "Permit local documentation search and bounded reads only inside configured workspaces and paths.",
|
|
333
696
|
tools: ["docs.documents.search", "docs.documents.read"],
|
|
334
697
|
decisionModel: "deny_until_scoped",
|
|
335
698
|
config: {
|
|
699
|
+
...DISABLE_NON_DOCS_POLICY,
|
|
336
700
|
docs: {
|
|
337
701
|
documents: {
|
|
338
702
|
enabled: true,
|
|
@@ -349,8 +713,101 @@ export const POLICY_TEMPLATES = [
|
|
|
349
713
|
"Local document roots are configured adapter-side, while policy controls normalized action scope.",
|
|
350
714
|
"Returned content should be bounded and redacted by the connector before the agent sees it.",
|
|
351
715
|
],
|
|
716
|
+
fixtures: [
|
|
717
|
+
PolicyFixtureInputSchema.parse({
|
|
718
|
+
id: "doc-search-allow-query",
|
|
719
|
+
name: "allow a bounded docs search",
|
|
720
|
+
expectedDecision: "allow",
|
|
721
|
+
expectedReasons: ["docs_search_inside_policy"],
|
|
722
|
+
action: templateAction("AXT-TEMPLATE-11", "Search product docs", "docs.documents.search", "docs:local", { workspace: "local", query: "ActionPass ledger", maxResults: 5 }),
|
|
723
|
+
}),
|
|
724
|
+
PolicyFixtureInputSchema.parse({
|
|
725
|
+
id: "doc-search-deny-private-read",
|
|
726
|
+
name: "deny a private docs path read",
|
|
727
|
+
expectedDecision: "deny",
|
|
728
|
+
expectedReasons: ["docs_path_denied"],
|
|
729
|
+
action: templateAction("AXT-TEMPLATE-12", "Read private planning notes", "docs.documents.read", "docs:local", { workspace: "local", path: "private/roadmap.md", maxBytes: 1000 }),
|
|
730
|
+
}),
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
id: "guarded-prod",
|
|
735
|
+
name: "Guarded production changes",
|
|
736
|
+
category: "production_change",
|
|
737
|
+
summary: "Allow routine repository changes, deny secret paths, and require approval for production-impact changes.",
|
|
738
|
+
tools: ["github.pull_requests.create", "github.contents.write", "github.contents.read"],
|
|
739
|
+
decisionModel: "step_up",
|
|
740
|
+
config: {
|
|
741
|
+
...DISABLE_NON_REPO_POLICY,
|
|
742
|
+
github: {
|
|
743
|
+
pullRequests: {
|
|
744
|
+
requiredBaseBranch: "main",
|
|
745
|
+
maxFilesChanged: 8,
|
|
746
|
+
denyPathPrefixes: [".env", "secrets/"],
|
|
747
|
+
stepUpPathPrefixes: ["infra/prod/", "deploy/", "auth/", "billing/"],
|
|
748
|
+
requiresTests: true,
|
|
749
|
+
},
|
|
750
|
+
contents: {
|
|
751
|
+
denyReadPathPrefixes: [".env", "secrets/", "infra/prod/"],
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
evidence: ["touchesProduction", "filesChanged", "diff", "payloadHash"],
|
|
756
|
+
notes: [
|
|
757
|
+
"The native evaluator steps up PR payloads with touchesProduction=true.",
|
|
758
|
+
"Approval binds the exact production-impact payload before execution.",
|
|
759
|
+
],
|
|
760
|
+
fixtures: [
|
|
761
|
+
PolicyFixtureInputSchema.parse({
|
|
762
|
+
id: "guarded-prod-allow-app-pr",
|
|
763
|
+
name: "allow a tested non-production PR",
|
|
764
|
+
expectedDecision: "allow",
|
|
765
|
+
expectedReasons: ["github_pr_inside_policy"],
|
|
766
|
+
action: templateAction("AXT-TEMPLATE-13", "Ship an application fix", "github.pull_requests.create", "repo:company/web-app", {
|
|
767
|
+
baseBranch: "main",
|
|
768
|
+
filesChanged: ["src/lib/cache.ts"],
|
|
769
|
+
testsPassed: true,
|
|
770
|
+
touchesProduction: false,
|
|
771
|
+
}),
|
|
772
|
+
}),
|
|
773
|
+
PolicyFixtureInputSchema.parse({
|
|
774
|
+
id: "guarded-prod-deny-secret-write",
|
|
775
|
+
name: "deny a secret file write",
|
|
776
|
+
expectedDecision: "deny",
|
|
777
|
+
expectedReasons: ["payload_touches_denied_path"],
|
|
778
|
+
action: templateAction("AXT-TEMPLATE-14", "Write a production secret", "github.contents.write", "repo:company/web-app", {
|
|
779
|
+
path: ".env.production",
|
|
780
|
+
branch: "main",
|
|
781
|
+
content: "SECRET=value",
|
|
782
|
+
message: "update production env",
|
|
783
|
+
}),
|
|
784
|
+
}),
|
|
785
|
+
PolicyFixtureInputSchema.parse({
|
|
786
|
+
id: "guarded-prod-step-up-prod-pr",
|
|
787
|
+
name: "step up production-impact PRs",
|
|
788
|
+
expectedDecision: "step_up",
|
|
789
|
+
expectedReasons: [
|
|
790
|
+
"payload_touches_step_up_path",
|
|
791
|
+
"payload_declares_production_impact",
|
|
792
|
+
],
|
|
793
|
+
action: templateAction("AXT-TEMPLATE-15", "Change production deployment config", "github.pull_requests.create", "repo:company/web-app", {
|
|
794
|
+
baseBranch: "main",
|
|
795
|
+
filesChanged: ["deploy/prod.yml"],
|
|
796
|
+
testsPassed: true,
|
|
797
|
+
touchesProduction: true,
|
|
798
|
+
}),
|
|
799
|
+
}),
|
|
800
|
+
],
|
|
352
801
|
},
|
|
353
802
|
];
|
|
803
|
+
const POLICY_TEMPLATE_ALIASES = {
|
|
804
|
+
"coding-agent-repo-guardrails": "repo-only-coding",
|
|
805
|
+
"staging-cloud-read-only": "staging-reads",
|
|
806
|
+
"production-impact-step-up": "guarded-prod",
|
|
807
|
+
"iam-mutation-deny": "guarded-prod",
|
|
808
|
+
"external-slack-step-up": "ticket-updates",
|
|
809
|
+
"local-docs-read-scoped": "doc-search",
|
|
810
|
+
};
|
|
354
811
|
export const DEFAULT_POLICY_REVIEW_FIXTURES = [
|
|
355
812
|
PolicyFixtureInputSchema.parse({
|
|
356
813
|
id: "allow-github-pr",
|
|
@@ -440,7 +897,8 @@ export function getPolicyTemplates() {
|
|
|
440
897
|
return POLICY_TEMPLATES;
|
|
441
898
|
}
|
|
442
899
|
export function getPolicyTemplate(id) {
|
|
443
|
-
|
|
900
|
+
const resolvedId = POLICY_TEMPLATE_ALIASES[id] ?? id;
|
|
901
|
+
return POLICY_TEMPLATES.find((template) => template.id === resolvedId) ?? null;
|
|
444
902
|
}
|
|
445
903
|
export function mergePolicyConfig(baseInput = {}, patchInput = {}) {
|
|
446
904
|
const base = PolicyConfigSchema.parse(baseInput);
|
|
@@ -454,27 +912,6 @@ export function applyPolicyTemplate(baseInput, templateId) {
|
|
|
454
912
|
}
|
|
455
913
|
return mergePolicyConfig(baseInput, template.config);
|
|
456
914
|
}
|
|
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
915
|
export function runPolicyFixtures(configInput = {}, fixtureInputs = DEFAULT_POLICY_REVIEW_FIXTURES) {
|
|
479
916
|
const config = PolicyConfigSchema.parse(configInput);
|
|
480
917
|
return fixtureInputs.map((fixtureInput, index) => {
|
|
@@ -495,7 +932,7 @@ export function runPolicyFixtures(configInput = {}, fixtureInputs = DEFAULT_POLI
|
|
|
495
932
|
sourcePath: fixture.sourcePath ?? null,
|
|
496
933
|
passed: decisionResult.decision === fixture.expectedDecision && reasonsPassed,
|
|
497
934
|
decision: decisionResult,
|
|
498
|
-
explanation:
|
|
935
|
+
explanation: explainDecision(decisionResult),
|
|
499
936
|
cedar: toCedarRequest(action),
|
|
500
937
|
opa: toOpaInput(action),
|
|
501
938
|
};
|
|
@@ -563,9 +1000,38 @@ export function previewPolicyTemplates(configInput = {}, templates = getPolicyTe
|
|
|
563
1000
|
};
|
|
564
1001
|
});
|
|
565
1002
|
}
|
|
566
|
-
export function evaluatePolicy(actionInput, configInput = {}) {
|
|
1003
|
+
export function evaluatePolicy(actionInput, configInput = {}, context = {}) {
|
|
567
1004
|
const action = NormalizedActionSchema.parse(actionInput);
|
|
568
1005
|
const config = PolicyConfigSchema.parse(configInput);
|
|
1006
|
+
const baseDecision = config.rules.length > 0
|
|
1007
|
+
? evaluateRulePolicy(action, config, context).decision
|
|
1008
|
+
: evaluateBuiltinPolicy(action, config);
|
|
1009
|
+
return applyBehaviorPolicy(config, context, applyProvenancePolicy(action, config, baseDecision));
|
|
1010
|
+
}
|
|
1011
|
+
export function simulatePolicy(actionInput, configInput = {}, context = {}) {
|
|
1012
|
+
const action = NormalizedActionSchema.parse(actionInput);
|
|
1013
|
+
const config = PolicyConfigSchema.parse(configInput);
|
|
1014
|
+
if (config.rules.length === 0) {
|
|
1015
|
+
return {
|
|
1016
|
+
schemaVersion: POLICY_SCHEMA_VERSION,
|
|
1017
|
+
action,
|
|
1018
|
+
mode: "builtin_preset",
|
|
1019
|
+
defaultDecision: "deny",
|
|
1020
|
+
decision: applyProvenancePolicy(action, config, evaluateBuiltinPolicy(action, config)),
|
|
1021
|
+
rules: [],
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const evaluated = evaluateRulePolicy(action, config, context);
|
|
1025
|
+
return {
|
|
1026
|
+
schemaVersion: POLICY_SCHEMA_VERSION,
|
|
1027
|
+
action,
|
|
1028
|
+
mode: "rules",
|
|
1029
|
+
defaultDecision: "deny",
|
|
1030
|
+
decision: applyBehaviorPolicy(config, context, applyProvenancePolicy(action, config, evaluated.decision)),
|
|
1031
|
+
rules: evaluated.traces,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function evaluateBuiltinPolicy(action, config) {
|
|
569
1035
|
switch (action.capability.tool) {
|
|
570
1036
|
case "github.pull_requests.create":
|
|
571
1037
|
return evaluateGitHubPullRequest(action, config);
|
|
@@ -575,6 +1041,12 @@ export function evaluatePolicy(actionInput, configInput = {}) {
|
|
|
575
1041
|
return evaluateGitHubContentWrite(action, config);
|
|
576
1042
|
case "github.branches.create":
|
|
577
1043
|
return evaluateGitHubBranchCreate(action, config);
|
|
1044
|
+
case "github.pull_request_review_comments.create":
|
|
1045
|
+
return evaluateGitHubReviewComment(action, config);
|
|
1046
|
+
case "github.check_runs.create":
|
|
1047
|
+
return evaluateGitHubCheckRun(action, config);
|
|
1048
|
+
case "github.issue_comments.create":
|
|
1049
|
+
return evaluateGitHubIssueComment(action, config);
|
|
578
1050
|
case "slack.chat.postMessage":
|
|
579
1051
|
return evaluateSlackMessage(action, config);
|
|
580
1052
|
case "linear.issues.read":
|
|
@@ -596,6 +1068,10 @@ export function evaluatePolicy(actionInput, configInput = {}) {
|
|
|
596
1068
|
return evaluateDocsDocument(action, config);
|
|
597
1069
|
case "mcp.tool.call":
|
|
598
1070
|
return evaluateMcpToolCall(action, config);
|
|
1071
|
+
case "postgres.query.select":
|
|
1072
|
+
return evaluatePostgresSelect(action, config);
|
|
1073
|
+
case "drive.files.read":
|
|
1074
|
+
return evaluateDriveFileRead(action, config);
|
|
599
1075
|
default:
|
|
600
1076
|
return decision("deny", [`unsupported_tool:${action.capability.tool}`], config, {
|
|
601
1077
|
maxFilesChanged: 0,
|
|
@@ -603,6 +1079,280 @@ export function evaluatePolicy(actionInput, configInput = {}) {
|
|
|
603
1079
|
});
|
|
604
1080
|
}
|
|
605
1081
|
}
|
|
1082
|
+
function evaluateDriveFileRead(action, config) {
|
|
1083
|
+
const policy = config.drive.reads;
|
|
1084
|
+
const fileId = getString(action.capability.payload.fileId);
|
|
1085
|
+
const maxBytes = getPositiveInteger(action.capability.payload.maxBytes);
|
|
1086
|
+
if (!policy.enabled) {
|
|
1087
|
+
return decision("deny", ["drive_reads_disabled"], config, {
|
|
1088
|
+
maxFilesChanged: 0,
|
|
1089
|
+
blockedPaths: [],
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
if (!fileId || !maxBytes) {
|
|
1093
|
+
return decision("deny", ["drive_read_payload_invalid"], config, {
|
|
1094
|
+
maxFilesChanged: 0,
|
|
1095
|
+
blockedPaths: [],
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (action.capability.resource !== `drive:file:${fileId}`) {
|
|
1099
|
+
return decision("deny", ["drive_file_resource_mismatch"], config, {
|
|
1100
|
+
maxFilesChanged: 0,
|
|
1101
|
+
blockedPaths: [],
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
if (!policy.allowedFileIds.includes(fileId)) {
|
|
1105
|
+
return decision("deny", ["drive_file_not_allowed"], config, {
|
|
1106
|
+
maxFilesChanged: 0,
|
|
1107
|
+
blockedPaths: [],
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
if (maxBytes > policy.maxReadBytes) {
|
|
1111
|
+
return decision("deny", ["drive_read_byte_limit_exceeded"], config, {
|
|
1112
|
+
maxFilesChanged: 0,
|
|
1113
|
+
blockedPaths: [],
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
return decision("allow", ["drive_file_inside_policy"], config, {
|
|
1117
|
+
maxFilesChanged: 0,
|
|
1118
|
+
blockedPaths: [],
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
function evaluatePostgresSelect(action, config) {
|
|
1122
|
+
const policy = config.postgres.reads;
|
|
1123
|
+
const payload = action.capability.payload;
|
|
1124
|
+
const statement = getString(payload.statement);
|
|
1125
|
+
const parameters = Array.isArray(payload.parameters) ? payload.parameters : null;
|
|
1126
|
+
const maxRows = getPositiveInteger(payload.maxRows);
|
|
1127
|
+
if (!policy.enabled) {
|
|
1128
|
+
return decision("deny", ["postgres_reads_disabled"], config, {
|
|
1129
|
+
maxFilesChanged: 0,
|
|
1130
|
+
blockedPaths: [],
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
if (!policy.allowedDatabases.includes(action.capability.resource)) {
|
|
1134
|
+
return decision("deny", ["postgres_database_not_allowed"], config, {
|
|
1135
|
+
maxFilesChanged: 0,
|
|
1136
|
+
blockedPaths: [],
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
if (!statement || !parameters || !maxRows) {
|
|
1140
|
+
return decision("deny", ["postgres_query_payload_invalid"], config, {
|
|
1141
|
+
maxFilesChanged: 0,
|
|
1142
|
+
blockedPaths: [],
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
const analyzed = analyzePostgresSelect(statement);
|
|
1146
|
+
if (!analyzed.ok) {
|
|
1147
|
+
return decision("deny", [analyzed.reason], config, {
|
|
1148
|
+
maxFilesChanged: 0,
|
|
1149
|
+
blockedPaths: [],
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
const { analysis } = analyzed;
|
|
1153
|
+
if (analysis.parameterCount !== parameters.length) {
|
|
1154
|
+
return decision("deny", ["postgres_parameter_count_mismatch"], config, {
|
|
1155
|
+
maxFilesChanged: 0,
|
|
1156
|
+
blockedPaths: [],
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
if (analysis.tables.some((table) => !policy.allowedTables.includes(table))) {
|
|
1160
|
+
return decision("deny", ["postgres_table_not_allowed"], config, {
|
|
1161
|
+
maxFilesChanged: 0,
|
|
1162
|
+
blockedPaths: analysis.tables,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
if (analysis.tables.some((table) => !table.includes("."))) {
|
|
1166
|
+
return decision("deny", ["postgres_table_must_be_schema_qualified"], config, {
|
|
1167
|
+
maxFilesChanged: 0,
|
|
1168
|
+
blockedPaths: analysis.tables,
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
if (analysis.functions.some((functionName) => !policy.allowedFunctions.includes(functionName))) {
|
|
1172
|
+
return decision("deny", ["postgres_function_not_allowed"], config, {
|
|
1173
|
+
maxFilesChanged: 0,
|
|
1174
|
+
blockedPaths: analysis.functions,
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
if (maxRows > policy.maxRows) {
|
|
1178
|
+
return decision("deny", ["postgres_row_limit_exceeded"], config, {
|
|
1179
|
+
maxFilesChanged: 0,
|
|
1180
|
+
blockedPaths: [],
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
if (policy.requirePredicate && !analysis.hasPredicate) {
|
|
1184
|
+
return decision("step_up", ["postgres_unpredicated_scan_step_up"], config, {
|
|
1185
|
+
maxFilesChanged: 0,
|
|
1186
|
+
blockedPaths: [],
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
return decision("allow", ["postgres_select_inside_policy"], config, {
|
|
1190
|
+
maxFilesChanged: 0,
|
|
1191
|
+
blockedPaths: analysis.tables,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
function applyProvenancePolicy(action, config, baseDecision) {
|
|
1195
|
+
if (baseDecision.decision === "deny")
|
|
1196
|
+
return baseDecision;
|
|
1197
|
+
const guard = evaluateProvenanceGuard(action, config);
|
|
1198
|
+
if (!guard)
|
|
1199
|
+
return baseDecision;
|
|
1200
|
+
if (guard.decision === "deny") {
|
|
1201
|
+
return { ...baseDecision, decision: "deny", reasons: [guard.reason] };
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
...baseDecision,
|
|
1205
|
+
decision: "step_up",
|
|
1206
|
+
reasons: [...new Set([...baseDecision.reasons, guard.reason])],
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Escalate-only behavior guard (M5.6). Maps the proxy-supplied novelty/sequence
|
|
1211
|
+
* signals to `step_up` reason codes. It may ONLY raise an `allow` to `step_up`;
|
|
1212
|
+
* it never returns `allow` and never weakens a `deny`/`step_up`. This is the
|
|
1213
|
+
* code-level enforcement of the PRD §4 fence: a signal can escalate, never
|
|
1214
|
+
* authorize.
|
|
1215
|
+
*/
|
|
1216
|
+
function applyBehaviorPolicy(config, context, decision) {
|
|
1217
|
+
if (decision.decision !== "allow")
|
|
1218
|
+
return decision;
|
|
1219
|
+
const reasons = behaviorStepUpReasons(config, context.behaviorSignals);
|
|
1220
|
+
if (reasons.length === 0)
|
|
1221
|
+
return decision;
|
|
1222
|
+
return {
|
|
1223
|
+
...decision,
|
|
1224
|
+
decision: "step_up",
|
|
1225
|
+
reasons: [...new Set([...decision.reasons, ...reasons])],
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
function behaviorStepUpReasons(config, signals) {
|
|
1229
|
+
if (!config.behavior.enabled || !signals)
|
|
1230
|
+
return [];
|
|
1231
|
+
const reasons = [];
|
|
1232
|
+
if (config.behavior.firstSeen && signals.firstSeenCombo) {
|
|
1233
|
+
reasons.push("behavior_first_seen");
|
|
1234
|
+
}
|
|
1235
|
+
if (config.behavior.abnormalSequence && signals.abnormalSequence) {
|
|
1236
|
+
reasons.push("behavior_abnormal_sequence");
|
|
1237
|
+
}
|
|
1238
|
+
return reasons;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* The step-up reason codes the behavior guard would emit for the given signals
|
|
1242
|
+
* and config. Read-only; used by the proxy/tests to inspect the guard without
|
|
1243
|
+
* a full policy evaluation.
|
|
1244
|
+
*/
|
|
1245
|
+
export function evaluateBehaviorGuard(configInput = {}, signals = undefined) {
|
|
1246
|
+
const config = PolicyConfigSchema.parse(configInput);
|
|
1247
|
+
return behaviorStepUpReasons(config, signals);
|
|
1248
|
+
}
|
|
1249
|
+
export function evaluateProvenanceGuard(actionInput, configInput = {}) {
|
|
1250
|
+
const action = NormalizedActionSchema.parse(actionInput);
|
|
1251
|
+
const config = PolicyConfigSchema.parse(configInput);
|
|
1252
|
+
if (!config.provenance.enabled)
|
|
1253
|
+
return null;
|
|
1254
|
+
const fields = PROVENANCE_COVERAGE[action.capability.tool];
|
|
1255
|
+
if (!fields)
|
|
1256
|
+
return null;
|
|
1257
|
+
const presentFields = fields.filter((field) => action.capability.payload[field] !== undefined);
|
|
1258
|
+
if (presentFields.length === 0)
|
|
1259
|
+
return null;
|
|
1260
|
+
const provenance = action.provenance;
|
|
1261
|
+
if (!provenance) {
|
|
1262
|
+
return { decision: "deny", reason: "provenance_unknown_egress" };
|
|
1263
|
+
}
|
|
1264
|
+
const sourcesById = new Map(provenance.sources.map((source) => [source.id, source]));
|
|
1265
|
+
let untrusted = false;
|
|
1266
|
+
for (const field of presentFields) {
|
|
1267
|
+
const path = `/capability/payload/${field}`;
|
|
1268
|
+
const binding = provenance.bindings.find((entry) => entry.path === path);
|
|
1269
|
+
if (!binding) {
|
|
1270
|
+
return { decision: "deny", reason: "provenance_unknown_egress" };
|
|
1271
|
+
}
|
|
1272
|
+
const sources = binding.sourceIds.map((id) => sourcesById.get(id));
|
|
1273
|
+
if (sources.some((source) => !source || source.integrity === "unknown")) {
|
|
1274
|
+
return { decision: "deny", reason: "provenance_unknown_egress" };
|
|
1275
|
+
}
|
|
1276
|
+
if (sources.some((source) => source?.integrity === "untrusted")) {
|
|
1277
|
+
untrusted = true;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
if (!untrusted)
|
|
1281
|
+
return null;
|
|
1282
|
+
return {
|
|
1283
|
+
decision: config.provenance.untrustedDecision,
|
|
1284
|
+
reason: "provenance_untrusted_egress",
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
function evaluateRulePolicy(action, config, context) {
|
|
1288
|
+
const traces = config.rules.map((rule) => traceRule(rule, action));
|
|
1289
|
+
const matching = config.rules.filter((rule, index) => rule.enabled && traces[index]?.matched);
|
|
1290
|
+
if (matching.length === 0) {
|
|
1291
|
+
return {
|
|
1292
|
+
decision: decision(config.defaultDecision, ["policy_default_deny"], config, { maxFilesChanged: 0, blockedPaths: [] }, { ruleId: "default_deny", matchedRuleIds: [] }),
|
|
1293
|
+
traces,
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
const selectedEffect = matching.some((rule) => rule.effect === "deny")
|
|
1297
|
+
? "deny"
|
|
1298
|
+
: matching.some((rule) => rule.effect === "step_up")
|
|
1299
|
+
? "step_up"
|
|
1300
|
+
: matching.some((rule) => rule.effect === "allow" &&
|
|
1301
|
+
rule.obligations.requiredApproverRoles.length > 0)
|
|
1302
|
+
? "step_up"
|
|
1303
|
+
: "allow";
|
|
1304
|
+
const determining = matching.filter((rule) => {
|
|
1305
|
+
if (selectedEffect === "deny")
|
|
1306
|
+
return rule.effect === "deny";
|
|
1307
|
+
if (selectedEffect === "step_up") {
|
|
1308
|
+
return (rule.effect === "step_up" ||
|
|
1309
|
+
(rule.effect === "allow" &&
|
|
1310
|
+
rule.obligations.requiredApproverRoles.length > 0));
|
|
1311
|
+
}
|
|
1312
|
+
return rule.effect === "allow";
|
|
1313
|
+
});
|
|
1314
|
+
const obligations = mergeRuleObligations(matching);
|
|
1315
|
+
const blockedPath = actionPaths(action).find((path) => obligations.blockedPaths.some((prefix) => path.startsWith(prefix)));
|
|
1316
|
+
const now = context.now ?? new Date();
|
|
1317
|
+
const timeWindowClosed = obligations.timeWindow && !insideTimeWindow(now, obligations.timeWindow);
|
|
1318
|
+
const rateLimitExceeded = obligations.rateLimit &&
|
|
1319
|
+
(context.rateLimitCounts?.[obligations.rateLimit.ruleId] ?? 0) >=
|
|
1320
|
+
obligations.rateLimit.maxActions;
|
|
1321
|
+
let effect = selectedEffect;
|
|
1322
|
+
const reasons = determining.map((rule) => rule.reason ?? `policy_rule:${rule.id}`);
|
|
1323
|
+
if (blockedPath) {
|
|
1324
|
+
effect = "deny";
|
|
1325
|
+
reasons.splice(0, reasons.length, `obligation_blocked_path:${blockedPath}`);
|
|
1326
|
+
}
|
|
1327
|
+
else if (timeWindowClosed) {
|
|
1328
|
+
effect = "deny";
|
|
1329
|
+
reasons.splice(0, reasons.length, "obligation_time_window_closed");
|
|
1330
|
+
}
|
|
1331
|
+
else if (rateLimitExceeded) {
|
|
1332
|
+
effect = "deny";
|
|
1333
|
+
reasons.splice(0, reasons.length, "obligation_rate_limit_exceeded");
|
|
1334
|
+
}
|
|
1335
|
+
else if (effect === "step_up" &&
|
|
1336
|
+
obligations.requiredApproverRoles.length > 0 &&
|
|
1337
|
+
!reasons.includes("required_approver_role")) {
|
|
1338
|
+
reasons.push("required_approver_role");
|
|
1339
|
+
}
|
|
1340
|
+
const primaryRule = determining[0] ?? matching[0];
|
|
1341
|
+
return {
|
|
1342
|
+
decision: decision(effect, reasons, config, {
|
|
1343
|
+
maxFilesChanged: 0,
|
|
1344
|
+
blockedPaths: obligations.blockedPaths,
|
|
1345
|
+
}, {
|
|
1346
|
+
ruleId: primaryRule.id,
|
|
1347
|
+
matchedRuleIds: matching.map((rule) => rule.id),
|
|
1348
|
+
expiresInSeconds: obligations.expiresInSeconds,
|
|
1349
|
+
requiredApproverRoles: obligations.requiredApproverRoles,
|
|
1350
|
+
timeWindow: obligations.timeWindow,
|
|
1351
|
+
rateLimit: obligations.rateLimit,
|
|
1352
|
+
}),
|
|
1353
|
+
traces,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
606
1356
|
function evaluateGitHubContentWrite(action, config) {
|
|
607
1357
|
const policy = config.github.pullRequests;
|
|
608
1358
|
const path = getString(action.capability.payload.path);
|
|
@@ -657,6 +1407,86 @@ function evaluateGitHubBranchCreate(action, config) {
|
|
|
657
1407
|
blockedPaths: [],
|
|
658
1408
|
});
|
|
659
1409
|
}
|
|
1410
|
+
function evaluateGitHubReviewComment(action, config) {
|
|
1411
|
+
const policy = config.github.pullRequests;
|
|
1412
|
+
const payload = action.capability.payload;
|
|
1413
|
+
const path = getString(payload.path);
|
|
1414
|
+
const body = getString(payload.body);
|
|
1415
|
+
const commitId = getString(payload.commitId);
|
|
1416
|
+
const side = getString(payload.side);
|
|
1417
|
+
const pullNumber = getPositiveInteger(payload.pullNumber);
|
|
1418
|
+
const line = getPositiveInteger(payload.line);
|
|
1419
|
+
if (!path || !body || !commitId || !pullNumber || !line) {
|
|
1420
|
+
return decision("deny", ["github_review_comment_payload_invalid"], config, {
|
|
1421
|
+
maxFilesChanged: 0,
|
|
1422
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
if (side !== "LEFT" && side !== "RIGHT") {
|
|
1426
|
+
return decision("deny", ["github_review_comment_side_invalid"], config, {
|
|
1427
|
+
maxFilesChanged: 0,
|
|
1428
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
if (pathsMatch([path], policy.denyPathPrefixes)) {
|
|
1432
|
+
return decision("deny", ["payload_touches_denied_path"], config, {
|
|
1433
|
+
maxFilesChanged: 0,
|
|
1434
|
+
blockedPaths: policy.denyPathPrefixes,
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
if (pathsMatch([path], policy.stepUpPathPrefixes)) {
|
|
1438
|
+
return decision("step_up", ["github_review_protected_path_step_up"], config, {
|
|
1439
|
+
maxFilesChanged: 0,
|
|
1440
|
+
blockedPaths: [
|
|
1441
|
+
...policy.denyPathPrefixes,
|
|
1442
|
+
...policy.stepUpPathPrefixes,
|
|
1443
|
+
],
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
return decision("allow", ["github_review_comment_inside_policy"], config, {
|
|
1447
|
+
maxFilesChanged: 0,
|
|
1448
|
+
blockedPaths: [
|
|
1449
|
+
...policy.denyPathPrefixes,
|
|
1450
|
+
...policy.stepUpPathPrefixes,
|
|
1451
|
+
],
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
function evaluateGitHubCheckRun(action, config) {
|
|
1455
|
+
const payload = action.capability.payload;
|
|
1456
|
+
const name = getString(payload.name);
|
|
1457
|
+
const headSha = getString(payload.headSha);
|
|
1458
|
+
const status = getString(payload.status);
|
|
1459
|
+
const conclusion = getString(payload.conclusion);
|
|
1460
|
+
if (!name || !headSha) {
|
|
1461
|
+
return decision("deny", ["github_check_run_payload_invalid"], config, {
|
|
1462
|
+
maxFilesChanged: 0,
|
|
1463
|
+
blockedPaths: [],
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
if (status === "completed" && !conclusion) {
|
|
1467
|
+
return decision("deny", ["github_check_run_conclusion_required"], config, {
|
|
1468
|
+
maxFilesChanged: 0,
|
|
1469
|
+
blockedPaths: [],
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
return decision("allow", ["github_check_run_inside_policy"], config, {
|
|
1473
|
+
maxFilesChanged: 0,
|
|
1474
|
+
blockedPaths: [],
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
function evaluateGitHubIssueComment(action, config) {
|
|
1478
|
+
const payload = action.capability.payload;
|
|
1479
|
+
if (!getPositiveInteger(payload.issueNumber) || !getString(payload.body)) {
|
|
1480
|
+
return decision("deny", ["github_issue_comment_payload_invalid"], config, {
|
|
1481
|
+
maxFilesChanged: 0,
|
|
1482
|
+
blockedPaths: [],
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
return decision("allow", ["github_issue_comment_inside_policy"], config, {
|
|
1486
|
+
maxFilesChanged: 0,
|
|
1487
|
+
blockedPaths: [],
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
660
1490
|
export function toCedarRequest(actionInput) {
|
|
661
1491
|
const action = NormalizedActionSchema.parse(actionInput);
|
|
662
1492
|
return {
|
|
@@ -1050,7 +1880,7 @@ function evaluateMcpToolCall(action, config) {
|
|
|
1050
1880
|
blockedPaths: [],
|
|
1051
1881
|
});
|
|
1052
1882
|
}
|
|
1053
|
-
function decision(value, reasons, config, constraints) {
|
|
1883
|
+
function decision(value, reasons, config, constraints, options) {
|
|
1054
1884
|
return {
|
|
1055
1885
|
decision: value,
|
|
1056
1886
|
reasons,
|
|
@@ -1061,12 +1891,128 @@ function decision(value, reasons, config, constraints) {
|
|
|
1061
1891
|
opaCompatible: true,
|
|
1062
1892
|
},
|
|
1063
1893
|
constraints: {
|
|
1064
|
-
expiresInSeconds: DEFAULT_EXPIRES_IN_SECONDS,
|
|
1894
|
+
expiresInSeconds: options?.expiresInSeconds ?? DEFAULT_EXPIRES_IN_SECONDS,
|
|
1065
1895
|
maxFilesChanged: constraints.maxFilesChanged,
|
|
1066
1896
|
blockedPaths: constraints.blockedPaths,
|
|
1067
1897
|
},
|
|
1898
|
+
rule: options
|
|
1899
|
+
? {
|
|
1900
|
+
id: options.ruleId,
|
|
1901
|
+
matchedRuleIds: options.matchedRuleIds,
|
|
1902
|
+
}
|
|
1903
|
+
: undefined,
|
|
1904
|
+
obligations: options
|
|
1905
|
+
? {
|
|
1906
|
+
requiredApproverRoles: options.requiredApproverRoles ?? [],
|
|
1907
|
+
timeWindow: options.timeWindow,
|
|
1908
|
+
rateLimit: options.rateLimit,
|
|
1909
|
+
}
|
|
1910
|
+
: undefined,
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
function traceRule(rule, action) {
|
|
1914
|
+
const mismatches = [];
|
|
1915
|
+
if (!rule.enabled) {
|
|
1916
|
+
mismatches.push("rule_disabled");
|
|
1917
|
+
}
|
|
1918
|
+
if (rule.match.tools &&
|
|
1919
|
+
!matchesAny(action.capability.tool, asMatchers(rule.match.tools))) {
|
|
1920
|
+
mismatches.push("tool");
|
|
1921
|
+
}
|
|
1922
|
+
if (rule.match.resources &&
|
|
1923
|
+
!matchesAny(action.capability.resource, asMatchers(rule.match.resources))) {
|
|
1924
|
+
mismatches.push("resource");
|
|
1925
|
+
}
|
|
1926
|
+
const actor = rule.match.actors;
|
|
1927
|
+
if (actor?.agentIds && !matchesAny(action.actor.agentId, asMatchers(actor.agentIds))) {
|
|
1928
|
+
mismatches.push("actor.agentId");
|
|
1929
|
+
}
|
|
1930
|
+
if (actor?.humanOwners &&
|
|
1931
|
+
!matchesAny(action.actor.humanOwner, asMatchers(actor.humanOwners))) {
|
|
1932
|
+
mismatches.push("actor.humanOwner");
|
|
1933
|
+
}
|
|
1934
|
+
if (actor?.runtimes &&
|
|
1935
|
+
!matchesAny(action.actor.runtime, asMatchers(actor.runtimes))) {
|
|
1936
|
+
mismatches.push("actor.runtime");
|
|
1937
|
+
}
|
|
1938
|
+
if (actor?.tenants &&
|
|
1939
|
+
!matchesAny(action.actor.tenant ?? "", asMatchers(actor.tenants))) {
|
|
1940
|
+
mismatches.push("actor.tenant");
|
|
1941
|
+
}
|
|
1942
|
+
return {
|
|
1943
|
+
ruleId: rule.id,
|
|
1944
|
+
matched: mismatches.length === 0,
|
|
1945
|
+
effect: rule.effect,
|
|
1946
|
+
mismatches,
|
|
1947
|
+
obligations: rule.obligations,
|
|
1068
1948
|
};
|
|
1069
1949
|
}
|
|
1950
|
+
function asMatchers(value) {
|
|
1951
|
+
return Array.isArray(value) ? value : [value];
|
|
1952
|
+
}
|
|
1953
|
+
function matchesAny(value, patterns) {
|
|
1954
|
+
return patterns.some((pattern) => wildcardMatch(value, pattern));
|
|
1955
|
+
}
|
|
1956
|
+
function wildcardMatch(value, pattern) {
|
|
1957
|
+
if (pattern === "*") {
|
|
1958
|
+
return true;
|
|
1959
|
+
}
|
|
1960
|
+
const expression = pattern
|
|
1961
|
+
.split("*")
|
|
1962
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
1963
|
+
.join(".*");
|
|
1964
|
+
return new RegExp(`^${expression}$`).test(value);
|
|
1965
|
+
}
|
|
1966
|
+
function actionPaths(action) {
|
|
1967
|
+
return [
|
|
1968
|
+
...getStringArray(action.capability.payload.filesChanged),
|
|
1969
|
+
...getStringArray(action.capability.payload.paths),
|
|
1970
|
+
getString(action.capability.payload.path),
|
|
1971
|
+
getString(action.capability.payload.prefix),
|
|
1972
|
+
].filter((value) => Boolean(value));
|
|
1973
|
+
}
|
|
1974
|
+
function mergeRuleObligations(rules) {
|
|
1975
|
+
const expires = rules
|
|
1976
|
+
.map((rule) => rule.obligations.expiresInSeconds)
|
|
1977
|
+
.filter((value) => value !== undefined);
|
|
1978
|
+
const timeWindows = rules
|
|
1979
|
+
.map((rule) => rule.obligations.timeWindow)
|
|
1980
|
+
.filter((value) => value !== undefined);
|
|
1981
|
+
const rateLimits = rules.flatMap((rule) => rule.obligations.rateLimit
|
|
1982
|
+
? [
|
|
1983
|
+
{
|
|
1984
|
+
ruleId: rule.id,
|
|
1985
|
+
...rule.obligations.rateLimit,
|
|
1986
|
+
},
|
|
1987
|
+
]
|
|
1988
|
+
: []);
|
|
1989
|
+
return {
|
|
1990
|
+
expiresInSeconds: expires.length > 0 ? Math.min(...expires) : DEFAULT_EXPIRES_IN_SECONDS,
|
|
1991
|
+
requiredApproverRoles: [
|
|
1992
|
+
...new Set(rules.flatMap((rule) => rule.obligations.requiredApproverRoles)),
|
|
1993
|
+
].sort(),
|
|
1994
|
+
blockedPaths: [
|
|
1995
|
+
...new Set(rules.flatMap((rule) => rule.obligations.blockedPathPrefixes)),
|
|
1996
|
+
].sort(),
|
|
1997
|
+
timeWindow: timeWindows[0],
|
|
1998
|
+
rateLimit: rateLimits.sort((left, right) => left.maxActions / left.windowSeconds -
|
|
1999
|
+
right.maxActions / right.windowSeconds)[0],
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
function insideTimeWindow(now, window) {
|
|
2003
|
+
if (window.daysOfWeek.length > 0 &&
|
|
2004
|
+
!window.daysOfWeek.includes(now.getUTCDay())) {
|
|
2005
|
+
return false;
|
|
2006
|
+
}
|
|
2007
|
+
const hour = now.getUTCHours();
|
|
2008
|
+
if (window.startHourUtc === window.endHourUtc) {
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
2011
|
+
if (window.startHourUtc < window.endHourUtc) {
|
|
2012
|
+
return hour >= window.startHourUtc && hour < window.endHourUtc;
|
|
2013
|
+
}
|
|
2014
|
+
return hour >= window.startHourUtc || hour < window.endHourUtc;
|
|
2015
|
+
}
|
|
1070
2016
|
function resourceToCedar(resource) {
|
|
1071
2017
|
const [kind, id] = resource.split(":", 2);
|
|
1072
2018
|
return {
|
|
@@ -1083,87 +2029,6 @@ function isUnsafeRelativePath(path) {
|
|
|
1083
2029
|
}
|
|
1084
2030
|
return path.split(/[\\/]+/).some((segment) => segment === ".." || segment === "");
|
|
1085
2031
|
}
|
|
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
2032
|
function collectPatchPaths(input, prefix = "") {
|
|
1168
2033
|
if (!isPlainObject(input)) {
|
|
1169
2034
|
return prefix ? [prefix] : [];
|
|
@@ -1196,6 +2061,12 @@ function getString(value) {
|
|
|
1196
2061
|
function getNumber(value) {
|
|
1197
2062
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1198
2063
|
}
|
|
2064
|
+
function getPositiveInteger(value) {
|
|
2065
|
+
const number = getNumber(value);
|
|
2066
|
+
return number !== null && Number.isInteger(number) && number > 0
|
|
2067
|
+
? number
|
|
2068
|
+
: null;
|
|
2069
|
+
}
|
|
1199
2070
|
function deepMerge(base, patch) {
|
|
1200
2071
|
if (Array.isArray(base) || Array.isArray(patch)) {
|
|
1201
2072
|
return (patch ?? base);
|