@inhuman.tools/policy 0.0.0-stage → 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.
Files changed (141) hide show
  1. package/authorize.scm +4 -0
  2. package/dist/acknowledge.d.ts +70 -0
  3. package/dist/acknowledge.d.ts.map +1 -0
  4. package/dist/acknowledge.js +162 -0
  5. package/dist/acknowledge.js.map +1 -0
  6. package/dist/authorize.d.ts +8 -0
  7. package/dist/authorize.d.ts.map +1 -0
  8. package/dist/authorize.js +39 -0
  9. package/dist/authorize.js.map +1 -0
  10. package/dist/bind.d.ts +6 -0
  11. package/dist/bind.d.ts.map +1 -0
  12. package/dist/bind.js +45 -0
  13. package/dist/bind.js.map +1 -0
  14. package/dist/check.d.ts +30 -0
  15. package/dist/check.d.ts.map +1 -0
  16. package/dist/check.js +389 -0
  17. package/dist/check.js.map +1 -0
  18. package/dist/classes.d.ts +23 -0
  19. package/dist/classes.d.ts.map +1 -0
  20. package/dist/classes.js +139 -0
  21. package/dist/classes.js.map +1 -0
  22. package/dist/compile.d.ts +40 -0
  23. package/dist/compile.d.ts.map +1 -0
  24. package/dist/compile.js +311 -0
  25. package/dist/compile.js.map +1 -0
  26. package/dist/deadline.d.ts +5 -0
  27. package/dist/deadline.d.ts.map +1 -0
  28. package/dist/deadline.js +17 -0
  29. package/dist/deadline.js.map +1 -0
  30. package/dist/decide.d.ts +64 -0
  31. package/dist/decide.d.ts.map +1 -0
  32. package/dist/decide.js +482 -0
  33. package/dist/decide.js.map +1 -0
  34. package/dist/error-with-invariant.d.ts +9 -0
  35. package/dist/error-with-invariant.d.ts.map +1 -0
  36. package/dist/error-with-invariant.js +12 -0
  37. package/dist/error-with-invariant.js.map +1 -0
  38. package/dist/floor.d.ts +9 -0
  39. package/dist/floor.d.ts.map +1 -0
  40. package/dist/floor.js +9 -0
  41. package/dist/floor.js.map +1 -0
  42. package/dist/glob-expand.d.ts +10 -0
  43. package/dist/glob-expand.d.ts.map +1 -0
  44. package/dist/glob-expand.js +132 -0
  45. package/dist/glob-expand.js.map +1 -0
  46. package/dist/guards.d.ts +26 -0
  47. package/dist/guards.d.ts.map +1 -0
  48. package/dist/guards.js +419 -0
  49. package/dist/guards.js.map +1 -0
  50. package/dist/identity.d.ts +11 -0
  51. package/dist/identity.d.ts.map +1 -0
  52. package/dist/identity.js +23 -0
  53. package/dist/identity.js.map +1 -0
  54. package/dist/index.d.ts +8 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +8 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/license-law.d.ts +43 -0
  59. package/dist/license-law.d.ts.map +1 -0
  60. package/dist/license-law.js +129 -0
  61. package/dist/license-law.js.map +1 -0
  62. package/dist/license.d.ts +3 -0
  63. package/dist/license.d.ts.map +1 -0
  64. package/dist/license.js +2 -0
  65. package/dist/license.js.map +1 -0
  66. package/dist/lift.d.ts +4 -0
  67. package/dist/lift.d.ts.map +1 -0
  68. package/dist/lift.js +14 -0
  69. package/dist/lift.js.map +1 -0
  70. package/dist/path-policy.d.ts +53 -0
  71. package/dist/path-policy.d.ts.map +1 -0
  72. package/dist/path-policy.js +248 -0
  73. package/dist/path-policy.js.map +1 -0
  74. package/dist/peel-cwd.d.ts +8 -0
  75. package/dist/peel-cwd.d.ts.map +1 -0
  76. package/dist/peel-cwd.js +120 -0
  77. package/dist/peel-cwd.js.map +1 -0
  78. package/dist/prep.d.ts +58 -0
  79. package/dist/prep.d.ts.map +1 -0
  80. package/dist/prep.js +690 -0
  81. package/dist/prep.js.map +1 -0
  82. package/dist/processor.d.ts +71 -0
  83. package/dist/processor.d.ts.map +1 -0
  84. package/dist/processor.js +132 -0
  85. package/dist/processor.js.map +1 -0
  86. package/dist/reasonable-defaults.d.ts +1448 -0
  87. package/dist/reasonable-defaults.d.ts.map +1 -0
  88. package/dist/reasonable-defaults.js +6 -0
  89. package/dist/reasonable-defaults.js.map +1 -0
  90. package/dist/reasonable-defaults.json +1662 -0
  91. package/dist/run-authorization.d.ts +8 -0
  92. package/dist/run-authorization.d.ts.map +1 -0
  93. package/dist/run-authorization.js +16 -0
  94. package/dist/run-authorization.js.map +1 -0
  95. package/dist/schemas/tirith.d.ts +58 -0
  96. package/dist/schemas/tirith.d.ts.map +1 -0
  97. package/dist/schemas/tirith.js +23 -0
  98. package/dist/schemas/tirith.js.map +1 -0
  99. package/dist/tirith-units.d.ts +2 -0
  100. package/dist/tirith-units.d.ts.map +1 -0
  101. package/dist/tirith-units.js +227 -0
  102. package/dist/tirith-units.js.map +1 -0
  103. package/dist/tirith.d.ts +24 -0
  104. package/dist/tirith.d.ts.map +1 -0
  105. package/dist/tirith.js +311 -0
  106. package/dist/tirith.js.map +1 -0
  107. package/dist/tool-path.d.ts +8 -0
  108. package/dist/tool-path.d.ts.map +1 -0
  109. package/dist/tool-path.js +38 -0
  110. package/dist/tool-path.js.map +1 -0
  111. package/dist/types.d.ts +87 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +14 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/verdict.d.ts +15 -0
  116. package/dist/verdict.d.ts.map +1 -0
  117. package/dist/verdict.js +55 -0
  118. package/dist/verdict.js.map +1 -0
  119. package/package.json +62 -3
  120. package/src/acknowledge.ts +206 -0
  121. package/src/authorize.ts +38 -0
  122. package/src/bind.ts +58 -0
  123. package/src/check.ts +552 -0
  124. package/src/classes.ts +156 -0
  125. package/src/compile.ts +370 -0
  126. package/src/deadline.ts +15 -0
  127. package/src/floor.ts +16 -0
  128. package/src/glob-expand.ts +137 -0
  129. package/src/guards.ts +428 -0
  130. package/src/identity.ts +26 -0
  131. package/src/index.ts +24 -0
  132. package/src/lift.ts +15 -0
  133. package/src/path-policy.ts +280 -0
  134. package/src/processor.ts +187 -0
  135. package/src/reasonable-defaults.json +1662 -0
  136. package/src/reasonable-defaults.ts +8 -0
  137. package/src/run-authorization.ts +23 -0
  138. package/src/tool-path.ts +43 -0
  139. package/src/types.ts +102 -0
  140. package/src/verdict.ts +63 -0
  141. package/README.md +0 -3
