@armoriq/policy-ir 0.1.4 → 0.1.6

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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PolicyDocument, V as Verdict, A as Approval, L as Layer, C as Condition, S as Statement, a as SourceTarget, b as PolicySource, D as Diagnostics } from './document-entry-CDOQVeis.cjs';
2
- export { c as Capture, d as ConditionOp, e as ConditionSpec, f as Defaults, g as Diagnostic, K as KeyTier, N as NEVER, h as NetworkEntry, O as Overlay, i as ParseResult, j as PolicySpec, k as PolicyTarget, l as Provenance, R as RuleValue, m as RuleVariant, n as SCHEMA_VERSION, o as SOURCE_OPERATORS, p as SchemaBinding, q as Severity, r as SourceOperator, s as StatementMatch, t as Subject, T as ToolsSection, u as VERDICTS, v as layerOfTarget, w as parseDocument, x as parseYaml, y as serialize, z as validate } from './document-entry-CDOQVeis.cjs';
1
+ import { P as PolicyDocument, V as Verdict, A as Approval, L as Layer, C as Condition, S as Statement, D as Diagnostics, E as EnforceabilityReport, a as PolicySource, I as InventoryTool, b as InventoryPort } from './document-entry-iS3HikJ9.cjs';
2
+ export { c as CAPABILITY_KEYS, d as COMPOSE_INTENT_MAX, e as Capture, f as ComposeAnswer, g as ComposeChangedCall, h as ComposeExample, i as ComposeHeldServer, j as ComposePreview, k as ComposeQuestion, l as ComposeReplay, m as ComposeRequest, n as ComposeResponse, o as ComposeTarget, p as ComposeUnsupported, q as ConditionOp, r as ConditionSpec, s as Defaults, t as Diagnostic, u as Enforceability, K as KeyTier, N as NEVER, v as NetworkEntry, O as Overlay, w as ParseResult, x as PolicySpec, y as PolicyTarget, z as Provenance, R as RuleValue, B as RuleVariant, F as SCHEMA_VERSION, G as SOURCE_OPERATORS, H as SchemaBinding, J as Section, M as Severity, Q as SourceOperator, T as SourceTarget, U as StatementMatch, W as Subject, X as ToolsSection, Y as VERDICTS, Z as enforceabilityReport, _ as layerOfTarget, $ as parseDocument, a0 as parseYaml, a1 as serialize, a2 as validate } from './document-entry-iS3HikJ9.cjs';
3
3
 
4
4
  /**
5
5
  * Canonical JSON: sorted keys, no whitespace, ASCII escapes for anything
@@ -43,6 +43,11 @@ interface Principal {
43
43
  /** One call, as the enforce path and the local hook both see it. */
