@armoriq/policy-ir 0.1.3 → 0.1.5

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.
@@ -40,6 +40,8 @@ interface Defaults {
40
40
  network: Verdict;
41
41
  files: Verdict;
42
42
  skills: Verdict;
43
+ /** Who approves a hold from `tools.default`: the source's `spec.approvalDefaults`. */
44
+ approval?: Approval;
43
45
  }
44
46
  interface Capture {
45
47
  arguments: boolean;
@@ -211,12 +213,35 @@ declare function parseYaml(text: string): ParseResult;
211
213
  /** Parse an already-loaded YAML/JSON value strictly. */
212
214
  declare function parseDocument(value: unknown): ParseResult;
213
215
 
216
+ /** One tool the target can reach, as the inventory knows it. */
217
+ interface InventoryTool {
218
+ /** The wire name the model sees; for an MCP tool the host may also supply the qualified `mcp__<server>__<tool>` form as `qualifiedName`. */
219
+ name: string;
220
+ qualifiedName?: string;
221
+ owner: {
222
+ type: 'mcp_server' | 'agent' | 'local';
223
+ id: string;
224
+ name?: string;
225
+ };
226
+ schemaHash: string | null;
227
+ inputSchema?: unknown;
228
+ capabilities?: string[];
229
+ /** False for a row the observed feed created and no owner has declared: the compiler warns instead of binding. */
230
+ declared: boolean;
231
+ }
232
+ /** What a host implements: conmap on `tool_snapshots`, the Armor CLI on the local tool list. */
233
+ interface InventoryPort {
234
+ toolsForTarget(target: SourceTarget): Promise<InventoryTool[]>;
235
+ }
236
+ declare const CAPABILITY_KEYS: ReadonlySet<string>;
237
+
214
238
  /**
215
239
  * Everything that can be checked without an inventory. Inventory-dependent
216
240
  * checks (roster clamp, glob collisions on real tools, schema binding) live in
217
- * the compiler.
241
+ * the compiler. An inventory, when given, lets the loosening check match a
242
+ * tool's bare and qualified names across the base and an overlay.
218
243
  */
219
- declare function validate(source: PolicySource): Diagnostics;
244
+ declare function validate(source: PolicySource, inventory?: InventoryTool[]): Diagnostics;
220
245
 
221
246
  /**
222
247
  * PolicySource back to YAML such that `parse(serialize(x))` equals `x`
@@ -226,4 +251,86 @@ declare function validate(source: PolicySource): Diagnostics;
226
251
  */
227
252
  declare function serialize(source: PolicySource): string;
228
253
 
229
- export { type Approval as A, type Condition as C, Diagnostics as D, type KeyTier as K, type Layer as L, NEVER as N, type Overlay as O, type PolicyDocument as P, type RuleValue as R, type Statement as S, type ToolsSection as T, type Verdict as V, type SourceTarget as a, type PolicySource as b, type Capture as c, type ConditionOp as d, type ConditionSpec as e, type Defaults as f, type Diagnostic as g, type NetworkEntry as h, type ParseResult as i, type PolicySpec as j, type PolicyTarget as k, type Provenance as l, type RuleVariant as m, SCHEMA_VERSION as n, SOURCE_OPERATORS as o, type SchemaBinding as p, type Severity as q, type SourceOperator as r, type StatementMatch as s, type Subject as t, VERDICTS as u, layerOfTarget as v, parseDocument as w, parseYaml as x, serialize as y, validate as z };
254
+ type Enforceability = 'enforced' | 'argument-only' | 'inert';
255
+ type Section = 'tools' | 'files' | 'network' | 'skills' | 'secrets';
256
+ /**
257
+ * Per-section honesty (FR-9, FR-19): a local device sees real paths and
258
+ * hosts; the enterprise SDK path sees only path-shaped and URL-shaped
259
+ * arguments; skills and secrets have no enterprise surface at all.
260
+ */
261
+ interface EnforceabilityReport {
262
+ sections: Partial<Record<Section, Enforceability>>;
263
+ /** Tools in the inventory that no capability rule reaches, when the document uses capability keys (M-12). */
264
+ notCoveredByCapabilityRules: string[];
265
+ }
266
+ declare function enforceabilityReport(source: PolicySource, inventory: InventoryTool[]): EnforceabilityReport;
267
+
268
+ declare const COMPOSE_INTENT_MAX = 4000;
269
+ interface ComposeTarget {
270
+ agent: string;
271
+ }
272
+ interface ComposeAnswer {
273
+ questionId: string;
274
+ /** The question as it was asked, since compose keeps no state between rounds. */
275
+ question: string;
276
+ text: string;
277
+ }
278
+ interface ComposeRequest {
279
+ target: ComposeTarget;
280
+ intent: string;
281
+ draft?: string;
282
+ answers?: ComposeAnswer[];
283
+ }
284
+ interface ComposeUnsupported {
285
+ sentence: string;
286
+ reason: string;
287
+ }
288
+ interface ComposeQuestion {
289
+ id: string;
290
+ text: string;
291
+ }
292
+ interface ComposeChangedCall {
293
+ auditId: string;
294
+ tool: string;
295
+ arguments: Record<string, unknown>;
296
+ executedAt: string;
297
+ from: Verdict;
298
+ to: Verdict;
299
+ }
300
+ interface ComposeReplay {
301
+ calls: number;
302
+ verdicts: Record<Verdict, number>;
303
+ changed: ComposeChangedCall[];
304
+ }
305
+ interface ComposeExample {
306
+ /** Same format as `Diagnostic.path`: `/spec/tools/rules/<key>/<variant>`. */
307
+ path: string;
308
+ call: {
309
+ tool: string;
310
+ qualifiedName?: string;
311
+ arguments: Record<string, unknown>;
312
+ };
313
+ verdict: Verdict;
314
+ }
315
+ interface ComposePreview {
316
+ replay: ComposeReplay | null;
317
+ examples: ComposeExample[];
318
+ }
319
+ interface ComposeResponse {
320
+ composeId: string;
321
+ document: PolicyDocument | null;
322
+ yaml: string;
323
+ diagnostics: Diagnostic[];
324
+ report: EnforceabilityReport;
325
+ unsupported: ComposeUnsupported[];
326
+ questions: ComposeQuestion[];
327
+ /** Source sentence per rule, keyed by `Diagnostic.path`: `/spec/tools/rules/<key>/<variant>`. */
328
+ sources: Record<string, string>;
329
+ preview: ComposePreview;
330
+ model: string;
331
+ rounds: number;
332
+ /** The model's one-paragraph explanation of the draft. */
333
+ reasoning?: string;
334
+ }
335
+
336
+ export { parseYaml as $, type Approval as A, SCHEMA_VERSION as B, type Condition as C, Diagnostics as D, type EnforceabilityReport as E, SOURCE_OPERATORS as F, type SchemaBinding as G, type Section as H, type InventoryTool as I, type Severity as J, type KeyTier as K, type Layer as L, type SourceOperator as M, NEVER as N, type Overlay as O, type PolicyDocument as P, type SourceTarget as Q, type RuleValue as R, type Statement as S, type StatementMatch as T, type Subject as U, type Verdict as V, type ToolsSection as W, VERDICTS as X, enforceabilityReport as Y, layerOfTarget as Z, parseDocument as _, type PolicySource as a, serialize as a0, validate as a1, type InventoryPort as b, CAPABILITY_KEYS as c, COMPOSE_INTENT_MAX as d, type Capture as e, type ComposeAnswer as f, type ComposeChangedCall as g, type ComposeExample as h, type ComposePreview as i, type ComposeQuestion as j, type ComposeReplay as k, type ComposeRequest as l, type ComposeResponse as m, type ComposeTarget as n, type ComposeUnsupported as o, type ConditionOp as p, type ConditionSpec as q, type Defaults as r, type Diagnostic as s, type Enforceability as t, type NetworkEntry as u, type ParseResult as v, type PolicySpec as w, type PolicyTarget as x, type Provenance as y, type RuleVariant as z };
@@ -40,6 +40,8 @@ interface Defaults {
40
40
  network: Verdict;
41
41
  files: Verdict;
42
42
  skills: Verdict;
43
+ /** Who approves a hold from `tools.default`: the source's `spec.approvalDefaults`. */
44
+ approval?: Approval;
43
45
  }
44
46
  interface Capture {
45
47
  arguments: boolean;
@@ -211,12 +213,35 @@ declare function parseYaml(text: string): ParseResult;
211
213
  /** Parse an already-loaded YAML/JSON value strictly. */
212
214
  declare function parseDocument(value: unknown): ParseResult;
213
215
 
216
+ /** One tool the target can reach, as the inventory knows it. */
217
+ interface InventoryTool {
218
+ /** The wire name the model sees; for an MCP tool the host may also supply the qualified `mcp__<server>__<tool>` form as `qualifiedName`. */
219
+ name: string;
220
+ qualifiedName?: string;
221
+ owner: {
222
+ type: 'mcp_server' | 'agent' | 'local';
223
+ id: string;
224
+ name?: string;
225
+ };
226
+ schemaHash: string | null;
227
+ inputSchema?: unknown;
228
+ capabilities?: string[];
229
+ /** False for a row the observed feed created and no owner has declared: the compiler warns instead of binding. */
230
+ declared: boolean;
231
+ }
232
+ /** What a host implements: conmap on `tool_snapshots`, the Armor CLI on the local tool list. */
233
+ interface InventoryPort {
234
+ toolsForTarget(target: SourceTarget): Promise<InventoryTool[]>;
235
+ }
236
+ declare const CAPABILITY_KEYS: ReadonlySet<string>;
237
+
214
238
  /**
215
239
  * Everything that can be checked without an inventory. Inventory-dependent
216
240
  * checks (roster clamp, glob collisions on real tools, schema binding) live in
217
- * the compiler.
241
+ * the compiler. An inventory, when given, lets the loosening check match a
242
+ * tool's bare and qualified names across the base and an overlay.
218
243
  */
219
- declare function validate(source: PolicySource): Diagnostics;
244
+ declare function validate(source: PolicySource, inventory?: InventoryTool[]): Diagnostics;
220
245
 
221
246
  /**
222
247
  * PolicySource back to YAML such that `parse(serialize(x))` equals `x`
@@ -226,4 +251,86 @@ declare function validate(source: PolicySource): Diagnostics;
226
251
  */
227
252
  declare function serialize(source: PolicySource): string;
228
253
 
229
- export { type Approval as A, type Condition as C, Diagnostics as D, type KeyTier as K, type Layer as L, NEVER as N, type Overlay as O, type PolicyDocument as P, type RuleValue as R, type Statement as S, type ToolsSection as T, type Verdict as V, type SourceTarget as a, type PolicySource as b, type Capture as c, type ConditionOp as d, type ConditionSpec as e, type Defaults as f, type Diagnostic as g, type NetworkEntry as h, type ParseResult as i, type PolicySpec as j, type PolicyTarget as k, type Provenance as l, type RuleVariant as m, SCHEMA_VERSION as n, SOURCE_OPERATORS as o, type SchemaBinding as p, type Severity as q, type SourceOperator as r, type StatementMatch as s, type Subject as t, VERDICTS as u, layerOfTarget as v, parseDocument as w, parseYaml as x, serialize as y, validate as z };
254
+ type Enforceability = 'enforced' | 'argument-only' | 'inert';
255
+ type Section = 'tools' | 'files' | 'network' | 'skills' | 'secrets';
256
+ /**
257
+ * Per-section honesty (FR-9, FR-19): a local device sees real paths and
258
+ * hosts; the enterprise SDK path sees only path-shaped and URL-shaped
259
+ * arguments; skills and secrets have no enterprise surface at all.
260
+ */
261
+ interface EnforceabilityReport {
262
+ sections: Partial<Record<Section, Enforceability>>;
263
+ /** Tools in the inventory that no capability rule reaches, when the document uses capability keys (M-12). */
264
+ notCoveredByCapabilityRules: string[];
265
+ }
266
+ declare function enforceabilityReport(source: PolicySource, inventory: InventoryTool[]): EnforceabilityReport;
267
+
268
+ declare const COMPOSE_INTENT_MAX = 4000;
269
+ interface ComposeTarget {
270
+ agent: string;
271
+ }
272
+ interface ComposeAnswer {
273
+ questionId: string;
274
+ /** The question as it was asked, since compose keeps no state between rounds. */
275
+ question: string;
276
+ text: string;
277
+ }
278
+ interface ComposeRequest {
279
+ target: ComposeTarget;
280
+ intent: string;
281
+ draft?: string;
282
+ answers?: ComposeAnswer[];
283
+ }
284
+ interface ComposeUnsupported {
285
+ sentence: string;
286
+ reason: string;
287
+ }
288
+ interface ComposeQuestion {
289
+ id: string;
290
+ text: string;
291
+ }
292
+ interface ComposeChangedCall {
293
+ auditId: string;
294
+ tool: string;
295
+ arguments: Record<string, unknown>;
296
+ executedAt: string;
297
+ from: Verdict;
298
+ to: Verdict;
299
+ }
300
+ interface ComposeReplay {
301
+ calls: number;
302
+ verdicts: Record<Verdict, number>;
303
+ changed: ComposeChangedCall[];
304
+ }
305
+ interface ComposeExample {
306
+ /** Same format as `Diagnostic.path`: `/spec/tools/rules/<key>/<variant>`. */
307
+ path: string;
308
+ call: {
309
+ tool: string;
310
+ qualifiedName?: string;
311
+ arguments: Record<string, unknown>;
312
+ };
313
+ verdict: Verdict;
314
+ }
315
+ interface ComposePreview {
316
+ replay: ComposeReplay | null;
317
+ examples: ComposeExample[];
318
+ }
319
+ interface ComposeResponse {
320
+ composeId: string;
321
+ document: PolicyDocument | null;
322
+ yaml: string;
323
+ diagnostics: Diagnostic[];
324
+ report: EnforceabilityReport;
325
+ unsupported: ComposeUnsupported[];
326
+ questions: ComposeQuestion[];
327
+ /** Source sentence per rule, keyed by `Diagnostic.path`: `/spec/tools/rules/<key>/<variant>`. */
328
+ sources: Record<string, string>;
329
+ preview: ComposePreview;
330
+ model: string;
331
+ rounds: number;
332
+ /** The model's one-paragraph explanation of the draft. */
333
+ reasoning?: string;
334
+ }
335
+
336
+ export { parseYaml as $, type Approval as A, SCHEMA_VERSION as B, type Condition as C, Diagnostics as D, type EnforceabilityReport as E, SOURCE_OPERATORS as F, type SchemaBinding as G, type Section as H, type InventoryTool as I, type Severity as J, type KeyTier as K, type Layer as L, type SourceOperator as M, NEVER as N, type Overlay as O, type PolicyDocument as P, type SourceTarget as Q, type RuleValue as R, type Statement as S, type StatementMatch as T, type Subject as U, type Verdict as V, type ToolsSection as W, VERDICTS as X, enforceabilityReport as Y, layerOfTarget as Z, parseDocument as _, type PolicySource as a, serialize as a0, validate as a1, type InventoryPort as b, CAPABILITY_KEYS as c, COMPOSE_INTENT_MAX as d, type Capture as e, type ComposeAnswer as f, type ComposeChangedCall as g, type ComposeExample as h, type ComposePreview as i, type ComposeQuestion as j, type ComposeReplay as k, type ComposeRequest as l, type ComposeResponse as m, type ComposeTarget as n, type ComposeUnsupported as o, type ConditionOp as p, type ConditionSpec as q, type Defaults as r, type Diagnostic as s, type Enforceability as t, type NetworkEntry as u, type ParseResult as v, type PolicySpec as w, type PolicyTarget as x, type Provenance as y, type RuleVariant as z };
package/dist/document.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/document-entry.ts
21
21
  var document_entry_exports = {};
22
22
  __export(document_entry_exports, {
23
+ COMPOSE_INTENT_MAX: () => COMPOSE_INTENT_MAX,
23
24
  Diagnostics: () => Diagnostics,
24
25
  NEVER: () => NEVER,
25
26
  SOURCE_OPERATORS: () => SOURCE_OPERATORS,
@@ -437,6 +438,81 @@ function parseCapture(r, value) {
437
438
  // src/evaluate/input.ts
438
439
  var VERDICT_RANK = { allow: 0, hold: 1, block: 2 };
439
440
 
441
+ // src/evaluate/glob.ts
442
+ function globToRegExp(pattern) {
443
+ let source = "^";
444
+ for (let i = 0; i < pattern.length; i++) {
445
+ const ch = pattern[i];
446
+ if (ch === "*") {
447
+ const double = pattern[i + 1] === "*";
448
+ source += double ? ".*" : "[^/]*";
449
+ if (double) i++;
450
+ } else if (ch === "?") {
451
+ source += "[^/]";
452
+ } else {
453
+ source += escapeRegExp(ch);
454
+ }
455
+ }
456
+ return new RegExp(source + "$");
457
+ }
458
+ function matchGlob(pattern, value) {
459
+ return globToRegExp(pattern).test(value);
460
+ }
461
+ function isGlob(key) {
462
+ return key.includes("*") || key.includes("?");
463
+ }
464
+ function escapeRegExp(ch) {
465
+ return /[\\^$.|?*+()[\]{}]/.test(ch) ? "\\" + ch : ch;
466
+ }
467
+
468
+ // src/evaluate/select.ts
469
+ function selectKey(keys, names) {
470
+ let best = null;
471
+ for (const key of keys) {
472
+ if (!names.some((name) => keyMatches(key, name))) continue;
473
+ if (best === null || keyBeats(key, best)) best = key;
474
+ }
475
+ return best;
476
+ }
477
+ function keyMatches(key, tool) {
478
+ return isGlob(key) ? matchGlob(key, tool) : key === tool;
479
+ }
480
+ function keyBeats(candidate, incumbent) {
481
+ const candidateGlob = isGlob(candidate);
482
+ const incumbentGlob = isGlob(incumbent);
483
+ if (candidateGlob !== incumbentGlob) return !candidateGlob;
484
+ return candidate.length > incumbent.length;
485
+ }
486
+
487
+ // src/document/loosening.ts
488
+ function looseningsOf(base, overlay, inventory) {
489
+ const baseKeys = Object.keys(base.rules ?? {});
490
+ const overlayKeys = Object.keys(overlay);
491
+ const out = [];
492
+ for (const key of overlayKeys) {
493
+ const verdict = loosest(overlay[key]);
494
+ const bases = [baseVerdict(base, baseKeys.includes(key) ? key : null)];
495
+ for (const tool of inventory) {
496
+ const names = tool.qualifiedName === void 0 ? [tool.name] : [tool.name, tool.qualifiedName];
497
+ if (selectKey(overlayKeys, names) === key) bases.push(baseVerdict(base, selectKey(baseKeys, names)));
498
+ }
499
+ const strictest = bases.reduce((a, b) => looser(a, b) ? b : a);
500
+ if (looser(verdict, strictest)) out.push({ key, verdict, base: strictest });
501
+ }
502
+ return out;
503
+ }
504
+ function baseVerdict(base, key) {
505
+ const variants = key === null ? void 0 : base.rules?.[key];
506
+ if (!variants) return base.default;
507
+ return variants.reduce((v, x) => looser(v, x.action) ? x.action : v, "allow");
508
+ }
509
+ function loosest(variants) {
510
+ return variants.reduce((v, x) => looser(x.action, v) ? x.action : v, "block");
511
+ }
512
+ function looser(a, b) {
513
+ return VERDICT_RANK[a] < VERDICT_RANK[b];
514
+ }
515
+
440
516
  // src/document/portable-regex.ts
441
517
  function isPortablePattern(pattern) {
442
518
  if (typeof pattern !== "string") return false;
@@ -551,9 +627,9 @@ function isSurrogate(c) {
551
627
  }
552
628
 
553
629
  // src/document/validate.ts
554
- function validate(source) {
630
+ function validate(source, inventory = []) {
555
631
  const diag = new Diagnostics();
556
- const ctx = { diag, approvalDefaults: source.spec.approvalDefaults, base: source.spec.tools };
632
+ const ctx = { diag, approvalDefaults: source.spec.approvalDefaults, base: source.spec.tools, inventory };
557
633
  validateTargetSections(ctx, source);
558
634
  if (source.spec.approvalDefaults) validateApproval(ctx, source.spec.approvalDefaults, "/spec/approvalDefaults", true);
559
635
  validateRules(ctx, source.spec.tools.rules, "/spec/tools/rules");
@@ -646,23 +722,14 @@ function validateOverlays(ctx, overlays2, path) {
646
722
  }
647
723
  function flagLoosening(ctx, overlay, path) {
648
724
  const baseDefault = ctx.base.default;
649
- if (overlay.tools?.default && looser(overlay.tools.default, baseDefault)) {
725
+ if (overlay.tools?.default && looser2(overlay.tools.default, baseDefault)) {
650
726
  ctx.diag.warning("loosening_grant", `${path}/tools/default`, `default ${overlay.tools.default} is looser than the base default ${baseDefault}`);
651
727
  }
652
- for (const [key, variants] of Object.entries(overlay.tools?.rules ?? {})) {
653
- const base = baseVerdictFor(ctx.base, key);
654
- const loosest = variants.reduce((v, x) => looser(x.action, v) ? x.action : v, "block");
655
- if (looser(loosest, base)) {
656
- ctx.diag.warning("loosening_grant", `${path}/tools/rules/${key}`, `${key}: ${loosest} is looser than the base ${base}`);
657
- }
728
+ for (const { key, verdict, base } of looseningsOf(ctx.base, overlay.tools?.rules ?? {}, ctx.inventory)) {
729
+ ctx.diag.warning("loosening_grant", `${path}/tools/rules/${key}`, `${key}: ${verdict} is looser than the base ${base}`);
658
730
  }
659
731
  }
660
- function baseVerdictFor(base, key) {
661
- const variants = base.rules?.[key];
662
- if (!variants) return base.default;
663
- return variants.reduce((v, x) => looser(v, x.action) ? x.action : v, "allow");
664
- }
665
- function looser(a, b) {
732
+ function looser2(a, b) {
666
733
  return VERDICT_RANK[a] < VERDICT_RANK[b];
667
734
  }
668
735
  function validateCapture(ctx, source) {
@@ -708,8 +775,12 @@ function overlays(map) {
708
775
  }
709
776
  return out;
710
777
  }
778
+
779
+ // src/compose.ts
780
+ var COMPOSE_INTENT_MAX = 4e3;
711
781
  // Annotate the CommonJS export names for ESM import in node:
712
782
  0 && (module.exports = {
783
+ COMPOSE_INTENT_MAX,
713
784
  Diagnostics,
714
785
  NEVER,
715
786
  SOURCE_OPERATORS,