package/src/compile.ts ADDED
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Compile a policy.json document into CommandNode + path terms.
3
+ * `"--"` is a handle, not a subcommand: omitted `policy` inherits.
4
+ * Extra keys on a command object are refused — verbs live under `subcommands`.
5
+ */
6
+
7
+ import { ErrorWithInvariant } from "@inhuman.tools/error-with-invariant";
8
+
9
+ import { isClassId, type ClassId } from "./classes.js";
10
+ import { GUARDS } from "./guards.js";
11
+ import { mergePathTerms, normalizePath, type PathTerm } from "./path-policy.js";
12
+ import type { ArgType, CommandNode, Disposition, FlagConstraint, Matcher } from "./types.js";
13
+
14
+ type PathRule = PathTerm["rules"][number];
15
+
16
+ export class PolicyCompileError extends ErrorWithInvariant {
17
+ override readonly name = "PolicyCompileError";
18
+ }
19
+
20
+ export class PolicyAssumeError extends ErrorWithInvariant {
21
+ override readonly name = "PolicyAssumeError";
22
+ readonly expected?: Disposition;
23
+ readonly actual?: Disposition;
24
+ readonly command?: string;
25
+
26
+ constructor(message: string, detail?: { expected: Disposition; actual: Disposition; command: string }) {
27
+ super(message);
28
+ if (detail !== undefined) {
29
+ this.expected = detail.expected;
30
+ this.actual = detail.actual;
31
+ this.command = detail.command;
32
+ }
33
+ }
34
+ }
35
+
36
+ const DISPS = new Set(["policyApprove", "approve", "allow", "ask", "deny", "block"]);
37
+ const RESERVED = new Set([
38
+ "policy",
39
+ "class",
40
+ "family_args",
41
+ "arg_types",
42
+ "arg_rest_type",
43
+ "flag_paths",
44
+ "guard",
45
+ "matcher",
46
+ "--",
47
+ "subcommands",
48
+ ]);
49
+
50
+ type AssumeSmoke = {
51
+ readonly command: string;
52
+ readonly expect: Disposition;
53
+ /** Asserted against the classes of the votes that decided the tier. */
54
+ readonly expectClass?: ClassId;
55
+ };
56
+
57
+ type CompiledDocument = {
58
+ readonly bash?: CommandNode;
59
+ readonly read?: PathTerm;
60
+ readonly write?: PathTerm;
61
+ readonly assume: readonly AssumeSmoke[];
62
+ };
63
+
64
+ export function compilePolicy(document: unknown): CompiledDocument {
65
+ const raw = document;
66
+ if (raw === null || raw === undefined) return { assume: [] };
67
+ PolicyCompileError.invariant(isRecord(raw), "policy.json: document must be a mapping");
68
+ const unknown = Object.keys(raw).filter(
69
+ (key) =>
70
+ !key.startsWith(".") &&
71
+ ![
72
+ "read_file",
73
+ "write_file",
74
+ "read_block",
75
+ "write_block",
76
+ "bash",
77
+ "assume",
78
+ "read_miss_class",
79
+ "write_miss_class",
80
+ ].includes(key),
81
+ );
82
+ PolicyCompileError.invariant(unknown.length === 0, `policy.json: unknown top-level key ${unknown[0]}`);
83
+ const readMiss = parseClass(raw.read_miss_class, "read_miss_class") ?? "envelope-read";
84
+ const writeMiss = parseClass(raw.write_miss_class, "write_miss_class") ?? "envelope-write";
85
+ return {
86
+ bash: raw.bash === undefined ? undefined : parseCommandDef(raw.bash, "bash", true),
87
+ read: mergePathTerms(
88
+ "read",
89
+ readMiss,
90
+ parsePathList(raw.read_file, "read_file", "policyApprove", "envelope-read"),
91
+ parseBlockList(raw.read_block, "read_block", "credential"),
92
+ ),
93
+ write: mergePathTerms(
94
+ "write",
95
+ writeMiss,
96
+ parsePathList(raw.write_file, "write_file", "ask", "workspace-write"),
97
+ parseBlockList(raw.write_block, "write_block", "control-plane"),
98
+ ),
99
+ assume: parseAssume(raw.assume),
100
+ };
101
+ }
102
+
103
+ /** A class id from the closed taxonomy. An unknown id is a compile error, never a new class. */
104
+ function parseClass(value: unknown, path: string): ClassId | undefined {
105
+ if (value === undefined) return undefined;
106
+ PolicyCompileError.invariant(isClassId(value), `policy.json: unknown class ${String(value)} at ${path}`);
107
+ return value;
108
+ }
109
+
110
+ /** A path item is a string, or `{ path, class }` to tag the rule with a dist class. */
111
+ function pathItem(item: unknown, field: string): { readonly pattern: string; readonly class?: ClassId } {
112
+ if (typeof item === "string") return { pattern: item };
113
+ PolicyCompileError.invariant(isRecord(item), `${field}: each pattern is a string or { path, class }`);
114
+ const extra = Object.keys(item).filter((key) => key !== "path" && key !== "class");
115
+ PolicyCompileError.invariant(extra.length === 0, `${field}: unknown key ${extra[0]}`);
116
+ PolicyCompileError.invariant(typeof item.path === "string", `${field}: { path } is a string`);
117
+ return { pattern: item.path, class: parseClass(item.class, `${field}.class`) };
118
+ }
119
+
120
+ function parsePathList(
121
+ value: unknown,
122
+ field: string,
123
+ enable: "policyApprove" | "ask",
124
+ enableClass: ClassId,
125
+ ): PathRule[] | undefined {
126
+ if (value === undefined) return undefined;
127
+ PolicyCompileError.invariant(Array.isArray(value), `${field}: list of strings`);
128
+ return value.map((raw) => {
129
+ const { pattern: item, class: tagged } = pathItem(raw, field);
130
+ PolicyCompileError.invariant(
131
+ !item.startsWith("-"),
132
+ "path pattern: - is not a prefix; ! is deny on the enable list",
133
+ );
134
+ const deny = item.startsWith("!");
135
+ const stem = normalizePath(deny ? item.slice(1) : item);
136
+ PolicyCompileError.invariant(stem !== undefined, "path pattern: empty or unresolvable");
137
+ return {
138
+ id: `${field}:${deny ? "!" : ""}${stem}`,
139
+ polarity: deny ? "deny" : enable,
140
+ stem,
141
+ unanchored: stem !== "." && !stem.startsWith("/") && !stem.includes("/"),
142
+ class: tagged ?? (deny ? "protected-file" : enableClass),
143
+ } satisfies PathRule;
144
+ });
145
+ }
146
+
147
+ function parseBlockList(value: unknown, field: string, defaultClass: ClassId): PathRule[] | undefined {
148
+ if (value === undefined) return undefined;
149
+ PolicyCompileError.invariant(Array.isArray(value), `${field}: list of strings`);
150
+ return value.map((raw) => {
151
+ const { pattern: item, class: tagged } = pathItem(raw, field);
152
+ PolicyCompileError.invariant(
153
+ !item.startsWith("!") && !item.startsWith("-"),
154
+ "block list: stem without a polarity prefix",
155
+ );
156
+ const stem = normalizePath(item);
157
+ PolicyCompileError.invariant(stem !== undefined, "path pattern: empty or unresolvable");
158
+ return {
159
+ id: `${field}:${stem}`,
160
+ polarity: "block" as const,
161
+ stem,
162
+ unanchored: stem !== "." && !stem.startsWith("/") && !stem.includes("/"),
163
+ class: tagged ?? defaultClass,
164
+ } satisfies PathRule;
165
+ });
166
+ }
167
+
168
+ function parseAssume(value: unknown): AssumeSmoke[] {
169
+ if (value === undefined) return [];
170
+ PolicyCompileError.invariant(Array.isArray(value), "assume: list of { command, expect }");
171
+ return value.map((item, i) => {
172
+ PolicyCompileError.invariant(isRecord(item), `assume[${i}]: mapping`);
173
+ PolicyCompileError.invariant(typeof item.command === "string", `assume[${i}]: command is a string`);
174
+ const expect = asDisposition(item.expect);
175
+ PolicyCompileError.invariant(expect !== undefined, `assume[${i}]: expect is approve|ask|deny|block`);
176
+ const expectClass = parseClass(item.expect_class, `assume[${i}].expect_class`);
177
+ return { command: item.command, expect, ...(expectClass === undefined ? {} : { expectClass }) };
178
+ });
179
+ }
180
+
181
+ function parseFamilyArgs(value: unknown, path: string): number {
182
+ if (value === undefined) return 0;
183
+ PolicyCompileError.invariant(
184
+ typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 4,
185
+ `${path}.family_args: integer 0..4`,
186
+ );
187
+ return value;
188
+ }
189
+
190
+ function parseCommandDef(value: unknown, path: string, policyRequired: boolean): CommandNode {
191
+ const disp = asDisposition(value);
192
+ if (disp !== undefined) {
193
+ return { policy: disp, familyArgs: 0, argTypes: [], flagPaths: {}, guards: [], matchers: [], subcommands: {} };
194
+ }
195
+ PolicyCompileError.invariant(isRecord(value), `${path}: command is a disposition or a mapping`);
196
+ const extra = Object.keys(value).filter((key) => !RESERVED.has(key));
197
+ PolicyCompileError.invariant(extra.length === 0, `${path}: unknown key ${extra[0]} — verbs go under subcommands`);
198
+ const policy = value.policy === undefined ? undefined : asDisposition(value.policy);
199
+ PolicyCompileError.invariant(
200
+ value.policy === undefined || policy !== undefined,
201
+ `${path}.policy: approve|ask|deny|block`,
202
+ );
203
+ PolicyCompileError.invariant(!policyRequired || policy !== undefined, `${path}: policy is required`);
204
+ const subcommands: Record<string, CommandNode> = {};
205
+ if (value.subcommands !== undefined) {
206
+ PolicyCompileError.invariant(isRecord(value.subcommands), `${path}.subcommands: mapping`);
207
+ for (const [name, def] of Object.entries(value.subcommands)) {
208
+ subcommands[String(name)] = parseCommandDef(def, `${path}.subcommands.${name}`, true);
209
+ }
210
+ }
211
+ return {
212
+ policy,
213
+ class: parseClass(value.class, `${path}.class`),
214
+ familyArgs: parseFamilyArgs(value.family_args, path),
215
+ argTypes: parseArgTypes(value.arg_types, `${path}.arg_types`),
216
+ argRestType: parseArgType(value.arg_rest_type, `${path}.arg_rest_type`),
217
+ flagPaths: parseFlagPaths(value.flag_paths, `${path}.flag_paths`),
218
+ guards: parseGuards(value.guard, `${path}.guard`),
219
+ matchers: parseMatchers(value.matcher, `${path}.matcher`),
220
+ dash: value["--"] === undefined ? undefined : parseCommandDef(value["--"], `${path}.--`, false),
221
+ subcommands,
222
+ };
223
+ }
224
+
225
+ function parseArgTypes(value: unknown, path: string): ArgType[] {
226
+ if (value === undefined) return [];
227
+ PolicyCompileError.invariant(Array.isArray(value), `${path}: list`);
228
+ return value.map((item, i) => {
229
+ const parsed = parseArgType(item, `${path}[${i}]`);
230
+ PolicyCompileError.invariant(parsed !== undefined, `${path}[${i}]: missing`);
231
+ return parsed;
232
+ });
233
+ }
234
+
235
+ function parseArgType(value: unknown, path: string): ArgType | undefined {
236
+ if (value === undefined) return undefined;
237
+ PolicyCompileError.invariant(value === "read_path" || value === "write_path", `${path}: read_path|write_path`);
238
+ return value;
239
+ }
240
+
241
+ /** `{ "--output": "write_path", "-o": "write_path" }`. Long names also match every unambiguous-prefix spelling. */
242
+ function parseFlagPaths(value: unknown, path: string): Record<string, ArgType> {
243
+ if (value === undefined) return {};
244
+ PolicyCompileError.invariant(isRecord(value), `${path}: mapping of option to read_path|write_path`);
245
+ const out: Record<string, ArgType> = {};
246
+ for (const [name, kind] of Object.entries(value)) {
247
+ PolicyCompileError.invariant(name.startsWith("-"), `${path}.${name}: option name`);
248
+ const parsed = parseArgType(kind, `${path}.${name}`);
249
+ PolicyCompileError.invariant(parsed !== undefined, `${path}.${name}: missing`);
250
+ for (const spelling of withPrefixes([name])) out[spelling] = parsed;
251
+ }
252
+ return out;
253
+ }
254
+
255
+ function parseGuards(value: unknown, path: string): string[] {
256
+ const names = stringList(value, path);
257
+ for (const name of names) PolicyCompileError.invariant(GUARDS.has(name), `${path}: unknown guard ${name}`);
258
+ return names;
259
+ }
260
+
261
+ /** getopt_long accepts any unique prefix of a long option; the policy treats every prefix as the option. */
262
+ function withPrefixes(names: readonly string[]): string[] {
263
+ const out = new Set(names);
264
+ for (const name of names) {
265
+ if (!name.startsWith("--")) continue;
266
+ for (let end = 3; end < name.length; end += 1) out.add(name.slice(0, end));
267
+ }
268
+ return [...out];
269
+ }
270
+
271
+ function parseMatchers(value: unknown, path: string): Matcher[] {
272
+ if (value === undefined) return [];
273
+ PolicyCompileError.invariant(Array.isArray(value), `${path}: list`);
274
+ return value.map((item, i) => {
275
+ const here = `${path}[${i}]`;
276
+ PolicyCompileError.invariant(isRecord(item), `${here}: mapping`);
277
+ const extra = Object.keys(item).filter(
278
+ (key) => !["flags", "glob", "tok", "matcher", "policy", "class"].includes(key),
279
+ );
280
+ PolicyCompileError.invariant(extra.length === 0, `${here}: unknown key ${extra[0]}`);
281
+ const policy = item.policy === undefined ? undefined : asDisposition(item.policy);
282
+ PolicyCompileError.invariant(
283
+ item.policy === undefined || policy !== undefined,
284
+ `${here}.policy: approve|ask|deny|block`,
285
+ );
286
+ return {
287
+ class: parseClass(item.class, `${here}.class`),
288
+ flags: parseFlags(item.flags, `${here}.flags`),
289
+ globs: stringList(item.glob, `${here}.glob`),
290
+ toks: stringList(item.tok, `${here}.tok`),
291
+ matchers: parseMatchers(item.matcher, `${here}.matcher`),
292
+ policy,
293
+ };
294
+ });
295
+ }
296
+
297
+ function parseFlags(value: unknown, path: string): Record<string, FlagConstraint> {
298
+ if (value === undefined) return {};
299
+ PolicyCompileError.invariant(isRecord(value), `${path}: mapping`);
300
+ const out: Record<string, FlagConstraint> = {};
301
+ for (const [raw, spec] of Object.entries(value)) {
302
+ const canonical = String(raw);
303
+ out[canonical] = parseFlagConstraint(spec, canonical, `${path}.${canonical}`);
304
+ }
305
+ return out;
306
+ }
307
+
308
+ function parseFlagConstraint(value: unknown, canonical: string, path: string): FlagConstraint {
309
+ if (value === true || value === null) {
310
+ return { names: [canonical], vals: [], takesValue: false };
311
+ }
312
+ PolicyCompileError.invariant(isRecord(value), `${path}: true or { alias, value, value_type }`);
313
+ const extra = Object.keys(value).filter((key) => !["alias", "value", "value_type", "abbrev"].includes(key));
314
+ PolicyCompileError.invariant(extra.length === 0, `${path}: unknown key ${extra[0]}`);
315
+ const aliases = stringList(value.alias, `${path}.alias`);
316
+ const vals = stringList(value.value, `${path}.value`);
317
+ const takesValue = vals.length > 0 || value.value_type !== undefined;
318
+ if (value.value_type !== undefined) {
319
+ PolicyCompileError.invariant(typeof value.value_type === "string", `${path}.value_type: string`);
320
+ }
321
+ PolicyCompileError.invariant(
322
+ value.abbrev === undefined || typeof value.abbrev === "boolean",
323
+ `${path}.abbrev: boolean`,
324
+ );
325
+ const names = [canonical, ...aliases];
326
+ return { names: value.abbrev === true ? withPrefixes(names) : names, vals, takesValue };
327
+ }
328
+
329
+ function stringList(value: unknown, path: string): string[] {
330
+ if (value === undefined) return [];
331
+ if (typeof value === "string") return [value];
332
+ PolicyCompileError.invariant(Array.isArray(value), `${path}: string or list of strings`);
333
+ return value.map((item, i) => {
334
+ PolicyCompileError.invariant(typeof item === "string", `${path}[${i}]: string`);
335
+ return item;
336
+ });
337
+ }
338
+
339
+ function asDisposition(value: unknown): Disposition | undefined {
340
+ if (typeof value !== "string" || !DISPS.has(value)) return undefined;
341
+ if (value === "allow" || value === "approve" || value === "policyApprove") return "policyApprove";
342
+ return value as Disposition;
343
+ }
344
+
345
+ export function flagVocab(node: CommandNode): Set<string> {
346
+ const names = new Set<string>();
347
+ addMatcherFlags(node.matchers, names, false);
348
+ return names;
349
+ }
350
+
351
+ export function valueFlagNames(node: CommandNode): Set<string> {
352
+ const names = new Set<string>();
353
+ addMatcherFlags(node.matchers, names, true);
354
+ for (const name of Object.keys(node.flagPaths)) names.add(name);
355
+ return names;
356
+ }
357
+
358
+ function addMatcherFlags(matchers: readonly Matcher[], names: Set<string>, valueOnly: boolean): void {
359
+ for (const matcher of matchers) {
360
+ for (const flag of Object.values(matcher.flags)) {
361
+ if (valueOnly && !flag.takesValue) continue;
362
+ for (const name of flag.names) names.add(name);
363
+ }
364
+ addMatcherFlags(matcher.matchers, names, valueOnly);
365
+ }
366
+ }
367
+
368
+ function isRecord(value: unknown): value is Record<string, unknown> {
369
+ return value !== null && typeof value === "object" && !Array.isArray(value);
370
+ }
@@ -0,0 +1,15 @@
1
+ /** Judge budget shared by the adapters. Crush's hook self-bounds at the same 20 s. */
2
+ export const JUDGE_DEADLINE_MS = 20_000;
3
+
4
+ /** Reject when `work` has not settled in `ms`; the caller maps a rejection to deny. */
5
+ export async function withDeadline<T>(work: Promise<T>, ms: number = JUDGE_DEADLINE_MS): Promise<T> {
6
+ let timer: ReturnType<typeof setTimeout> | undefined;
7
+ const expired = new Promise<never>((_resolve, reject) => {
8
+ timer = setTimeout(() => reject(new Error("authorization timed out")), ms);
9
+ });
10
+ try {
11
+ return await Promise.race([work, expired]);
12
+ } finally {
13
+ if (timer !== undefined) clearTimeout(timer);
14
+ }
15
+ }
package/src/floor.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { bindPolicy } from "./bind.js";
2
+ export { CLASS_IDS, CLASSES, isClassId, TIER_NAMES, type ClassId, type ClassInfo } from "./classes.js";
3
+ export { whichOnPath, type Which } from "./identity.js";
4
+ export { lattice } from "./lift.js";
5
+ export { classifierResources, policyEnvCapability, processorCapability } from "./processor.js";
6
+ export { reasonableDefaultsSource } from "./reasonable-defaults.js";
7
+ export {
8
+ classesOf,
9
+ type Disposition,
10
+ type FloorVerdict,
11
+ type FloorVote,
12
+ type LiftKind,
13
+ type Policy,
14
+ type UnitIdentity,
15
+ } from "./types.js";
16
+ export { APPROVE, cast, join } from "./verdict.js";
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Lexical brace expansion (`.e{nv,x}`, `a{1..3}`, `{a,b}{c,d}`), the way bash does it before
3
+ * globbing. Used to judge a shell word as every word it could become. Bounded: past `limit`
4
+ * words the caller falls back to a wildcard superset.
5
+ */
6
+
7
+ const DEFAULT_LIMIT = 256;
8
+
9
+ type Group = { readonly start: number; readonly end: number; readonly items: readonly string[] };
10
+
11
+ export function expandBraces(
12
+ word: string,
13
+ limit: number = DEFAULT_LIMIT,
14
+ ): { readonly words: readonly string[]; readonly overflow: boolean } {
15
+ const done: string[] = [];
16
+ const work: string[] = [word];
17
+ while (work.length > 0) {
18
+ const next = work.pop()!;
19
+ const group = firstGroup(next);
20
+ if (group === undefined) {
21
+ done.push(next);
22
+ } else {
23
+ const head = next.slice(0, group.start);
24
+ const tail = next.slice(group.end + 1);
25
+ for (const item of group.items) work.push(`${head}${item}${tail}`);
26
+ }
27
+ if (done.length + work.length > limit) return { words: [], overflow: true };
28
+ }
29
+ return { words: done.toReversed(), overflow: false };
30
+ }
31
+
32
+ /** The first top-level `{...}` that bash would expand (a comma list or a range), or undefined. */
33
+ function firstGroup(text: string): Group | undefined {
34
+ for (let start = 0; start < text.length; start += 1) {
35
+ if (text[start] === "\\") {
36
+ start += 1;
37
+ continue;
38
+ }
39
+ if (text[start] !== "{") continue;
40
+ const end = matchingBrace(text, start);
41
+ if (end === undefined) continue;
42
+ const body = text.slice(start + 1, end);
43
+ const items = commaItems(body) ?? rangeItems(body);
44
+ if (items !== undefined) return { start, end, items };
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ function matchingBrace(text: string, start: number): number | undefined {
50
+ let depth = 0;
51
+ for (let i = start; i < text.length; i += 1) {
52
+ const ch = text[i];
53
+ switch (ch) {
54
+ case "\\": {
55
+ i += 1;
56
+
57
+ break;
58
+ }
59
+ case "{": {
60
+ depth += 1;
61
+
62
+ break;
63
+ }
64
+ case "}": {
65
+ depth -= 1;
66
+ if (depth === 0) return i;
67
+
68
+ break;
69
+ }
70
+ // No default
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ /** Items of a top-level comma list. Undefined when there is no top-level comma. */
77
+ function commaItems(body: string): string[] | undefined {
78
+ const items: string[] = [];
79
+ let depth = 0;
80
+ let from = 0;
81
+ for (let i = 0; i < body.length; i += 1) {
82
+ const ch = body[i];
83
+ switch (ch) {
84
+ case "\\": {
85
+ i += 1;
86
+ break;
87
+ }
88
+ case "{": {
89
+ depth += 1;
90
+ break;
91
+ }
92
+ case "}": {
93
+ depth -= 1;
94
+ break;
95
+ }
96
+ default:
97
+ if (ch === "," && depth === 0) {
98
+ items.push(body.slice(from, i));
99
+ from = i + 1;
100
+ }
101
+ }
102
+ }
103
+ if (items.length === 0) return undefined;
104
+ items.push(body.slice(from));
105
+ return items;
106
+ }
107
+
108
+ const RANGE = /^(-?\d+|[A-Z])\.\.(-?\d+|[A-Z])(?:\.\.(-?\d+))?$/i;
109
+ const MAX_RANGE = 1024;
110
+
111
+ function rangeItems(body: string): string[] | undefined {
112
+ const match = RANGE.exec(body);
113
+ if (match === null) return undefined;
114
+ const [, a, b, stepText] = match;
115
+ const step = Math.max(1, Math.abs(Number(stepText ?? "1")) || 1);
116
+ const out: string[] = [];
117
+ if (/^-?\d+$/.test(a!) && /^-?\d+$/.test(b!)) {
118
+ const from = Number(a);
119
+ const to = Number(b);
120
+ const width =
121
+ /^-?0\d/.test(a!) || /^-?0\d/.test(b!) ? Math.max(a!.replace("-", "").length, b!.replace("-", "").length) : 0;
122
+ if (Math.abs(to - from) / step > MAX_RANGE) return Array.from({ length: MAX_RANGE + 1 }, (_, i) => String(i));
123
+ for (let n = from; from <= to ? n <= to : n >= to; n += from <= to ? step : -step) {
124
+ const text = String(Math.abs(n)).padStart(width, "0");
125
+ out.push(n < 0 ? `-${text}` : text);
126
+ }
127
+ return out;
128
+ }
129
+ if (a!.length === 1 && b!.length === 1 && !/\d/.test(a! + b!)) {
130
+ const from = a!.codePointAt(0)!;
131
+ const to = b!.codePointAt(0)!;
132
+ for (let c = from; from <= to ? c <= to : c >= to; c += from <= to ? step : -step)
133
+ out.push(String.fromCodePoint(c));
134
+ return out;
135
+ }
136
+ return undefined;
137
+ }