44
44
  interface CallInput {
45
45
  tool: string;
46
+ /**
47
+ * The inventory's `mcp__<server>__<tool>` name for the tool the host matched,
48
+ * so a rule keyed by the qualified name applies to a bare-name call.
49
+ */
50
+ qualifiedName?: string;
46
51
  arguments: Record<string, unknown>;
47
52
  /**
48
53
  * Derived fields a host may supply: `path`, `program`, `command`,
@@ -98,7 +103,7 @@ interface BoundDocuments {
98
103
  * Four stages over four layers (PRD §7.4):
99
104
  * 1. floor: a floor block ends evaluation; otherwise its verdict is retained
100
105
  * 2. resolve: the member scope inside the most specific document
101
- * 3. candidate: member rule, then agent, then group, then the most specific tools.default
106
+ * 3. candidate: member rule, then agent, then group, then the stricter of the bound tools.default values
102
107
  * 4. merge: the stricter of floor and candidate
103
108
  */
104
109
  declare function evaluate(input: CallInput, bound: BoundDocuments): Evaluation;
@@ -122,47 +127,12 @@ declare function conditionsHold(input: CallInput, conditions: Condition[]): bool
122
127
  declare function conditionHolds(input: CallInput, condition: Condition): boolean;
123
128
 
124
129
  /**
125
- * Key selection: an exact key beats any glob, a longer glob beats a shorter
126
- * one. Two same-length globs matching one tool are rejected at compile time;
127
- * here the first in document order wins so evaluation stays deterministic.
128
- */
129
- declare function selectRuleKey(statements: Statement[], tool: string): string | null;
130
-
131
- /** One tool the target can reach, as the inventory knows it. */
132
- interface InventoryTool {
133
- /** The wire name the model sees; for an MCP tool the host may also supply the qualified `mcp__<server>__<tool>` form as `qualifiedName`. */
134
- name: string;
135
- qualifiedName?: string;
136
- owner: {
137
- type: 'mcp_server' | 'agent' | 'local';
138
- id: string;
139
- name?: string;
140
- };
141
- schemaHash: string | null;
142
- inputSchema?: unknown;
143
- capabilities?: string[];
144
- /** False for a row the observed feed created and no owner has declared: the compiler warns instead of binding. */
145
- declared: boolean;
146
- }
147
- /** What a host implements: conmap on `tool_snapshots`, the Armor CLI on the local tool list. */
148
- interface InventoryPort {
149
- toolsForTarget(target: SourceTarget): Promise<InventoryTool[]>;
150
- }
151
- declare const CAPABILITY_KEYS: ReadonlySet<string>;
152
-
153
- type Enforceability = 'enforced' | 'argument-only' | 'inert';
154
- type Section = 'tools' | 'files' | 'network' | 'skills' | 'secrets';
155
- /**
156
- * Per-section honesty (FR-9, FR-19): a local device sees real paths and
157
- * hosts; the enterprise SDK path sees only path-shaped and URL-shaped
158
- * arguments; skills and secrets have no enterprise surface at all.
130
+ * Key selection: a key matches the call's bare name or its qualified name.
131
+ * An exact key beats any glob, a longer key beats a shorter one. Two
132
+ * same-length globs matching one tool are rejected at compile time; here the
133
+ * first in document order wins so evaluation stays deterministic.
159
134
  */
160
- interface EnforceabilityReport {
161
- sections: Partial<Record<Section, Enforceability>>;
162
- /** Tools in the inventory that no capability rule reaches, when the document uses capability keys (M-12). */
163
- notCoveredByCapabilityRules: string[];
164
- }
165
- declare function enforceabilityReport(source: PolicySource, inventory: InventoryTool[]): EnforceabilityReport;
135
+ declare function selectRuleKey(statements: Statement[], call: Pick<CallInput, 'tool' | 'qualifiedName'>): string | null;
166
136
 
167
137
  interface CompileOptions {
168
138
  policyId: string;
@@ -198,4 +168,4 @@ declare function checkArgumentKeys(fields: string[], tool: InventoryTool, diag:
198
168
 
199
169
  declare const KNOWN_CONTROLS: ReadonlySet<string>;
200
170
 
201
- export { Approval, type BoundDocuments, CAPABILITY_KEYS, type CallInput, type CompileOptions, type CompileResult, Condition, Diagnostics, type Enforceability, type EnforceabilityReport, type Evaluation, HASH_PREFIX, type InventoryPort, type InventoryTool, KNOWN_CONTROLS, Layer, type Obligation, PolicyDocument, PolicySource, type Principal, type SchemaDriftObligation, type Section, SourceTarget, Statement, VERDICT_RANK, Verdict, type VerdictProvenance, canonicalJson, checkArgumentKeys, checkGlobCollisions, compile, compileWithPort, conditionHolds, conditionsHold, enforceabilityReport, evaluate, globToRegExp, isGlob, matchGlob, policyHash, resolveField, resolveKey, schemaHash, selectRuleKey, sha256Canonical, stricter, verifyPolicyHash };
171
+ export { Approval, type BoundDocuments, type CallInput, type CompileOptions, type CompileResult, Condition, Diagnostics, EnforceabilityReport, type Evaluation, HASH_PREFIX, InventoryPort, InventoryTool, KNOWN_CONTROLS, Layer, type Obligation, PolicyDocument, PolicySource, type Principal, type SchemaDriftObligation, Statement, VERDICT_RANK, Verdict, type VerdictProvenance, canonicalJson, checkArgumentKeys, checkGlobCollisions, compile, compileWithPort, conditionHolds, conditionsHold, evaluate, globToRegExp, isGlob, matchGlob, policyHash, resolveField, resolveKey, schemaHash, selectRuleKey, sha256Canonical, stricter, verifyPolicyHash };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PolicyDocument, V as Verdict, A as Approval, L as Layer, C as Condition, S as Statement, a as SourceTarget, b as PolicySource, D as Diagnostics } from './document-entry-CDOQVeis.js';
2
- export { c as Capture, d as ConditionOp, e as ConditionSpec, f as Defaults, g as Diagnostic, K as KeyTier, N as NEVER, h as NetworkEntry, O as Overlay, i as ParseResult, j as PolicySpec, k as PolicyTarget, l as Provenance, R as RuleValue, m as RuleVariant, n as SCHEMA_VERSION, o as SOURCE_OPERATORS, p as SchemaBinding, q as Severity, r as SourceOperator, s as StatementMatch, t as Subject, T as ToolsSection, u as VERDICTS, v as layerOfTarget, w as parseDocument, x as parseYaml, y as serialize, z as validate } from './document-entry-CDOQVeis.js';
1
+ import { P as PolicyDocument, V as Verdict, A as Approval, L as Layer, C as Condition, S as Statement, D as Diagnostics, E as EnforceabilityReport, a as PolicySource, I as InventoryTool, b as InventoryPort } from './document-entry-iS3HikJ9.js';
2
+ export { c as CAPABILITY_KEYS, d as COMPOSE_INTENT_MAX, e as Capture, f as ComposeAnswer, g as ComposeChangedCall, h as ComposeExample, i as ComposeHeldServer, j as ComposePreview, k as ComposeQuestion, l as ComposeReplay, m as ComposeRequest, n as ComposeResponse, o as ComposeTarget, p as ComposeUnsupported, q as ConditionOp, r as ConditionSpec, s as Defaults, t as Diagnostic, u as Enforceability, K as KeyTier, N as NEVER, v as NetworkEntry, O as Overlay, w as ParseResult, x as PolicySpec, y as PolicyTarget, z as Provenance, R as RuleValue, B as RuleVariant, F as SCHEMA_VERSION, G as SOURCE_OPERATORS, H as SchemaBinding, J as Section, M as Severity, Q as SourceOperator, T as SourceTarget, U as StatementMatch, W as Subject, X as ToolsSection, Y as VERDICTS, Z as enforceabilityReport, _ as layerOfTarget, $ as parseDocument, a0 as parseYaml, a1 as serialize, a2 as validate } from './document-entry-iS3HikJ9.js';
3
3
 
4
4
  /**
5
5
  * Canonical JSON: sorted keys, no whitespace, ASCII escapes for anything
@@ -43,6 +43,11 @@ interface Principal {
43
43
  /** One call, as the enforce path and the local hook both see it. */
44
44
  interface CallInput {
45
45
  tool: string;
46
+ /**
47
+ * The inventory's `mcp__<server>__<tool>` name for the tool the host matched,
48
+ * so a rule keyed by the qualified name applies to a bare-name call.
49
+ */
50
+ qualifiedName?: string;
46
51
  arguments: Record<string, unknown>;
47
52
  /**
48
53
  * Derived fields a host may supply: `path`, `program`, `command`,
@@ -98,7 +103,7 @@ interface BoundDocuments {
98
103
  * Four stages over four layers (PRD §7.4):
99
104
  * 1. floor: a floor block ends evaluation; otherwise its verdict is retained
100
105
  * 2. resolve: the member scope inside the most specific document
101
- * 3. candidate: member rule, then agent, then group, then the most specific tools.default
106
+ * 3. candidate: member rule, then agent, then group, then the stricter of the bound tools.default values
102
107
  * 4. merge: the stricter of floor and candidate
103
108
  */
104
109
  declare function evaluate(input: CallInput, bound: BoundDocuments): Evaluation;
@@ -122,47 +127,12 @@ declare function conditionsHold(input: CallInput, conditions: Condition[]): bool
122
127
  declare function conditionHolds(input: CallInput, condition: Condition): boolean;
123
128
 
124
129
  /**
125
- * Key selection: an exact key beats any glob, a longer glob beats a shorter
126
- * one. Two same-length globs matching one tool are rejected at compile time;
127
- * here the first in document order wins so evaluation stays deterministic.
128
- */
129
- declare function selectRuleKey(statements: Statement[], tool: string): string | null;
130
-
131
- /** One tool the target can reach, as the inventory knows it. */
132
- interface InventoryTool {
133
- /** The wire name the model sees; for an MCP tool the host may also supply the qualified `mcp__<server>__<tool>` form as `qualifiedName`. */
134
- name: string;
135
- qualifiedName?: string;
136
- owner: {
137
- type: 'mcp_server' | 'agent' | 'local';
138
- id: string;
139
- name?: string;
140
- };
141
- schemaHash: string | null;
142
- inputSchema?: unknown;
143
- capabilities?: string[];
144
- /** False for a row the observed feed created and no owner has declared: the compiler warns instead of binding. */
145
- declared: boolean;
146
- }
147
- /** What a host implements: conmap on `tool_snapshots`, the Armor CLI on the local tool list. */
148
- interface InventoryPort {
149
- toolsForTarget(target: SourceTarget): Promise<InventoryTool[]>;
150
- }
151
- declare const CAPABILITY_KEYS: ReadonlySet<string>;
152
-
153
- type Enforceability = 'enforced' | 'argument-only' | 'inert';
154
- type Section = 'tools' | 'files' | 'network' | 'skills' | 'secrets';
155
- /**
156
- * Per-section honesty (FR-9, FR-19): a local device sees real paths and
157
- * hosts; the enterprise SDK path sees only path-shaped and URL-shaped
158
- * arguments; skills and secrets have no enterprise surface at all.
130
+ * Key selection: a key matches the call's bare name or its qualified name.
131
+ * An exact key beats any glob, a longer key beats a shorter one. Two
132
+ * same-length globs matching one tool are rejected at compile time; here the
133
+ * first in document order wins so evaluation stays deterministic.
159
134
  */
160
- interface EnforceabilityReport {
161
- sections: Partial<Record<Section, Enforceability>>;
162
- /** Tools in the inventory that no capability rule reaches, when the document uses capability keys (M-12). */
163
- notCoveredByCapabilityRules: string[];
164
- }
165
- declare function enforceabilityReport(source: PolicySource, inventory: InventoryTool[]): EnforceabilityReport;
135
+ declare function selectRuleKey(statements: Statement[], call: Pick<CallInput, 'tool' | 'qualifiedName'>): string | null;
166
136
 
167
137
  interface CompileOptions {
168
138
  policyId: string;
@@ -198,4 +168,4 @@ declare function checkArgumentKeys(fields: string[], tool: InventoryTool, diag:
198
168
 
199
169
  declare const KNOWN_CONTROLS: ReadonlySet<string>;
200
170
 
201
- export { Approval, type BoundDocuments, CAPABILITY_KEYS, type CallInput, type CompileOptions, type CompileResult, Condition, Diagnostics, type Enforceability, type EnforceabilityReport, type Evaluation, HASH_PREFIX, type InventoryPort, type InventoryTool, KNOWN_CONTROLS, Layer, type Obligation, PolicyDocument, PolicySource, type Principal, type SchemaDriftObligation, type Section, SourceTarget, Statement, VERDICT_RANK, Verdict, type VerdictProvenance, canonicalJson, checkArgumentKeys, checkGlobCollisions, compile, compileWithPort, conditionHolds, conditionsHold, enforceabilityReport, evaluate, globToRegExp, isGlob, matchGlob, policyHash, resolveField, resolveKey, schemaHash, selectRuleKey, sha256Canonical, stricter, verifyPolicyHash };
171
+ export { Approval, type BoundDocuments, type CallInput, type CompileOptions, type CompileResult, Condition, Diagnostics, EnforceabilityReport, type Evaluation, HASH_PREFIX, InventoryPort, InventoryTool, KNOWN_CONTROLS, Layer, type Obligation, PolicyDocument, PolicySource, type Principal, type SchemaDriftObligation, Statement, VERDICT_RANK, Verdict, type VerdictProvenance, canonicalJson, checkArgumentKeys, checkGlobCollisions, compile, compileWithPort, conditionHolds, conditionsHold, evaluate, globToRegExp, isGlob, matchGlob, policyHash, resolveField, resolveKey, schemaHash, selectRuleKey, sha256Canonical, stricter, verifyPolicyHash };
package/dist/index.js CHANGED
@@ -214,10 +214,13 @@ function numeric(a, e, test) {
214
214
  }
215
215
 
216
216
  // src/evaluate/select.ts
217
- function selectRuleKey(statements, tool) {
217
+ function selectRuleKey(statements, call) {
218
+ return selectKey(ruleKeysOf(statements), call.qualifiedName === void 0 ? [call.tool] : [call.tool, call.qualifiedName]);
219
+ }
220
+ function selectKey(keys, names) {
218
221
  let best = null;
219
- for (const key of ruleKeysOf(statements)) {
220
- if (!keyMatches(key, tool)) continue;
222
+ for (const key of keys) {
223
+ if (!names.some((name) => keyMatches(key, name))) continue;
221
224
  if (best === null || keyBeats(key, best)) best = key;
222
225
  }
223
226
  return best;
@@ -279,7 +282,7 @@ function decideLayer(statements, input) {
279
282
  return stricter(capability.statement.effect, ruleDecision.statement.effect) === ruleDecision.statement.effect ? ruleDecision : capability;
280
283
  }
281
284
  function decideRule(statements, input) {
282
- const key = selectRuleKey(statements, input.tool);
285
+ const key = selectRuleKey(statements, input);
283
286
  if (key === null) return null;
284
287
  const winner = variantsOf(statements, key).find((v) => conditionsHold(input, v.conditions));
285
288
  return winner ? { statement: winner, obligations: driftObligations(winner, input) } : null;
@@ -307,11 +310,12 @@ function driftObligations(statement, input) {
307
310
 
308
311
  // src/evaluate/evaluate.ts
309
312
  function evaluate(input, bound) {
313
+ if (!bound.floor && !bound.group && !bound.agent) throw new Error("evaluate: no document is bound to the target");
310
314
  const floor2 = decideFloor(bound.floor, input);
311
315
  if (floor2?.statement.effect === "block") return fromDecision(floor2, bound.floor, "floor");
312
- const candidate = decideCandidate(input, bound);
316
+ const candidate = decideCandidate(input, bound, floor2 === null);
313
317
  const floorVerdict = floor2?.statement.effect ?? null;
314
- if (floorVerdict && stricter(floorVerdict, candidate.verdict) !== candidate.verdict) {
318
+ if (!candidate || floorVerdict && stricter(floorVerdict, candidate.verdict) !== candidate.verdict) {
315
319
  return { ...fromDecision(floor2, bound.floor, "floor"), floorVerdict };
316
320
  }
317
321
  return { ...candidate, floorVerdict };
@@ -320,21 +324,28 @@ function decideFloor(doc, input) {
320
324
  if (!doc) return null;
321
325
  return decideLayer(liveStatements(doc, input).filter((s) => s.subject === null), input);
322
326
  }
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
+ function decideCandidate(input, bound, floorSilent) {
328
+ for (const doc2 of [bound.agent, bound.group]) {
329
+ if (!doc2) continue;
330
+ const statements = liveStatements(doc2, input);
327
331
  const member = decideLayer(memberScope(statements, input.member), input);
328
- if (member) return fromDecision(member, doc, "member");
332
+ if (member) return fromDecision(member, doc2, "member");
329
333
  const own = decideLayer(statements.filter((s) => s.subject === null), input);
330
- if (own) return fromDecision(own, doc, doc.layer);
334
+ if (own) return fromDecision(own, doc2, doc2.layer);
331
335
  }
332
- return fromDefault(mostSpecific(bound));
336
+ const doc = defaultDocument(bound, floorSilent, input.timestamp);
337
+ return doc ? fromDefault(doc) : null;
333
338
  }
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;
339
+ function defaultDocument(bound, floorSilent, timestamp) {
340
+ const all = [bound.agent, bound.group, bound.floor].filter((d) => d !== void 0);
341
+ const live = all.filter((d) => isLive(d.expiresAt, timestamp));
342
+ if (live.length === 0) return all[0] ?? null;
343
+ let winner = null;
344
+ for (const doc of live) {
345
+ if (doc === bound.floor && !floorSilent) continue;
346
+ if (!winner || stricter(winner.defaults.tools, doc.defaults.tools) !== winner.defaults.tools) winner = doc;
347
+ }
348
+ return winner;
338
349
  }
339
350
  function fromDecision(decision, doc, layer) {
340
351
  const { statement } = decision;
@@ -360,7 +371,7 @@ function fromDefault(doc) {
360
371
  return {
361
372
  verdict,
362
373
  floorVerdict: null,
363
- approval: null,
374
+ approval: verdict === "hold" ? doc.defaults.approval ?? null : null,
364
375
  obligations: [],
365
376
  provenance: { policyId: doc.policyId, statementId: null, ruleKey: "tools.default", variant: -1, layer: doc.layer }
366
377
  };
@@ -769,6 +780,35 @@ function parseCapture(r, value) {
769
780
  return out;
770
781
  }
771
782
 
783
+ // src/document/loosening.ts
784
+ function looseningsOf(base, overlay, inventory) {
785
+ const baseKeys = Object.keys(base.rules ?? {});
786
+ const overlayKeys = Object.keys(overlay);
787
+ const out = [];
788
+ for (const key of overlayKeys) {
789
+ const verdict = loosest(overlay[key]);
790
+ const bases = [baseVerdict(base, baseKeys.includes(key) ? key : null)];
791
+ for (const tool of inventory) {
792
+ const names = tool.qualifiedName === void 0 ? [tool.name] : [tool.name, tool.qualifiedName];
793
+ if (selectKey(overlayKeys, names) === key) bases.push(baseVerdict(base, selectKey(baseKeys, names)));
794
+ }
795
+ const strictest = bases.reduce((a, b) => looser(a, b) ? b : a);
796
+ if (looser(verdict, strictest)) out.push({ key, verdict, base: strictest });
797
+ }
798
+ return out;
799
+ }
800
+ function baseVerdict(base, key) {
801
+ const variants = key === null ? void 0 : base.rules?.[key];
802
+ if (!variants) return base.default;
803
+ return variants.reduce((v, x) => looser(v, x.action) ? x.action : v, "allow");
804
+ }
805
+ function loosest(variants) {
806
+ return variants.reduce((v, x) => looser(x.action, v) ? x.action : v, "block");
807
+ }
808
+ function looser(a, b) {
809
+ return VERDICT_RANK[a] < VERDICT_RANK[b];
810
+ }
811
+
772
812
  // src/document/portable-regex.ts
773
813
  function isPortablePattern(pattern) {
774
814
  if (typeof pattern !== "string") return false;
@@ -883,9 +923,9 @@ function isSurrogate(c) {
883
923
  }
884
924
 
885
925
  // src/document/validate.ts
886
- function validate(source) {
926
+ function validate(source, inventory = []) {
887
927
  const diag = new Diagnostics();
888
- const ctx = { diag, approvalDefaults: source.spec.approvalDefaults, base: source.spec.tools };
928
+ const ctx = { diag, approvalDefaults: source.spec.approvalDefaults, base: source.spec.tools, inventory };
889
929
  validateTargetSections(ctx, source);
890
930
  if (source.spec.approvalDefaults) validateApproval(ctx, source.spec.approvalDefaults, "/spec/approvalDefaults", true);
891
931
  validateRules(ctx, source.spec.tools.rules, "/spec/tools/rules");
@@ -978,23 +1018,14 @@ function validateOverlays(ctx, overlays2, path) {
978
1018
  }
979
1019
  function flagLoosening(ctx, overlay, path) {
980
1020
  const baseDefault = ctx.base.default;
981
- if (overlay.tools?.default && looser(overlay.tools.default, baseDefault)) {
1021
+ if (overlay.tools?.default && looser2(overlay.tools.default, baseDefault)) {
982
1022
  ctx.diag.warning("loosening_grant", `${path}/tools/default`, `default ${overlay.tools.default} is looser than the base default ${baseDefault}`);
983
1023
  }
984
- for (const [key, variants] of Object.entries(overlay.tools?.rules ?? {})) {
985
- const base = baseVerdictFor(ctx.base, key);
986
- const loosest = variants.reduce((v, x) => looser(x.action, v) ? x.action : v, "block");
987
- if (looser(loosest, base)) {
988
- ctx.diag.warning("loosening_grant", `${path}/tools/rules/${key}`, `${key}: ${loosest} is looser than the base ${base}`);
989
- }
1024
+ for (const { key, verdict, base } of looseningsOf(ctx.base, overlay.tools?.rules ?? {}, ctx.inventory)) {
1025
+ ctx.diag.warning("loosening_grant", `${path}/tools/rules/${key}`, `${key}: ${verdict} is looser than the base ${base}`);
990
1026
  }
991
1027
  }
992
- function baseVerdictFor(base, key) {
993
- const variants = base.rules?.[key];
994
- if (!variants) return base.default;
995
- return variants.reduce((v, x) => looser(v, x.action) ? x.action : v, "allow");
996
- }
997
- function looser(a, b) {
1028
+ function looser2(a, b) {
998
1029
  return VERDICT_RANK[a] < VERDICT_RANK[b];
999
1030
  }
1000
1031
  function validateCapture(ctx, source) {
@@ -1036,11 +1067,11 @@ function resolveKey(key, inventory, diag, path) {
1036
1067
  }
1037
1068
  function bindExact(key, tool, diag, path) {
1038
1069
  if (!tool) {
1039
- diag.warning("unknown_tool_key", path, `"${key}" is not in the target's inventory; the rule compiles and binds nothing until the tool appears`);
1070
+ diag.warning("unknown_tool_key", path, `"${key}" is not in the target's inventory yet; the rule is enforced on any call that names it, but its conditions are not checked against a schema`);
1040
1071
  return null;
1041
1072
  }
1042
1073
  if (!tool.declared) {
1043
- diag.warning("observed_only_tool", path, `"${key}" has only been observed, never declared; the rule compiles without a schema binding`);
1074
+ diag.warning("observed_only_tool", path, `"${key}" has been observed but never declared; the rule is enforced on any call that names it, but its conditions are not checked against a schema`);
1044
1075
  return null;
1045
1076
  }
1046
1077
  return tool;
@@ -1348,13 +1379,15 @@ function uncovered(source, inventory) {
1348
1379
 
1349
1380
  // src/compile/compile.ts
1350
1381
  function compile(source, inventory, options) {
1351
- const diag = validate(source);
1382
+ const diag = validate(source, inventory);
1352
1383
  const report = enforceabilityReport(source, inventory);
1353
1384
  if (diag.hasErrors) return { document: null, diagnostics: diag, report };
1354
1385
  const layer = layerOfTarget(targetOf2(source.target));
1355
1386
  const ctx = { inventory, diag, layer, approvalDefaults: source.spec.approvalDefaults, source: options.source ?? "yaml" };
1356
1387
  checkGlobCollisions(Object.keys(source.spec.tools.rules ?? {}), inventory, diag, "/spec/tools/rules");
1357
- checkDeviceFirst(source, diag);
1388
+ checkOverlayGlobCollisions(source.spec.members, inventory, diag, "/spec/members");
1389
+ checkOverlayGlobCollisions(source.spec.products, inventory, diag, "/spec/products");
1390
+ checkDeviceFirst(source, inventory, diag);
1358
1391
  const statements = [
1359
1392
  ...lowerRules(source.spec.tools.rules, ctx, "/spec/tools/rules", null, source.spec.expiresAt),
1360
1393
  ...lowerFloors(source.spec, ctx),
@@ -1390,7 +1423,8 @@ function defaultsOf(source) {
1390
1423
  tools: source.spec.tools.default,
1391
1424
  network: source.spec.network?.default ?? "block",
1392
1425
  files: "allow",
1393
- skills: source.spec.skills?.default ?? "allow"
1426
+ skills: source.spec.skills?.default ?? "allow",
1427
+ ...source.spec.approvalDefaults ? { approval: source.spec.approvalDefaults } : {}
1394
1428
  };
1395
1429
  }
1396
1430
  function captureOf(source) {
@@ -1402,24 +1436,19 @@ function captureOf(source) {
1402
1436
  sample: c?.sample ?? 1
1403
1437
  };
1404
1438
  }
1405
- function checkDeviceFirst(source, diag) {
1439
+ function checkOverlayGlobCollisions(overlays2, inventory, diag, path) {
1440
+ for (const [key, overlay] of Object.entries(overlays2 ?? {})) {
1441
+ checkGlobCollisions(Object.keys(overlay.tools?.rules ?? {}), inventory, diag, `${path}/${key}/tools/rules`);
1442
+ }
1443
+ }
1444
+ function checkDeviceFirst(source, inventory, diag) {
1406
1445
  if (!("device" in source.target) || source.precedence === "agent-first") return;
1407
1446
  for (const [product, overlay] of Object.entries(source.spec.products ?? {})) {
1408
- for (const [key, variants] of Object.entries(overlay.tools?.rules ?? {})) {
1409
- if (loosensBase(variants, baseRankFor(source, key))) {
1410
- 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`);
1411
- }
1447
+ for (const { key } of looseningsOf(source.spec.tools, overlay.tools?.rules ?? {}, inventory)) {
1448
+ 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`);
1412
1449
  }
1413
1450
  }
1414
1451
  }
1415
- function baseRankFor(source, key) {
1416
- const base = source.spec.tools.rules?.[key];
1417
- if (!Array.isArray(base)) return VERDICT_RANK[source.spec.tools.default];
1418
- return Math.min(...base.map((v) => VERDICT_RANK[v.action]));
1419
- }
1420
- function loosensBase(variants, baseRank) {
1421
- return variants.some((v) => VERDICT_RANK[v.action] < baseRank);
1422
- }
1423
1452
 
1424
1453
  // src/compile/serialize.ts
1425
1454
  import { stringify } from "yaml";
@@ -1455,8 +1484,12 @@ function overlays(map) {
1455
1484
  }
1456
1485
  return out;
1457
1486
  }
1487
+
1488
+ // src/compose.ts
1489
+ var COMPOSE_INTENT_MAX = 1e5;
1458
1490
  export {
1459
1491
  CAPABILITY_KEYS,
1492
+ COMPOSE_INTENT_MAX,
1460
1493
  Diagnostics,
1461
1494
  HASH_PREFIX,
1462
1495
  KNOWN_CONTROLS,