@armoriq/policy-ir 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1373 @@
1
+ // src/ir.ts
2
+ var SCHEMA_VERSION = "armor.policy.v2";
3
+ function layerOfTarget(target) {
4
+ if ("org" in target) return "floor";
5
+ if ("group" in target) return "group";
6
+ if ("agent" in target) return "agent";
7
+ return "local";
8
+ }
9
+
10
+ // src/canonical.ts
11
+ function canonicalJson(value) {
12
+ if (value === null || value === void 0) return "null";
13
+ switch (typeof value) {
14
+ case "number":
15
+ return canonicalNumber(value);
16
+ case "boolean":
17
+ return value ? "true" : "false";
18
+ case "string":
19
+ return escapeAscii(value);
20
+ case "object":
21
+ return Array.isArray(value) ? "[" + value.map(canonicalJson).join(",") + "]" : canonicalObject(value);
22
+ default:
23
+ throw new TypeError(`unsupported type in canonical JSON: ${typeof value}`);
24
+ }
25
+ }
26
+ function canonicalNumber(value) {
27
+ if (!Number.isFinite(value)) {
28
+ throw new TypeError("non-finite number is not valid in canonical JSON");
29
+ }
30
+ return JSON.stringify(value);
31
+ }
32
+ function canonicalObject(obj) {
33
+ const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
34
+ return "{" + keys.map((k) => escapeAscii(k) + ":" + canonicalJson(obj[k])).join(",") + "}";
35
+ }
36
+ var SHORT_ESCAPES = {
37
+ 8: "\\b",
38
+ 9: "\\t",
39
+ 10: "\\n",
40
+ 12: "\\f",
41
+ 13: "\\r",
42
+ 34: '\\"',
43
+ 92: "\\\\"
44
+ };
45
+ function escapeAscii(s) {
46
+ let out = '"';
47
+ for (let i = 0; i < s.length; i++) {
48
+ const code = s.charCodeAt(i);
49
+ const short = SHORT_ESCAPES[code];
50
+ if (short !== void 0) out += short;
51
+ else if (code < 32 || code > 126) out += "\\u" + code.toString(16).padStart(4, "0");
52
+ else out += s[i];
53
+ }
54
+ return out + '"';
55
+ }
56
+
57
+ // src/hash.ts
58
+ import { createHash } from "crypto";
59
+ var HASH_PREFIX = "sha256:";
60
+ function sha256Canonical(value) {
61
+ return HASH_PREFIX + createHash("sha256").update(canonicalJson(value), "utf8").digest("hex");
62
+ }
63
+ function policyHash(document) {
64
+ const { policyHash: _omitted, ...rest } = document;
65
+ return sha256Canonical(rest);
66
+ }
67
+ function verifyPolicyHash(document) {
68
+ return typeof document.policyHash === "string" && document.policyHash === policyHash(document);
69
+ }
70
+
71
+ // src/schema-hash.ts
72
+ import { createHash as createHash2 } from "crypto";
73
+ function schemaHash(inputSchema) {
74
+ if (inputSchema === null || inputSchema === void 0) return null;
75
+ const canonical = canonicalizeSchema(inputSchema, inputSchema, /* @__PURE__ */ new Set());
76
+ return "sha256:" + createHash2("sha256").update(canonicalJson(canonical), "utf8").digest("hex");
77
+ }
78
+ var SCHEMA_MAP_KEYWORDS = /* @__PURE__ */ new Set([
79
+ "properties",
80
+ "patternProperties",
81
+ "$defs",
82
+ "definitions",
83
+ "dependentSchemas"
84
+ ]);
85
+ var DATA_KEYWORDS = /* @__PURE__ */ new Set(["enum", "const", "default", "examples"]);
86
+ var ANNOTATION_KEYS = /* @__PURE__ */ new Set(["description", "title"]);
87
+ function canonicalizeSchema(node, root, seen, isSchemaMap = false) {
88
+ if (Array.isArray(node)) return node.map((item) => canonicalizeSchema(item, root, seen));
89
+ if (node === null || typeof node !== "object") return node;
90
+ const obj = node;
91
+ if (isSchemaMap) return mapValues(obj, (v) => canonicalizeSchema(v, root, seen));
92
+ const ref = obj["$ref"];
93
+ if (typeof ref === "string" && ref.startsWith("#")) return canonicalizeRef(obj, ref, root, seen);
94
+ return canonicalizeKeywords(obj, root, seen);
95
+ }
96
+ function canonicalizeKeywords(obj, root, seen) {
97
+ const out = {};
98
+ for (const [k, v] of Object.entries(obj)) {
99
+ if (ANNOTATION_KEYS.has(k)) continue;
100
+ out[k] = DATA_KEYWORDS.has(k) ? v : canonicalizeSchema(v, root, seen, SCHEMA_MAP_KEYWORDS.has(k));
101
+ }
102
+ return out;
103
+ }
104
+ function canonicalizeRef(obj, ref, root, seen) {
105
+ if (seen.has(ref)) return { $ref: ref };
106
+ const target = resolvePointer(root, ref);
107
+ if (target === void 0) return { $ref: ref };
108
+ const next = new Set(seen);
109
+ next.add(ref);
110
+ const resolved = canonicalizeSchema(target, root, next);
111
+ const { $ref: _ref, ...siblings } = obj;
112
+ if (Object.keys(siblings).length === 0) return resolved;
113
+ const base = isPlainObject(resolved) ? { ...resolved } : { $ref: ref };
114
+ return Object.assign(base, canonicalizeKeywords(siblings, root, next));
115
+ }
116
+ function resolvePointer(root, ref) {
117
+ if (ref === "#") return root;
118
+ if (!ref.startsWith("#/")) return void 0;
119
+ let node = root;
120
+ for (const rawSeg of ref.slice(2).split("/")) {
121
+ const seg = decodeURIComponent(rawSeg).replace(/~1/g, "/").replace(/~0/g, "~");
122
+ node = childOf(node, seg);
123
+ if (node === void 0) return void 0;
124
+ }
125
+ return node;
126
+ }
127
+ function childOf(node, seg) {
128
+ if (node === null || typeof node !== "object") return void 0;
129
+ return Array.isArray(node) ? node[Number(seg)] : node[seg];
130
+ }
131
+ function isPlainObject(value) {
132
+ return value !== null && typeof value === "object" && !Array.isArray(value);
133
+ }
134
+ function mapValues(obj, fn) {
135
+ const out = {};
136
+ for (const [k, v] of Object.entries(obj)) out[k] = fn(v);
137
+ return out;
138
+ }
139
+
140
+ // src/evaluate/input.ts
141
+ var VERDICT_RANK = { allow: 0, hold: 1, block: 2 };
142
+ function stricter(a, b) {
143
+ return VERDICT_RANK[a] >= VERDICT_RANK[b] ? a : b;
144
+ }
145
+
146
+ // src/evaluate/glob.ts
147
+ function globToRegExp(pattern) {
148
+ let source = "^";
149
+ for (let i = 0; i < pattern.length; i++) {
150
+ const ch = pattern[i];
151
+ if (ch === "*") {
152
+ const double = pattern[i + 1] === "*";
153
+ source += double ? ".*" : "[^/]*";
154
+ if (double) i++;
155
+ } else if (ch === "?") {
156
+ source += "[^/]";
157
+ } else {
158
+ source += escapeRegExp(ch);
159
+ }
160
+ }
161
+ return new RegExp(source + "$");
162
+ }
163
+ function matchGlob(pattern, value) {
164
+ return globToRegExp(pattern).test(value);
165
+ }
166
+ function isGlob(key) {
167
+ return key.includes("*") || key.includes("?");
168
+ }
169
+ function escapeRegExp(ch) {
170
+ return /[\\^$.|?*+()[\]{}]/.test(ch) ? "\\" + ch : ch;
171
+ }
172
+
173
+ // src/evaluate/conditions.ts
174
+ function resolveField(input, field) {
175
+ const [root, ...rest] = field.split(".");
176
+ const base = root === "args" ? input.arguments : input.derived?.[root ?? ""];
177
+ return rest.reduce(childOf2, base);
178
+ }
179
+ function childOf2(node, key) {
180
+ if (node === null || typeof node !== "object") return void 0;
181
+ return node[key];
182
+ }
183
+ function conditionsHold(input, conditions) {
184
+ return conditions.every((c) => conditionHolds(input, c));
185
+ }
186
+ function conditionHolds(input, condition) {
187
+ const actual = resolveField(input, condition.field);
188
+ if (condition.op === "exists") return actual !== void 0 === (condition.value ?? true);
189
+ if (actual === void 0) return false;
190
+ return compare(condition.op, actual, condition.value);
191
+ }
192
+ var COMPARATORS = {
193
+ eq: (a, e) => a === e,
194
+ ne: (a, e) => a !== e,
195
+ gt: (a, e) => numeric(a, e, (x, y) => x > y),
196
+ gte: (a, e) => numeric(a, e, (x, y) => x >= y),
197
+ lt: (a, e) => numeric(a, e, (x, y) => x < y),
198
+ lte: (a, e) => numeric(a, e, (x, y) => x <= y),
199
+ in: (a, e) => Array.isArray(e) && e.includes(a),
200
+ notIn: (a, e) => Array.isArray(e) && !e.includes(a),
201
+ glob: (a, e) => strings(a, e, (x, y) => matchGlob(y, x)),
202
+ regex: (a, e) => strings(a, e, (x, y) => new RegExp(y).test(x)),
203
+ notRegex: (a, e) => strings(a, e, (x, y) => !new RegExp(y).test(x)),
204
+ startsWith: (a, e) => strings(a, e, (x, y) => x.startsWith(y))
205
+ };
206
+ function compare(op, actual, expected) {
207
+ return COMPARATORS[op](actual, expected);
208
+ }
209
+ function strings(a, e, test) {
210
+ return typeof a === "string" && typeof e === "string" && test(a, e);
211
+ }
212
+ function numeric(a, e, test) {
213
+ return typeof a === "number" && typeof e === "number" && test(a, e);
214
+ }
215
+
216
+ // src/evaluate/select.ts
217
+ function selectRuleKey(statements, tool) {
218
+ let best = null;
219
+ for (const key of ruleKeysOf(statements)) {
220
+ if (!keyMatches(key, tool)) continue;
221
+ if (best === null || keyBeats(key, best)) best = key;
222
+ }
223
+ return best;
224
+ }
225
+ function ruleKeysOf(statements) {
226
+ const keys = /* @__PURE__ */ new Set();
227
+ for (const s of statements) if ("tool" in s.match) keys.add(s.match.tool);
228
+ return keys;
229
+ }
230
+ function keyMatches(key, tool) {
231
+ return isGlob(key) ? matchGlob(key, tool) : key === tool;
232
+ }
233
+ function keyBeats(candidate, incumbent) {
234
+ const candidateGlob = isGlob(candidate);
235
+ const incumbentGlob = isGlob(incumbent);
236
+ if (candidateGlob !== incumbentGlob) return !candidateGlob;
237
+ return candidate.length > incumbent.length;
238
+ }
239
+ function variantsOf(statements, ruleKey) {
240
+ return statements.filter((s) => "tool" in s.match && s.match.tool === ruleKey).sort((a, b) => "tool" in a.match && "tool" in b.match ? a.match.order - b.match.order : 0);
241
+ }
242
+ function capabilityStatements(statements, tags) {
243
+ return statements.filter((s) => "capability" in s.match && tags.includes(s.match.capability));
244
+ }
245
+
246
+ // src/evaluate/layers.ts
247
+ function isLive(expiresAt, timestamp) {
248
+ return expiresAt === "never" || Date.parse(expiresAt) > Date.parse(timestamp);
249
+ }
250
+ function liveStatements(doc, input) {
251
+ if (!isLive(doc.expiresAt, input.timestamp)) return [];
252
+ return doc.statements.filter((s) => isLive(s.expiresAt, input.timestamp));
253
+ }
254
+ function memberScope(statements, principal) {
255
+ if (!principal) return [];
256
+ const subjects = statements.map((s) => s.subject).filter((s) => s !== null);
257
+ const winner = strongestSubject(subjects, principal);
258
+ return winner ? statements.filter((s) => s.subject !== null && sameSubject(s.subject, winner)) : [];
259
+ }
260
+ function strongestSubject(subjects, principal) {
261
+ const byId = subjects.find((s) => "membershipId" in s && s.membershipId === principal.membershipId);
262
+ if (byId) return byId;
263
+ const email = principal.email?.toLowerCase();
264
+ const byEmail = subjects.find((s) => "email" in s && s.email.toLowerCase() === email);
265
+ if (byEmail) return byEmail;
266
+ const groups = principal.groups ?? [];
267
+ const byGroup = subjects.find((s) => "group" in s && groups.includes(s.group));
268
+ if (byGroup) return byGroup;
269
+ return subjects.find((s) => "product" in s && s.product === principal.product) ?? null;
270
+ }
271
+ function sameSubject(a, b) {
272
+ return JSON.stringify(a) === JSON.stringify(b);
273
+ }
274
+ function decideLayer(statements, input) {
275
+ const ruleDecision = decideRule(statements, input);
276
+ const capability = decideCapabilities(statements, input);
277
+ if (!ruleDecision) return capability;
278
+ if (!capability) return ruleDecision;
279
+ return stricter(capability.statement.effect, ruleDecision.statement.effect) === ruleDecision.statement.effect ? ruleDecision : capability;
280
+ }
281
+ function decideRule(statements, input) {
282
+ const key = selectRuleKey(statements, input.tool);
283
+ if (key === null) return null;
284
+ const winner = variantsOf(statements, key).find((v) => conditionsHold(input, v.conditions));
285
+ return winner ? { statement: winner, obligations: driftObligations(winner, input) } : null;
286
+ }
287
+ function decideCapabilities(statements, input) {
288
+ const matching = capabilityStatements(statements, input.capabilities ?? []).filter(
289
+ (s) => conditionsHold(input, s.conditions)
290
+ );
291
+ let best = null;
292
+ for (const s of matching) {
293
+ if (!best || stricter(s.effect, best.effect) === s.effect) best = s;
294
+ }
295
+ return best ? { statement: best, obligations: [] } : null;
296
+ }
297
+ function driftObligations(statement, input) {
298
+ if (input.schemaHash === void 0) return [];
299
+ const drifted = statement.conditions.map((c) => c.schemaBinding?.schemaHash ?? null).filter((bound) => bound !== null && bound !== input.schemaHash);
300
+ return drifted.map((bound) => ({
301
+ type: "schema_drift",
302
+ statementId: statement.id,
303
+ bound,
304
+ observed: input.schemaHash ?? null
305
+ }));
306
+ }
307
+
308
+ // src/evaluate/evaluate.ts
309
+ function evaluate(input, bound) {
310
+ const floor2 = decideFloor(bound.floor, input);
311
+ if (floor2?.statement.effect === "block") return fromDecision(floor2, bound.floor, "floor");
312
+ const candidate = decideCandidate(input, bound);
313
+ const floorVerdict = floor2?.statement.effect ?? null;
314
+ if (floorVerdict && stricter(floorVerdict, candidate.verdict) !== candidate.verdict) {
315
+ return { ...fromDecision(floor2, bound.floor, "floor"), floorVerdict };
316
+ }
317
+ return { ...candidate, floorVerdict };
318
+ }
319
+ function decideFloor(doc, input) {
320
+ if (!doc) return null;
321
+ return decideLayer(liveStatements(doc, input).filter((s) => s.subject === null), input);
322
+ }
323
+ function decideCandidate(input, bound) {
324
+ for (const doc of [bound.agent, bound.group]) {
325
+ if (!doc) continue;
326
+ const statements = liveStatements(doc, input);
327
+ const member = decideLayer(memberScope(statements, input.member), input);
328
+ if (member) return fromDecision(member, doc, "member");
329
+ const own = decideLayer(statements.filter((s) => s.subject === null), input);
330
+ if (own) return fromDecision(own, doc, doc.layer);
331
+ }
332
+ return fromDefault(mostSpecific(bound));
333
+ }
334
+ function mostSpecific(bound) {
335
+ const doc = bound.agent ?? bound.group ?? bound.floor;
336
+ if (!doc) throw new Error("evaluate: no document is bound to the target");
337
+ return doc;
338
+ }
339
+ function fromDecision(decision, doc, layer) {
340
+ const { statement } = decision;
341
+ return {
342
+ verdict: statement.effect,
343
+ floorVerdict: null,
344
+ approval: statement.approval ?? null,
345
+ obligations: decision.obligations,
346
+ provenance: provenanceOf(statement, doc.policyId, layer)
347
+ };
348
+ }
349
+ function provenanceOf(statement, policyId, layer) {
350
+ return {
351
+ policyId,
352
+ statementId: statement.id,
353
+ ruleKey: statement.provenance.ruleKey,
354
+ variant: statement.provenance.variant,
355
+ layer
356
+ };
357
+ }
358
+ function fromDefault(doc) {
359
+ const verdict = doc.defaults.tools;
360
+ return {
361
+ verdict,
362
+ floorVerdict: null,
363
+ approval: null,
364
+ obligations: [],
365
+ provenance: { policyId: doc.policyId, statementId: null, ruleKey: "tools.default", variant: -1, layer: doc.layer }
366
+ };
367
+ }
368
+
369
+ // src/document/parse.ts
370
+ import { parse as parseYamlText } from "yaml";
371
+
372
+ // src/document/diagnostics.ts
373
+ var Diagnostics = class {
374
+ items = [];
375
+ error(code, path, message) {
376
+ this.items.push({ code, severity: "error", path, message });
377
+ }
378
+ warning(code, path, message) {
379
+ this.items.push({ code, severity: "warning", path, message });
380
+ }
381
+ get hasErrors() {
382
+ return this.items.some((d) => d.severity === "error");
383
+ }
384
+ };
385
+
386
+ // src/document/grammar.ts
387
+ var SOURCE_OPERATORS = [
388
+ "eq",
389
+ "neq",
390
+ "in",
391
+ "notIn",
392
+ "gt",
393
+ "gte",
394
+ "lt",
395
+ "lte",
396
+ "matches",
397
+ "notMatches",
398
+ "startsWith",
399
+ "glob",
400
+ "withinWorkspace",
401
+ "exists"
402
+ ];
403
+ var VERDICTS = ["allow", "hold", "block"];
404
+ var NEVER = "never";
405
+
406
+ // src/document/reader.ts
407
+ var Reader = class {
408
+ constructor(diag) {
409
+ this.diag = diag;
410
+ }
411
+ diag;
412
+ object(value, path, keys) {
413
+ if (!isPlainObject2(value)) {
414
+ this.diag.error("expected_object", path, `expected a mapping, got ${describe(value)}`);
415
+ return null;
416
+ }
417
+ for (const key of Object.keys(value)) {
418
+ if (!keys.includes(key)) this.diag.error("unknown_key", `${path}/${key}`, `unknown key "${key}"`);
419
+ }
420
+ return value;
421
+ }
422
+ /** A mapping whose keys are user-chosen (rule keys, member keys). */
423
+ map(value, path) {
424
+ if (isPlainObject2(value)) return value;
425
+ this.diag.error("expected_object", path, `expected a mapping, got ${describe(value)}`);
426
+ return null;
427
+ }
428
+ string(value, path) {
429
+ if (typeof value === "string" && value.length > 0) return value;
430
+ this.diag.error("expected_string", path, `expected a non-empty string, got ${describe(value)}`);
431
+ return null;
432
+ }
433
+ optionalString(value, path) {
434
+ return value === void 0 ? void 0 : this.string(value, path) ?? void 0;
435
+ }
436
+ boolean(value, path) {
437
+ if (value === void 0 || typeof value === "boolean") return value;
438
+ this.diag.error("expected_boolean", path, `expected true or false, got ${describe(value)}`);
439
+ return void 0;
440
+ }
441
+ number(value, path) {
442
+ if (value === void 0 || typeof value === "number" && Number.isFinite(value)) return value;
443
+ this.diag.error("expected_number", path, `expected a number, got ${describe(value)}`);
444
+ return void 0;
445
+ }
446
+ strings(value, path) {
447
+ if (value === void 0) return void 0;
448
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) return value;
449
+ this.diag.error("expected_string_list", path, `expected a list of strings, got ${describe(value)}`);
450
+ return void 0;
451
+ }
452
+ verdict(value, path) {
453
+ if (typeof value === "string" && VERDICTS.includes(value)) return value;
454
+ this.diag.error("expected_verdict", path, `expected allow, hold or block, got ${describe(value)}`);
455
+ return null;
456
+ }
457
+ optionalVerdict(value, path) {
458
+ return value === void 0 ? void 0 : this.verdict(value, path) ?? void 0;
459
+ }
460
+ /** `never` or ISO-8601; absent materializes to `never` (VC-7c, FR-22). */
461
+ expiresAt(value, path) {
462
+ if (value === void 0) return NEVER;
463
+ if (value instanceof Date) return value.toISOString();
464
+ if (value === NEVER) return NEVER;
465
+ if (typeof value === "string" && !Number.isNaN(Date.parse(value))) return new Date(value).toISOString();
466
+ this.diag.error("expected_expiry", path, `expected "never" or an ISO-8601 date-time, got ${describe(value)}`);
467
+ return NEVER;
468
+ }
469
+ };
470
+ function isPlainObject2(value) {
471
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
472
+ }
473
+ function describe(value) {
474
+ if (value === null) return "null";
475
+ if (Array.isArray(value)) return "a list";
476
+ if (value instanceof Date) return "a date";
477
+ return typeof value === "object" ? "a mapping" : JSON.stringify(value);
478
+ }
479
+
480
+ // src/document/parse.ts
481
+ function parseYaml(text) {
482
+ const diag = new Diagnostics();
483
+ let value;
484
+ try {
485
+ value = parseYamlText(text);
486
+ } catch (error) {
487
+ diag.error("yaml_syntax", "", error.message);
488
+ return { source: null, diagnostics: diag };
489
+ }
490
+ return parseValue(value, diag);
491
+ }
492
+ function parseDocument(value) {
493
+ return parseValue(value, new Diagnostics());
494
+ }
495
+ var ROOT_KEYS = ["apiVersion", "kind", "metadata", "target", "precedence", "spec"];
496
+ var SPEC_KEYS = ["tools", "files", "network", "skills", "secrets", "members", "products", "capture", "approvalDefaults", "expiresAt"];
497
+ var VARIANT_KEYS = ["action", "when", "approval", "reason", "controls", "expiresAt"];
498
+ function parseValue(value, diag) {
499
+ const r = new Reader(diag);
500
+ const root = r.object(value, "", ROOT_KEYS);
501
+ if (!root) return { source: null, diagnostics: diag };
502
+ if (root.apiVersion !== "armor.io/v2") diag.error("unsupported_api_version", "/apiVersion", `expected armor.io/v2`);
503
+ if (root.kind !== "Policy") diag.error("unsupported_kind", "/kind", `expected Policy`);
504
+ const metadata = parseMetadata(r, root.metadata);
505
+ const target = parseTarget(r, diag, root.target);
506
+ const precedence = parsePrecedence(diag, root.precedence);
507
+ const spec = parseSpec(r, diag, root.spec);
508
+ if (diag.hasErrors || !metadata || !target || !spec) return { source: null, diagnostics: diag };
509
+ const source = { apiVersion: "armor.io/v2", kind: "Policy", metadata, target, spec };
510
+ if (precedence) source.precedence = precedence;
511
+ return { source, diagnostics: diag };
512
+ }
513
+ function parseMetadata(r, value) {
514
+ const m = r.object(value, "/metadata", ["name", "description", "orgId", "labels"]);
515
+ if (!m) return null;
516
+ const name = r.string(m.name, "/metadata/name");
517
+ if (!name) return null;
518
+ const out = { name };
519
+ const description = r.optionalString(m.description, "/metadata/description");
520
+ const orgId = r.optionalString(m.orgId, "/metadata/orgId");
521
+ if (description) out.description = description;
522
+ if (orgId) out.orgId = orgId;
523
+ if (m.labels !== void 0) out.labels = parseLabels(r, m.labels);
524
+ return out;
525
+ }
526
+ function parseLabels(r, value) {
527
+ const labels = r.map(value, "/metadata/labels") ?? {};
528
+ const out = {};
529
+ for (const [k, v] of Object.entries(labels)) {
530
+ const s = r.string(v, `/metadata/labels/${k}`);
531
+ if (s) out[k] = s;
532
+ }
533
+ return out;
534
+ }
535
+ var TARGET_FORMS = ["agent", "group", "org", "device"];
536
+ function parseTarget(r, diag, value) {
537
+ const t = r.object(value, "/target", ["agent", "mcpServers", "group", "org", "device"]);
538
+ if (!t) return null;
539
+ const forms = TARGET_FORMS.filter((f) => t[f] !== void 0);
540
+ if (forms.length !== 1) {
541
+ diag.error("target_form", "/target", `exactly one of agent, group, org or device is required; got ${forms.length}`);
542
+ return null;
543
+ }
544
+ return targetOf(r, diag, t, forms[0]);
545
+ }
546
+ function targetOf(r, diag, t, form) {
547
+ if (form === "org" || form === "device") return fixedTarget(diag, t, form);
548
+ if (t.mcpServers !== void 0 && form !== "agent") diag.error("unknown_key", "/target/mcpServers", "mcpServers belongs to an agent target");
549
+ const id = r.string(t[form], `/target/${form}`);
550
+ if (!id) return null;
551
+ if (form === "group") return { group: id };
552
+ const servers = r.strings(t.mcpServers, "/target/mcpServers");
553
+ return servers ? { agent: id, mcpServers: servers } : { agent: id };
554
+ }
555
+ var FIXED_TARGETS = {
556
+ org: { value: "*", code: "target_org", message: 'the organization floor is written as org: "*"' },
557
+ device: { value: "local", code: "target_device", message: "the only device target is local" }
558
+ };
559
+ function fixedTarget(diag, t, form) {
560
+ const expected = FIXED_TARGETS[form];
561
+ if (t[form] === expected.value) return form === "org" ? { org: "*" } : { device: "local" };
562
+ diag.error(expected.code, `/target/${form}`, expected.message);
563
+ return null;
564
+ }
565
+ function parsePrecedence(diag, value) {
566
+ if (value === void 0 || value === "device-first" || value === "agent-first") return value;
567
+ diag.error("expected_precedence", "/precedence", "expected device-first or agent-first");
568
+ return void 0;
569
+ }
570
+ function parseSpec(r, diag, value) {
571
+ const s = r.object(value, "/spec", SPEC_KEYS);
572
+ if (!s) return null;
573
+ const tools = parseTools(r, diag, s.tools, "/spec/tools");
574
+ if (!tools) return null;
575
+ const spec = { tools, expiresAt: r.expiresAt(s.expiresAt, "/spec/expiresAt") };
576
+ assign(spec, "files", parseFiles(r, s.files));
577
+ assign(spec, "network", parseNetwork(r, s.network));
578
+ assign(spec, "skills", parseSkills(r, s.skills));
579
+ assign(spec, "secrets", parseSecrets(r, s.secrets));
580
+ assign(spec, "members", parseOverlays(r, diag, s.members, "/spec/members"));
581
+ assign(spec, "products", parseOverlays(r, diag, s.products, "/spec/products"));
582
+ assign(spec, "capture", parseCapture(r, s.capture));
583
+ assign(spec, "approvalDefaults", parseApproval(r, s.approvalDefaults, "/spec/approvalDefaults"));
584
+ return spec;
585
+ }
586
+ function assign(spec, key, value) {
587
+ if (value !== void 0) spec[key] = value;
588
+ }
589
+ function parseTools(r, diag, value, path) {
590
+ const t = r.object(value, path, ["default", "rules"]);
591
+ if (!t) return null;
592
+ const dflt = r.verdict(t.default, `${path}/default`);
593
+ if (!dflt) return null;
594
+ const rules = parseRules(r, diag, t.rules, `${path}/rules`);
595
+ return rules ? { default: dflt, rules } : { default: dflt };
596
+ }
597
+ function parseRules(r, diag, value, path) {
598
+ if (value === void 0) return void 0;
599
+ const map = r.map(value, path);
600
+ if (!map) return void 0;
601
+ const out = {};
602
+ for (const [key, rule] of Object.entries(map)) {
603
+ const parsed = parseRule(r, diag, rule, `${path}/${key}`);
604
+ if (parsed) out[key] = parsed;
605
+ }
606
+ return out;
607
+ }
608
+ function parseRule(r, diag, value, path) {
609
+ if (typeof value === "string") {
610
+ const v = r.verdict(value, path);
611
+ return v ? [{ action: v, expiresAt: "never" }] : null;
612
+ }
613
+ if (!Array.isArray(value)) {
614
+ diag.error("expected_rule", path, "expected a verdict or a list of variants");
615
+ return null;
616
+ }
617
+ const variants = value.map((v, i) => parseVariant(r, diag, v, `${path}/${i}`));
618
+ return variants.every((v) => v !== null) ? variants : null;
619
+ }
620
+ function parseVariant(r, diag, value, path) {
621
+ const v = r.object(value, path, VARIANT_KEYS);
622
+ if (!v) return null;
623
+ const action = r.verdict(v.action, `${path}/action`);
624
+ if (!action) return null;
625
+ const variant = { action, expiresAt: r.expiresAt(v.expiresAt, `${path}/expiresAt`) };
626
+ const when = parseWhen(r, diag, v.when, `${path}/when`);
627
+ const approval = parseApproval(r, v.approval, `${path}/approval`);
628
+ const reason = r.optionalString(v.reason, `${path}/reason`);
629
+ const controls = r.strings(v.controls, `${path}/controls`);
630
+ if (when) variant.when = when;
631
+ if (approval) variant.approval = approval;
632
+ if (reason) variant.reason = reason;
633
+ if (controls) variant.controls = controls;
634
+ return variant;
635
+ }
636
+ function parseWhen(r, diag, value, path) {
637
+ if (value === void 0) return void 0;
638
+ const map = r.map(value, path);
639
+ if (!map) return void 0;
640
+ const out = {};
641
+ for (const [field, spec] of Object.entries(map)) {
642
+ const cond = parseCondition(r, diag, spec, `${path}/${field}`);
643
+ if (cond) out[field] = cond;
644
+ }
645
+ return out;
646
+ }
647
+ function parseCondition(r, diag, value, path) {
648
+ const c = r.object(value, path, SOURCE_OPERATORS);
649
+ if (!c) return null;
650
+ const ops = Object.keys(c);
651
+ if (ops.length !== 1) {
652
+ diag.error("condition_operator", path, `exactly one operator is required; got ${ops.length}`);
653
+ return null;
654
+ }
655
+ return c;
656
+ }
657
+ function parseApproval(r, value, path) {
658
+ if (value === void 0) return void 0;
659
+ const a = r.object(value, path, ["approvers", "roles", "members", "anyMember"]);
660
+ if (!a) return void 0;
661
+ const approvers = r.number(a.approvers, `${path}/approvers`);
662
+ const out = { approvers: approvers ?? 1 };
663
+ const roles = r.strings(a.roles, `${path}/roles`);
664
+ const members = r.strings(a.members, `${path}/members`);
665
+ const anyMember = r.boolean(a.anyMember, `${path}/anyMember`);
666
+ if (roles) out.roles = roles;
667
+ if (members) out.members = members;
668
+ if (anyMember !== void 0) out.anyMember = anyMember;
669
+ return out;
670
+ }
671
+ function parseFiles(r, value) {
672
+ if (value === void 0) return void 0;
673
+ const f = r.object(value, "/spec/files", ["deny", "allowOutsideWorkspace"]);
674
+ if (!f) return void 0;
675
+ const out = {};
676
+ const deny = r.strings(f.deny, "/spec/files/deny");
677
+ const outside = r.boolean(f.allowOutsideWorkspace, "/spec/files/allowOutsideWorkspace");
678
+ if (deny) out.deny = deny;
679
+ if (outside !== void 0) out.allowOutsideWorkspace = outside;
680
+ return out;
681
+ }
682
+ function parseNetwork(r, value) {
683
+ if (value === void 0) return void 0;
684
+ const n = r.object(value, "/spec/network", ["default", "allow", "deny"]);
685
+ if (!n) return void 0;
686
+ const out = {};
687
+ const dflt = r.optionalVerdict(n.default, "/spec/network/default");
688
+ const allow = parseNetworkEntries(r, n.allow, "/spec/network/allow");
689
+ const deny = parseNetworkEntries(r, n.deny, "/spec/network/deny");
690
+ if (dflt) out.default = dflt;
691
+ if (allow) out.allow = allow;
692
+ if (deny) out.deny = deny;
693
+ return out;
694
+ }
695
+ function parseNetworkEntries(r, value, path) {
696
+ if (value === void 0) return void 0;
697
+ if (!Array.isArray(value)) {
698
+ r.strings(value, path);
699
+ return void 0;
700
+ }
701
+ const out = [];
702
+ value.forEach((entry, i) => {
703
+ if (typeof entry === "string") return void out.push(entry);
704
+ const e = r.object(entry, `${path}/${i}`, ["host", "reason"]);
705
+ const host = e ? r.string(e.host, `${path}/${i}/host`) : null;
706
+ if (e && host) out.push(e.reason === void 0 ? { host } : { host, reason: String(e.reason) });
707
+ });
708
+ return out;
709
+ }
710
+ function parseSkills(r, value) {
711
+ if (value === void 0) return void 0;
712
+ const s = r.object(value, "/spec/skills", ["default", "hold", "block"]);
713
+ if (!s) return void 0;
714
+ const out = {};
715
+ const dflt = r.optionalVerdict(s.default, "/spec/skills/default");
716
+ const hold = r.strings(s.hold, "/spec/skills/hold");
717
+ const block = r.strings(s.block, "/spec/skills/block");
718
+ if (dflt) out.default = dflt;
719
+ if (hold) out.hold = hold;
720
+ if (block) out.block = block;
721
+ return out;
722
+ }
723
+ function parseSecrets(r, value) {
724
+ if (value === void 0) return void 0;
725
+ const s = r.object(value, "/spec/secrets", ["envBlocklist"]);
726
+ if (!s) return void 0;
727
+ const envBlocklist = r.strings(s.envBlocklist, "/spec/secrets/envBlocklist");
728
+ return envBlocklist ? { envBlocklist } : {};
729
+ }
730
+ function parseOverlays(r, diag, value, path) {
731
+ if (value === void 0) return void 0;
732
+ const map = r.map(value, path);
733
+ if (!map) return void 0;
734
+ const out = {};
735
+ for (const [key, overlay] of Object.entries(map)) {
736
+ const parsed = parseOverlay(r, diag, overlay, `${path}/${key}`);
737
+ if (parsed) out[key] = parsed;
738
+ }
739
+ return out;
740
+ }
741
+ function parseOverlay(r, diag, value, path) {
742
+ const o = r.object(value, path, ["tools", "expiresAt"]);
743
+ if (!o) return null;
744
+ const overlay = { expiresAt: r.expiresAt(o.expiresAt, `${path}/expiresAt`) };
745
+ if (o.tools === void 0) return overlay;
746
+ const t = r.object(o.tools, `${path}/tools`, ["default", "rules"]);
747
+ if (!t) return overlay;
748
+ const tools = {};
749
+ const dflt = r.optionalVerdict(t.default, `${path}/tools/default`);
750
+ const rules = parseRules(r, diag, t.rules, `${path}/tools/rules`);
751
+ if (dflt) tools.default = dflt;
752
+ if (rules) tools.rules = rules;
753
+ overlay.tools = tools;
754
+ return overlay;
755
+ }
756
+ function parseCapture(r, value) {
757
+ if (value === void 0) return void 0;
758
+ const c = r.object(value, "/spec/capture", ["arguments", "redact", "maxBytesPerCall", "sample"]);
759
+ if (!c) return void 0;
760
+ const out = {};
761
+ const args = r.boolean(c.arguments, "/spec/capture/arguments");
762
+ const redact = r.strings(c.redact, "/spec/capture/redact");
763
+ const max = r.number(c.maxBytesPerCall, "/spec/capture/maxBytesPerCall");
764
+ const sample = r.number(c.sample, "/spec/capture/sample");
765
+ if (args !== void 0) out.arguments = args;
766
+ if (redact) out.redact = redact;
767
+ if (max !== void 0) out.maxBytesPerCall = max;
768
+ if (sample !== void 0) out.sample = sample;
769
+ return out;
770
+ }
771
+
772
+ // src/document/validate.ts
773
+ function validate(source) {
774
+ const diag = new Diagnostics();
775
+ const ctx = { diag, approvalDefaults: source.spec.approvalDefaults, base: source.spec.tools };
776
+ validateTargetSections(ctx, source);
777
+ if (source.spec.approvalDefaults) validateApproval(ctx, source.spec.approvalDefaults, "/spec/approvalDefaults", true);
778
+ validateRules(ctx, source.spec.tools.rules, "/spec/tools/rules");
779
+ validateOverlays(ctx, source.spec.members, "/spec/members");
780
+ validateOverlays(ctx, source.spec.products, "/spec/products");
781
+ validateCapture(ctx, source);
782
+ return diag;
783
+ }
784
+ function validateTargetSections(ctx, source) {
785
+ const local = "device" in source.target;
786
+ if (!local && source.spec.products) ctx.diag.error("local_only_section", "/spec/products", "products applies only under target device: local");
787
+ if (!local && source.precedence) ctx.diag.error("local_only_section", "/precedence", "precedence applies only under target device: local");
788
+ if (local && source.spec.members) ctx.diag.error("enterprise_only_section", "/spec/members", "members applies only to an enterprise target");
789
+ if (local && source.metadata.orgId) ctx.diag.error("enterprise_only_field", "/metadata/orgId", "a local policy carries no orgId");
790
+ }
791
+ function validateRules(ctx, rules, path) {
792
+ for (const [key, variants] of Object.entries(rules ?? {})) {
793
+ validateVariants(ctx, variants, `${path}/${key}`);
794
+ }
795
+ }
796
+ function validateVariants(ctx, variants, path) {
797
+ variants.forEach((variant, i) => {
798
+ const vpath = `${path}/${i}`;
799
+ if (i > 0 && isUnconditional(variants[i - 1])) {
800
+ ctx.diag.error("unreachable_variant", vpath, `variant ${i - 1} has no when, so this variant can never be reached`);
801
+ }
802
+ validateVariant(ctx, variant, vpath);
803
+ });
804
+ }
805
+ function isUnconditional(variant) {
806
+ return !variant.when || Object.keys(variant.when).length === 0;
807
+ }
808
+ function validateVariant(ctx, variant, path) {
809
+ if (variant.action === "hold") validateHoldApprover(ctx, variant, path);
810
+ for (const [field, spec] of Object.entries(variant.when ?? {})) validateCondition(ctx, spec, `${path}/when/${field}`);
811
+ }
812
+ function validateHoldApprover(ctx, variant, path) {
813
+ if (variant.approval) return void validateApproval(ctx, variant.approval, `${path}/approval`, true);
814
+ if (!ctx.approvalDefaults) {
815
+ ctx.diag.error("hold_without_approver", path, "a hold names an approval or the document sets approvalDefaults");
816
+ }
817
+ }
818
+ function validateApproval(ctx, approval, path, requireResolvable) {
819
+ if (!Number.isInteger(approval.approvers) || approval.approvers < 1) {
820
+ ctx.diag.error("approvers_count", `${path}/approvers`, "approvers is a whole number of at least 1");
821
+ }
822
+ if (requireResolvable && !namesAnApprover(approval)) {
823
+ ctx.diag.error("approval_unresolvable", path, "an approval names roles, members or anyMember: true");
824
+ }
825
+ }
826
+ function namesAnApprover(approval) {
827
+ return (approval.roles?.length ?? 0) > 0 || (approval.members?.length ?? 0) > 0 || approval.anyMember === true;
828
+ }
829
+ var OPERANDS = {
830
+ eq: [() => true, "any value"],
831
+ neq: [() => true, "any value"],
832
+ in: [Array.isArray, "a list"],
833
+ notIn: [Array.isArray, "a list"],
834
+ gt: [isNumber, "a number"],
835
+ gte: [isNumber, "a number"],
836
+ lt: [isNumber, "a number"],
837
+ lte: [isNumber, "a number"],
838
+ matches: [isRe2Pattern, "an RE2-compatible regular expression"],
839
+ notMatches: [isRe2Pattern, "an RE2-compatible regular expression"],
840
+ startsWith: [isString, "a string"],
841
+ glob: [isString, "a glob"],
842
+ withinWorkspace: [isBoolean, "true or false"],
843
+ exists: [isBoolean, "true or false"]
844
+ };
845
+ function validateCondition(ctx, spec, path) {
846
+ const [op, value] = Object.entries(spec)[0];
847
+ const [check, expected] = OPERANDS[op];
848
+ if (!check(value)) ctx.diag.error("condition_operand", `${path}/${op}`, `${op} takes ${expected}`);
849
+ }
850
+ function isNumber(v) {
851
+ return typeof v === "number" && Number.isFinite(v);
852
+ }
853
+ function isString(v) {
854
+ return typeof v === "string";
855
+ }
856
+ function isBoolean(v) {
857
+ return typeof v === "boolean";
858
+ }
859
+ function isRe2Pattern(v) {
860
+ if (typeof v !== "string") return false;
861
+ if (/\\[1-9]|\(\?[=!<]/.test(v)) return false;
862
+ try {
863
+ new RegExp(v);
864
+ return true;
865
+ } catch {
866
+ return false;
867
+ }
868
+ }
869
+ function validateOverlays(ctx, overlays2, path) {
870
+ for (const [key, overlay] of Object.entries(overlays2 ?? {})) {
871
+ const opath = `${path}/${key}`;
872
+ validateRules(ctx, overlay.tools?.rules, `${opath}/tools/rules`);
873
+ flagLoosening(ctx, overlay, opath);
874
+ }
875
+ }
876
+ function flagLoosening(ctx, overlay, path) {
877
+ const baseDefault = ctx.base.default;
878
+ if (overlay.tools?.default && looser(overlay.tools.default, baseDefault)) {
879
+ ctx.diag.warning("loosening_grant", `${path}/tools/default`, `default ${overlay.tools.default} is looser than the base default ${baseDefault}`);
880
+ }
881
+ for (const [key, variants] of Object.entries(overlay.tools?.rules ?? {})) {
882
+ const base = baseVerdictFor(ctx.base, key);
883
+ const loosest = variants.reduce((v, x) => looser(x.action, v) ? x.action : v, "block");
884
+ if (looser(loosest, base)) {
885
+ ctx.diag.warning("loosening_grant", `${path}/tools/rules/${key}`, `${key}: ${loosest} is looser than the base ${base}`);
886
+ }
887
+ }
888
+ }
889
+ function baseVerdictFor(base, key) {
890
+ const variants = base.rules?.[key];
891
+ if (!variants) return base.default;
892
+ return variants.reduce((v, x) => looser(v, x.action) ? x.action : v, "allow");
893
+ }
894
+ function looser(a, b) {
895
+ return VERDICT_RANK[a] < VERDICT_RANK[b];
896
+ }
897
+ function validateCapture(ctx, source) {
898
+ const c = source.spec.capture;
899
+ if (!c) return;
900
+ if (c.sample !== void 0 && (c.sample < 0 || c.sample > 1)) ctx.diag.error("capture_sample", "/spec/capture/sample", "sample is between 0 and 1");
901
+ if (c.maxBytesPerCall !== void 0 && (c.maxBytesPerCall < 0 || !Number.isInteger(c.maxBytesPerCall))) {
902
+ ctx.diag.error("capture_max_bytes", "/spec/capture/maxBytesPerCall", "maxBytesPerCall is a whole number of bytes");
903
+ }
904
+ }
905
+
906
+ // src/compile/port.ts
907
+ var CAPABILITY_KEYS = /* @__PURE__ */ new Set([
908
+ "shell",
909
+ "file.read",
910
+ "file.write",
911
+ "file.delete",
912
+ "web.fetch",
913
+ "web.search",
914
+ "skill",
915
+ "subagent",
916
+ "destructive",
917
+ "files",
918
+ "network",
919
+ "skills",
920
+ "secrets"
921
+ ]);
922
+
923
+ // src/compile/clamp.ts
924
+ function resolveKey(key, inventory, diag, path) {
925
+ if (isGlob(key)) return { kind: "glob", tool: null };
926
+ const matches = inventory.filter((t) => t.name === key || t.qualifiedName === key);
927
+ if (matches.length === 0 && isCapabilityKey(key, inventory)) return { kind: "capability", tool: null };
928
+ if (matches.length > 1 && !matches.every((t) => t.owner.id === matches[0].owner.id)) {
929
+ diag.error("ambiguous_tool_key", path, `"${key}" exists on ${matches.length} owners; use the qualified mcp__<server>__${key} form`);
930
+ return { kind: "exact", tool: null };
931
+ }
932
+ return { kind: "exact", tool: bindExact(key, matches[0] ?? null, diag, path) };
933
+ }
934
+ function bindExact(key, tool, diag, path) {
935
+ if (!tool) {
936
+ diag.warning("unknown_tool_key", path, `"${key}" is not in the target's inventory; the rule compiles and binds nothing until the tool appears`);
937
+ return null;
938
+ }
939
+ if (!tool.declared) {
940
+ diag.warning("observed_only_tool", path, `"${key}" has only been observed, never declared; the rule compiles without a schema binding`);
941
+ return null;
942
+ }
943
+ return tool;
944
+ }
945
+ function isCapabilityKey(key, inventory) {
946
+ return CAPABILITY_KEYS.has(key) || inventory.some((t) => t.capabilities?.includes(key));
947
+ }
948
+ function checkGlobCollisions(keys, inventory, diag, path) {
949
+ const globs = keys.filter(isGlob);
950
+ for (const tool of inventory) {
951
+ const matching = globs.filter((g) => matchGlob(g, tool.name) || tool.qualifiedName !== void 0 && matchGlob(g, tool.qualifiedName));
952
+ const collision = sameLengthPair(matching);
953
+ if (collision) diag.error("ambiguous_glob", path, `"${collision[0]}" and "${collision[1]}" both match ${tool.name} with the same specificity`);
954
+ }
955
+ }
956
+ function sameLengthPair(globs) {
957
+ const byLength = /* @__PURE__ */ new Map();
958
+ for (const g of globs) {
959
+ const other = byLength.get(g.length);
960
+ if (other !== void 0) return [other, g];
961
+ byLength.set(g.length, g);
962
+ }
963
+ return null;
964
+ }
965
+ function checkArgumentKeys(fields, tool, diag, path) {
966
+ const properties = schemaProperties(tool.inputSchema);
967
+ if (!properties) return;
968
+ for (const field of fields) {
969
+ const [root, first] = field.split(".");
970
+ if (root === "args" && first !== void 0 && !properties.has(first)) {
971
+ diag.warning("unknown_argument", `${path}/${field}`, `"${first}" is not a declared argument of ${tool.name}`);
972
+ }
973
+ }
974
+ }
975
+ function schemaProperties(schema) {
976
+ if (schema === null || typeof schema !== "object") return null;
977
+ const props = schema.properties;
978
+ if (props === null || typeof props !== "object") return null;
979
+ return new Set(Object.keys(props));
980
+ }
981
+
982
+ // src/compile/lower-floors.ts
983
+ function lowerFloors(spec, ctx) {
984
+ return [...lowerFiles(spec, ctx), ...lowerNetwork(spec, ctx), ...lowerSkills(spec, ctx), ...lowerSecrets(spec, ctx)];
985
+ }
986
+ function floor(capability, effect, conditions, order, ctx) {
987
+ return {
988
+ id: `st-${capability}-${effect}-${order}`,
989
+ subject: null,
990
+ match: { capability },
991
+ effect,
992
+ conditions,
993
+ expiresAt: "never",
994
+ provenance: { ruleKey: capability, variant: order, source: ctx.source, layer: ctx.layer }
995
+ };
996
+ }
997
+ function lowerFiles(spec, ctx) {
998
+ const out = (spec.files?.deny ?? []).map((glob, i) => floor("files", "block", [{ field: "path", op: "glob", value: glob }], i, ctx));
999
+ if (spec.files?.allowOutsideWorkspace === false) {
1000
+ out.push(floor("files", "block", [{ field: "withinWorkspace", op: "eq", value: false }], out.length, ctx));
1001
+ }
1002
+ return out;
1003
+ }
1004
+ function hostOf(entry) {
1005
+ return typeof entry === "string" ? entry : entry.host;
1006
+ }
1007
+ function lowerNetwork(spec, ctx) {
1008
+ const n = spec.network;
1009
+ if (!n) return [];
1010
+ const deny = (n.deny ?? []).map((e, i) => floor("network", "block", [{ field: "host", op: "glob", value: hostOf(e) }], i, ctx));
1011
+ const allow = (n.allow ?? []).map((e, i) => floor("network", "allow", [{ field: "host", op: "glob", value: hostOf(e) }], deny.length + i, ctx));
1012
+ return [...deny, ...allow];
1013
+ }
1014
+ function lowerSkills(spec, ctx) {
1015
+ const s = spec.skills;
1016
+ if (!s) return [];
1017
+ const hold = (s.hold ?? []).map((g, i) => floor("skills", "hold", [{ field: "skill", op: "glob", value: g }], i, ctx));
1018
+ const block = (s.block ?? []).map((g, i) => floor("skills", "block", [{ field: "skill", op: "glob", value: g }], hold.length + i, ctx));
1019
+ return [...hold, ...block];
1020
+ }
1021
+ function lowerSecrets(spec, ctx) {
1022
+ const names = spec.secrets?.envBlocklist ?? [];
1023
+ return names.length === 0 ? [] : [floor("secrets", "block", [{ field: "env", op: "in", value: names }], 0, ctx)];
1024
+ }
1025
+
1026
+ // src/compile/lower-conditions.ts
1027
+ var OP_MAP = {
1028
+ eq: "eq",
1029
+ neq: "ne",
1030
+ in: "in",
1031
+ notIn: "notIn",
1032
+ gt: "gt",
1033
+ gte: "gte",
1034
+ lt: "lt",
1035
+ lte: "lte",
1036
+ matches: "regex",
1037
+ notMatches: "notRegex",
1038
+ startsWith: "startsWith",
1039
+ glob: "glob",
1040
+ withinWorkspace: "eq",
1041
+ exists: "exists"
1042
+ };
1043
+ function lowerConditions(when, schemaHash2) {
1044
+ return Object.entries(when ?? {}).map(([field, spec]) => lowerCondition(field, spec, schemaHash2));
1045
+ }
1046
+ function lowerCondition(field, spec, schemaHash2) {
1047
+ const [op, operand] = Object.entries(spec)[0];
1048
+ const condition = {
1049
+ field: op === "withinWorkspace" ? "withinWorkspace" : field,
1050
+ op: OP_MAP[op],
1051
+ value: operand
1052
+ };
1053
+ if (schemaHash2 !== void 0 && field.startsWith("args.")) condition.schemaBinding = { schemaHash: schemaHash2 };
1054
+ return condition;
1055
+ }
1056
+
1057
+ // src/compile/controls.ts
1058
+ var SOC2 = [
1059
+ "CC1.1",
1060
+ "CC1.2",
1061
+ "CC1.3",
1062
+ "CC1.4",
1063
+ "CC1.5",
1064
+ "CC2.1",
1065
+ "CC2.2",
1066
+ "CC2.3",
1067
+ "CC3.1",
1068
+ "CC3.2",
1069
+ "CC3.3",
1070
+ "CC3.4",
1071
+ "CC4.1",
1072
+ "CC4.2",
1073
+ "CC5.1",
1074
+ "CC5.2",
1075
+ "CC5.3",
1076
+ "CC6.1",
1077
+ "CC6.2",
1078
+ "CC6.3",
1079
+ "CC6.4",
1080
+ "CC6.5",
1081
+ "CC6.6",
1082
+ "CC6.7",
1083
+ "CC6.8",
1084
+ "CC7.1",
1085
+ "CC7.2",
1086
+ "CC7.3",
1087
+ "CC7.4",
1088
+ "CC7.5",
1089
+ "CC8.1",
1090
+ "CC9.1",
1091
+ "CC9.2"
1092
+ ].map((c) => `SOC2-${c}`);
1093
+ var HIPAA = [
1094
+ "164.308(a)(1)",
1095
+ "164.308(a)(3)",
1096
+ "164.308(a)(4)",
1097
+ "164.308(a)(5)",
1098
+ "164.308(a)(6)",
1099
+ "164.310(a)(1)",
1100
+ "164.310(b)",
1101
+ "164.310(c)",
1102
+ "164.310(d)(1)",
1103
+ "164.312(a)(1)",
1104
+ "164.312(a)(2)(i)",
1105
+ "164.312(a)(2)(iv)",
1106
+ "164.312(b)",
1107
+ "164.312(c)(1)",
1108
+ "164.312(d)",
1109
+ "164.312(e)(1)"
1110
+ ].map((c) => `HIPAA-${c}`);
1111
+ var KNOWN_CONTROLS = /* @__PURE__ */ new Set([...SOC2, ...HIPAA]);
1112
+
1113
+ // src/compile/lower-rules.ts
1114
+ function lowerRules(rules, ctx, path, subject, scopeExpiry) {
1115
+ const out = [];
1116
+ for (const [key, value] of Object.entries(rules ?? {})) {
1117
+ out.push(...lowerRule(key, value, ctx, `${path}/${key}`, subject, scopeExpiry));
1118
+ }
1119
+ return out;
1120
+ }
1121
+ function lowerRule(key, variants, ctx, path, subject, scopeExpiry) {
1122
+ const resolved = resolveKey(key, ctx.inventory, ctx.diag, path);
1123
+ checkControls(variants, ctx.diag, path);
1124
+ const fields = variants.flatMap((v) => Object.keys(v.when ?? {}));
1125
+ if (resolved.tool) checkArgumentKeys(fields, resolved.tool, ctx.diag, path);
1126
+ const schemaHash2 = resolved.kind === "exact" ? resolved.tool?.schemaHash ?? null : void 0;
1127
+ return variants.map((variant, order) => ({
1128
+ id: statementId(key, order, subject),
1129
+ subject,
1130
+ match: matchFor(key, resolved.kind, order),
1131
+ effect: variant.action,
1132
+ conditions: lowerConditions(variant.when, schemaHash2),
1133
+ ...approvalFor(variant, ctx.approvalDefaults),
1134
+ expiresAt: earliest(variant.expiresAt, scopeExpiry),
1135
+ ...variant.controls ? { controls: variant.controls } : {},
1136
+ provenance: { ruleKey: key, variant: order, source: ctx.source, layer: subject ? "member" : ctx.layer }
1137
+ }));
1138
+ }
1139
+ function checkControls(variants, diag, path) {
1140
+ variants.forEach((v, i) => {
1141
+ for (const control of v.controls ?? []) {
1142
+ if (!KNOWN_CONTROLS.has(control)) diag.warning("unknown_control", `${path}/${i}/controls`, `unknown control identifier "${control}"`);
1143
+ }
1144
+ });
1145
+ }
1146
+ function matchFor(key, kind, order) {
1147
+ if (kind === "capability") return { capability: key };
1148
+ const keyTier = kind;
1149
+ return { tool: key, keyTier, order };
1150
+ }
1151
+ function approvalFor(variant, defaults) {
1152
+ if (variant.action !== "hold") return {};
1153
+ const approval = variant.approval ?? defaults;
1154
+ return approval ? { approval } : {};
1155
+ }
1156
+ function earliest(a, b) {
1157
+ if (a === "never") return b;
1158
+ if (b === "never") return a;
1159
+ return Date.parse(a) <= Date.parse(b) ? a : b;
1160
+ }
1161
+ function statementId(key, order, subject) {
1162
+ const slug = key.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
1163
+ const who = subject ? "-" + subjectSlug(subject) : "";
1164
+ return `st-${slug}${who}-${order}`;
1165
+ }
1166
+ function subjectSlug(subject) {
1167
+ const value = Object.values(subject)[0];
1168
+ return value.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase();
1169
+ }
1170
+ function subjectFor(key, kind) {
1171
+ if (kind === "products") return { product: key };
1172
+ if (key.includes("@")) return { email: key.toLowerCase() };
1173
+ if (key.startsWith("team:")) return { group: key.slice("team:".length) };
1174
+ return { membershipId: key };
1175
+ }
1176
+ function lowerOverlays(overlays2, kind, ctx, path) {
1177
+ const out = [];
1178
+ for (const [key, overlay] of Object.entries(overlays2 ?? {})) {
1179
+ const subject = subjectFor(key, kind);
1180
+ const opath = `${path}/${key}/tools`;
1181
+ out.push(...lowerRules(overlay.tools?.rules, ctx, `${opath}/rules`, subject, overlay.expiresAt));
1182
+ if (overlay.tools?.default) out.push(overlayDefault(overlay.tools.default, subject, overlay.expiresAt, ctx));
1183
+ }
1184
+ return out;
1185
+ }
1186
+ function overlayDefault(effect, subject, expiresAt, ctx) {
1187
+ return {
1188
+ id: statementId("*", 0, subject),
1189
+ subject,
1190
+ match: { tool: "*", keyTier: "glob", order: 0 },
1191
+ effect,
1192
+ conditions: [],
1193
+ ...effect === "hold" && ctx.approvalDefaults ? { approval: ctx.approvalDefaults } : {},
1194
+ expiresAt,
1195
+ provenance: { ruleKey: "tools.default", variant: 0, source: ctx.source, layer: "member" }
1196
+ };
1197
+ }
1198
+
1199
+ // src/compile/report.ts
1200
+ var ENTERPRISE = {
1201
+ tools: "enforced",
1202
+ files: "argument-only",
1203
+ network: "argument-only",
1204
+ skills: "inert",
1205
+ secrets: "inert"
1206
+ };
1207
+ var LOCAL = {
1208
+ tools: "enforced",
1209
+ files: "enforced",
1210
+ network: "enforced",
1211
+ skills: "enforced",
1212
+ secrets: "enforced"
1213
+ };
1214
+ function enforceabilityReport(source, inventory) {
1215
+ const table = "device" in source.target ? LOCAL : ENTERPRISE;
1216
+ const sections = { tools: table.tools };
1217
+ for (const section of ["files", "network", "skills", "secrets"]) {
1218
+ if (source.spec[section]) sections[section] = table[section];
1219
+ }
1220
+ return { sections, notCoveredByCapabilityRules: uncovered(source, inventory) };
1221
+ }
1222
+ function uncovered(source, inventory) {
1223
+ const capabilityKeys = Object.keys(source.spec.tools.rules ?? {}).filter((k) => CAPABILITY_KEYS.has(k) || inventory.some((t) => t.capabilities?.includes(k)));
1224
+ if (capabilityKeys.length === 0) return [];
1225
+ return inventory.filter((t) => !capabilityKeys.some((k) => t.capabilities?.includes(k))).map((t) => t.qualifiedName ?? t.name).sort();
1226
+ }
1227
+
1228
+ // src/compile/compile.ts
1229
+ function compile(source, inventory, options) {
1230
+ const diag = validate(source);
1231
+ const report = enforceabilityReport(source, inventory);
1232
+ if (diag.hasErrors) return { document: null, diagnostics: diag, report };
1233
+ const layer = layerOfTarget(targetOf2(source.target));
1234
+ const ctx = { inventory, diag, layer, approvalDefaults: source.spec.approvalDefaults, source: options.source ?? "yaml" };
1235
+ checkGlobCollisions(Object.keys(source.spec.tools.rules ?? {}), inventory, diag, "/spec/tools/rules");
1236
+ checkDeviceFirst(source, diag);
1237
+ const statements = [
1238
+ ...lowerRules(source.spec.tools.rules, ctx, "/spec/tools/rules", null, source.spec.expiresAt),
1239
+ ...lowerFloors(source.spec, ctx),
1240
+ ...lowerOverlays(source.spec.members, "members", ctx, "/spec/members"),
1241
+ ...lowerOverlays(source.spec.products, "products", ctx, "/spec/products")
1242
+ ];
1243
+ if (diag.hasErrors) return { document: null, diagnostics: diag, report };
1244
+ const document = {
1245
+ schemaVersion: SCHEMA_VERSION,
1246
+ policyId: options.policyId,
1247
+ target: targetOf2(source.target, source),
1248
+ layer,
1249
+ defaults: defaultsOf(source),
1250
+ capture: captureOf(source),
1251
+ expiresAt: source.spec.expiresAt,
1252
+ statements
1253
+ };
1254
+ document.policyHash = policyHash(document);
1255
+ return { document, diagnostics: diag, report };
1256
+ }
1257
+ async function compileWithPort(source, port, options) {
1258
+ return compile(source, await port.toolsForTarget(source.target), options);
1259
+ }
1260
+ function targetOf2(target, source) {
1261
+ if ("device" in target) {
1262
+ const products = Object.keys(source?.spec.products ?? {});
1263
+ return products.length ? { device: "local", products } : { device: "local" };
1264
+ }
1265
+ return target;
1266
+ }
1267
+ function defaultsOf(source) {
1268
+ return {
1269
+ tools: source.spec.tools.default,
1270
+ network: source.spec.network?.default ?? "block",
1271
+ files: "allow",
1272
+ skills: source.spec.skills?.default ?? "allow"
1273
+ };
1274
+ }
1275
+ function captureOf(source) {
1276
+ const c = source.spec.capture;
1277
+ return {
1278
+ arguments: c?.arguments ?? false,
1279
+ redact: c?.redact ?? ["*token*", "*secret*", "authorization"],
1280
+ maxBytesPerCall: c?.maxBytesPerCall ?? 16384,
1281
+ sample: c?.sample ?? 1
1282
+ };
1283
+ }
1284
+ function checkDeviceFirst(source, diag) {
1285
+ if (!("device" in source.target) || source.precedence === "agent-first") return;
1286
+ for (const [product, overlay] of Object.entries(source.spec.products ?? {})) {
1287
+ for (const [key, variants] of Object.entries(overlay.tools?.rules ?? {})) {
1288
+ if (loosensBase(variants, baseRankFor(source, key))) {
1289
+ diag.error("device_first_loosening", `/spec/products/${product}/tools/rules/${key}`, `under device-first a product overlay may only tighten the base; set precedence: agent-first to allow this`);
1290
+ }
1291
+ }
1292
+ }
1293
+ }
1294
+ function baseRankFor(source, key) {
1295
+ const base = source.spec.tools.rules?.[key];
1296
+ if (!Array.isArray(base)) return VERDICT_RANK[source.spec.tools.default];
1297
+ return Math.min(...base.map((v) => VERDICT_RANK[v.action]));
1298
+ }
1299
+ function loosensBase(variants, baseRank) {
1300
+ return variants.some((v) => VERDICT_RANK[v.action] < baseRank);
1301
+ }
1302
+
1303
+ // src/compile/serialize.ts
1304
+ import { stringify } from "yaml";
1305
+ function serialize(source) {
1306
+ const spec = { ...source.spec, tools: { ...source.spec.tools, rules: shorthand(source.spec.tools.rules) } };
1307
+ if (spec.members) spec.members = overlays(spec.members);
1308
+ if (spec.products) spec.products = overlays(spec.products);
1309
+ const doc = {
1310
+ apiVersion: source.apiVersion,
1311
+ kind: source.kind,
1312
+ metadata: source.metadata,
1313
+ target: source.target
1314
+ };
1315
+ if (source.precedence) doc.precedence = source.precedence;
1316
+ doc.spec = spec;
1317
+ return stringify(doc, { lineWidth: 0, defaultStringType: "PLAIN", defaultKeyType: "PLAIN" });
1318
+ }
1319
+ function shorthand(rules) {
1320
+ if (!rules) return void 0;
1321
+ const out = {};
1322
+ for (const [key, value] of Object.entries(rules)) out[key] = scalarIfPlain(value);
1323
+ return out;
1324
+ }
1325
+ function scalarIfPlain(variants) {
1326
+ const only = variants.length === 1 ? variants[0] : null;
1327
+ const plain = only && Object.keys(only).length === 2 && only.expiresAt === "never";
1328
+ return plain ? only.action : variants;
1329
+ }
1330
+ function overlays(map) {
1331
+ const out = {};
1332
+ for (const [key, overlay] of Object.entries(map)) {
1333
+ out[key] = overlay.tools?.rules ? { ...overlay, tools: { ...overlay.tools, rules: shorthand(overlay.tools.rules) } } : overlay;
1334
+ }
1335
+ return out;
1336
+ }
1337
+ export {
1338
+ CAPABILITY_KEYS,
1339
+ Diagnostics,
1340
+ HASH_PREFIX,
1341
+ KNOWN_CONTROLS,
1342
+ NEVER,
1343
+ SCHEMA_VERSION,
1344
+ SOURCE_OPERATORS,
1345
+ VERDICTS,
1346
+ VERDICT_RANK,
1347
+ canonicalJson,
1348
+ checkArgumentKeys,
1349
+ checkGlobCollisions,
1350
+ compile,
1351
+ compileWithPort,
1352
+ conditionHolds,
1353
+ conditionsHold,
1354
+ enforceabilityReport,
1355
+ evaluate,
1356
+ globToRegExp,
1357
+ isGlob,
1358
+ layerOfTarget,
1359
+ matchGlob,
1360
+ parseDocument,
1361
+ parseYaml,
1362
+ policyHash,
1363
+ resolveField,
1364
+ resolveKey,
1365
+ schemaHash,
1366
+ selectRuleKey,
1367
+ serialize,
1368
+ sha256Canonical,
1369
+ stricter,
1370
+ validate,
1371
+ verifyPolicyHash
1372
+ };
1373
+ //# sourceMappingURL=index.js.map