@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/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@inhuman.tools/policy",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "JSON floor and the authorization program. The caller supplies the decide capability.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./floor": {
15
+ "types": "./dist/floor.d.ts",
16
+ "import": "./dist/floor.js",
17
+ "default": "./dist/floor.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src",
23
+ "authorize.scm",
24
+ "!**/__tests__"
25
+ ],
26
+ "dependencies": {
27
+ "@here.build/arrival": "^0.10.2",
28
+ "minimatch": "10.2.6",
29
+ "zod": "^4.4.3",
30
+ "@inhuman.tools/error-with-invariant": "^0.1.0",
31
+ "@inhuman.tools/policy-bash-disassembler": "^0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^24.0.0",
35
+ "eslint": "9.39.2",
36
+ "typescript": "6.0.2",
37
+ "vitest": "4.1.0",
38
+ "@-internal-/eslint-configs": "0.0.0",
39
+ "@inhuman.tools/env-capability-decision-structured": "^0.0.1"
40
+ },
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/here-build/inhuman-harness.git",
45
+ "directory": "packages-policy/policy"
46
+ },
47
+ "browserslist": [
48
+ "last 2 chrome versions",
49
+ "last 2 firefox versions",
50
+ "last 2 safari versions",
51
+ "ios_saf >= 16.4",
52
+ "node >= 22"
53
+ ],
54
+ "author": "Merkle Bonsai",
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.build.json",
60
+ "lint": "eslint .",
61
+ "lint:fix": "eslint . --fix",
62
+ "test": "tsc -p tsconfig.json --noEmit && vitest run",
63
+ "typecheck": "tsc -p tsconfig.json --noEmit"
64
+ }
6
65
  }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * `needsAcknowledgement` for the adapters. One place for the flag schema, the matcher and the texts;
3
+ * an adapter only asks "does this call need an acknowledgement" and translates the answer into its
4
+ * host's confirmation mechanism.
5
+ *
6
+ * A flag forces a request for a call that would otherwise run (floor approve, judge allow, any
7
+ * resolution to allow). It never turns a block or a denial into anything. Flags only tighten:
8
+ * the effective set is the union of the user and project layers, so a project file can add and
9
+ * can never remove a user flag. The dist ships none.
10
+ *
11
+ * Adapters have no `modes` of their own, so `modes` is accepted (the schema is the daemon's) and
12
+ * ignored: a flag applies in every adapter.
13
+ *
14
+ * `class` matches the classes of the floor votes (`classesOf`). An approve has no classes, so a
15
+ * class flag only matches a call whose floor tier is ask or deny. `command` matches a prefix of
16
+ * `[argv0, ...spine, ...familyArgs]` of any shell unit in `verdict.seen`, so approved calls match.
17
+ */
18
+ import { z } from "zod";
19
+
20
+ import { defaultPolicy } from "./bind.js";
21
+ import { CLASS_IDS, type ClassId } from "./classes.js";
22
+ import { lattice } from "./lift.js";
23
+ import { normalizeToolPath } from "./tool-path.js";
24
+ import { classesOf, type FloorVerdict } from "./types.js";
25
+ import { join } from "./verdict.js";
26
+
27
+ const classIdSchema = z.enum(CLASS_IDS);
28
+
29
+ const matchSchema = z.union([
30
+ z.strictObject({ class: classIdSchema }),
31
+ z.strictObject({ mcp: z.string().min(1), account: z.string().min(1).optional(), tool: z.string().min(1).optional() }),
32
+ z.strictObject({ tool: z.string().min(1) }),
33
+ z.strictObject({ command: z.array(z.string().min(1)).min(1) }),
34
+ ]);
35
+
36
+ const flagSchema = z.strictObject({
37
+ match: matchSchema,
38
+ why: z.string().max(200).optional(),
39
+ modes: z.array(z.enum(["manual", "auto", "yolo"])).optional(),
40
+ });
41
+
42
+ export type AckMatch =
43
+ | { readonly class: ClassId }
44
+ | { readonly mcp: string; readonly account?: string; readonly tool?: string }
45
+ | { readonly tool: string }
46
+ | { readonly command: readonly string[] };
47
+
48
+ export type AckFlag = {
49
+ readonly match: AckMatch;
50
+ readonly why?: string;
51
+ readonly modes?: readonly ("manual" | "auto" | "yolo")[];
52
+ };
53
+
54
+ /** Throws a readable error for an invalid block; `undefined` is no flags. */
55
+ export function parseAcknowledge(value: unknown): readonly AckFlag[] {
56
+ if (value === undefined) return [];
57
+ const parsed = z.array(flagSchema).safeParse(value);
58
+ if (!parsed.success) {
59
+ const issue = parsed.error.issues[0];
60
+ throw new Error(
61
+ `invalid acknowledge config: ${issue === undefined ? "bad shape" : `${issue.path.join(".") || "(root)"}: ${issue.message}`}`,
62
+ );
63
+ }
64
+ return parsed.data as readonly AckFlag[];
65
+ }
66
+
67
+ /** A config file's text: a JSON array of flags, or an object with an `acknowledge` array. Empty text is none. */
68
+ export function parseAcknowledgeText(text: string | undefined): readonly AckFlag[] {
69
+ if (text === undefined || text.trim() === "") return [];
70
+ let raw: unknown;
71
+ try {
72
+ raw = JSON.parse(text);
73
+ } catch {
74
+ throw new Error("invalid acknowledge config: not json");
75
+ }
76
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
77
+ return parseAcknowledge((raw as Record<string, unknown>).acknowledge);
78
+ }
79
+ return parseAcknowledge(raw);
80
+ }
81
+
82
+ /** Layer rule: the union. Project can add; nothing in it removes a user flag. */
83
+ export function mergeAcknowledge(...layers: readonly (readonly AckFlag[] | undefined)[]): readonly AckFlag[] {
84
+ const out = new Map<string, AckFlag>();
85
+ for (const layer of layers) for (const flag of layer ?? []) out.set(JSON.stringify([flag.match, flag.why]), flag);
86
+ return [...out.values()];
87
+ }
88
+
89
+ /** `mcp__server__tool` is the common MCP tool-name shape; hosts that spell it differently pass `mcp` themselves. */
90
+ export function mcpOf(toolName: string): { readonly server: string; readonly tool: string } | undefined {
91
+ const found = /^mcp__(.+?)__(.+)$/.exec(toolName);
92
+ return found?.[1] === undefined || found[2] === undefined ? undefined : { server: found[1], tool: found[2] };
93
+ }
94
+
95
+ export type AckSubject = {
96
+ readonly tool?: string;
97
+ readonly mcp?: { readonly server: string; readonly account?: string; readonly tool?: string };
98
+ readonly verdict?: FloorVerdict;
99
+ };
100
+
101
+ export type AckRequirement = {
102
+ readonly labels: readonly string[];
103
+ readonly why: readonly string[];
104
+ };
105
+
106
+ export function labelOf(match: AckMatch): string {
107
+ if ("class" in match) return `class ${match.class}`;
108
+ if ("command" in match) return `command ${match.command.join(" ")}`;
109
+ if ("mcp" in match) {
110
+ return `mcp ${[match.mcp, match.tool].filter((part) => part !== undefined).join("/")}`;
111
+ }
112
+ return `tool ${match.tool}`;
113
+ }
114
+
115
+ function matches(match: AckMatch, subject: AckSubject): boolean {
116
+ if ("class" in match) return subject.verdict !== undefined && classesOf(subject.verdict).includes(match.class);
117
+ if ("command" in match) {
118
+ const units = subject.verdict?.seen ?? [];
119
+ return units.some((unit) => {
120
+ const identity = [unit.argv0, ...unit.spine, ...unit.familyArgs];
121
+ return match.command.length <= identity.length && match.command.every((word, i) => identity[i] === word);
122
+ });
123
+ }
124
+ const mcp = subject.mcp ?? (subject.tool === undefined ? undefined : mcpOf(subject.tool));
125
+ if ("mcp" in match) {
126
+ return (
127
+ mcp !== undefined &&
128
+ mcp.server === match.mcp &&
129
+ (match.tool === undefined || mcp.tool === match.tool) &&
130
+ (match.account === undefined || ("account" in mcp && mcp.account === match.account))
131
+ );
132
+ }
133
+ return subject.tool === match.tool || (mcp !== undefined && mcp.tool === match.tool);
134
+ }
135
+
136
+ /** Null when no flag matches. */
137
+ export function acknowledgeRequirement(flags: readonly AckFlag[], subject: AckSubject): AckRequirement | null {
138
+ const labels: string[] = [];
139
+ const why: string[] = [];
140
+ for (const flag of flags) {
141
+ if (!matches(flag.match, subject)) continue;
142
+ const label = labelOf(flag.match);
143
+ if (!labels.includes(label)) labels.push(label);
144
+ if (flag.why !== undefined && !why.includes(flag.why)) why.push(flag.why);
145
+ }
146
+ return labels.length === 0 ? null : { labels, why };
147
+ }
148
+
149
+ /** Strictest floor verdict for a call, with the host path preparation the adapters use. */
150
+ export function floorVerdictOf(
151
+ call:
152
+ | { readonly kind: "bash"; readonly command: string; readonly cwd?: string }
153
+ | {
154
+ readonly kind: "read" | "write";
155
+ readonly path: string;
156
+ readonly cwd?: string;
157
+ },
158
+ ): FloorVerdict {
159
+ if (call.kind === "bash") return defaultPolicy.explainBash(call.command, undefined);
160
+ const prepared = normalizeToolPath(call.path, call.cwd);
161
+ const explain = call.kind === "read" ? defaultPolicy.explainRead : defaultPolicy.explainWrite;
162
+ const first = explain(prepared);
163
+ const second = explain(prepared.toLowerCase());
164
+ const top = lattice([first.disposition, second.disposition]);
165
+ return join([first, second].filter((verdict) => verdict.disposition === top));
166
+ }
167
+
168
+ export function blockReason(requirement: AckRequirement): string {
169
+ return `needs acknowledgement: ${requirement.labels.join(", ")}`;
170
+ }
171
+
172
+ export type AckRequest = { readonly summary: string; readonly detail: string; readonly why?: string };
173
+ export type Acknowledger = (request: AckRequest) => Promise<boolean>;
174
+
175
+ const DETAIL_LIMIT = 500;
176
+
177
+ /** `text` is what the agent wrote (a command, a path, arguments). It is JSON-encoded and truncated. */
178
+ export function acknowledgeRequest(requirement: AckRequirement, tool: string, text: string): AckRequest {
179
+ const encoded = JSON.stringify(text);
180
+ const detail = encoded.length > DETAIL_LIMIT ? `${encoded.slice(0, DETAIL_LIMIT)}...` : encoded;
181
+ const why = requirement.why.length === 0 ? undefined : requirement.why.join("; ");
182
+ return {
183
+ summary: `Acknowledge ${tool}: ${requirement.labels.join(", ")}`,
184
+ detail,
185
+ ...(why === undefined ? {} : { why }),
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Ask the host. Undefined means acknowledged and the call may run; a string is the denial reason.
191
+ * No `acknowledger` is the degraded path: the reason is `needs acknowledgement: <label>`.
192
+ */
193
+ export async function requestAcknowledgement(
194
+ requirement: AckRequirement,
195
+ tool: string,
196
+ text: string,
197
+ acknowledger: Acknowledger | undefined,
198
+ ): Promise<string | undefined> {
199
+ if (acknowledger === undefined) return blockReason(requirement);
200
+ try {
201
+ const accepted = await acknowledger(acknowledgeRequest(requirement, tool, text));
202
+ return accepted ? undefined : `acknowledgement declined: ${requirement.labels.join(", ")}`;
203
+ } catch {
204
+ return `acknowledgement failed: ${requirement.labels.join(", ")}`;
205
+ }
206
+ }
@@ -0,0 +1,38 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { ErrorWithInvariant } from "@inhuman.tools/error-with-invariant";
6
+
7
+ export class DecideError extends ErrorWithInvariant {
8
+ override readonly name = "DecideError";
9
+ }
10
+
11
+ const programPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../authorize.scm");
12
+
13
+ let cached: string | undefined;
14
+
15
+ export function authorizationProgram(): string {
16
+ if (cached !== undefined) return cached;
17
+ cached = readFileSync(programPath, "utf8");
18
+ return cached;
19
+ }
20
+
21
+ /** A set `POLICY_AUTHORIZE` that cannot be read is an error, not the dist script. */
22
+ export function authorizationSource(override?: string, env?: NodeJS.ProcessEnv): string {
23
+ const inline = override?.trim();
24
+ if (inline) return inline;
25
+ const file = env?.POLICY_AUTHORIZE?.trim();
26
+ if (file === undefined || file === "") return authorizationProgram();
27
+ let text: string;
28
+ try {
29
+ text = readFileSync(file, "utf8");
30
+ } catch (error) {
31
+ const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
32
+ if (code === "ENOENT") throw new DecideError(`authorization program not found: ${file}`);
33
+ throw error;
34
+ }
35
+ const source = text.trim();
36
+ if (source === "") throw new DecideError("authorization program is empty");
37
+ return text;
38
+ }
package/src/bind.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { explainBash } from "./check.js";
2
+ import { compilePolicy, PolicyAssumeError } from "./compile.js";
3
+ import type { Which } from "./identity.js";
4
+ import { explainPath, writeRemainderKind } from "./path-policy.js";
5
+ import { reasonableDefaults } from "./reasonable-defaults.js";
6
+ import { classesOf, type FloorVerdict, type Policy } from "./types.js";
7
+ import { cast } from "./verdict.js";
8
+
9
+ const unresolved: Which = () => undefined;
10
+
11
+ export function bindPolicy(document: unknown, which: Which = unresolved): Policy {
12
+ const compiled = compilePolicy(document);
13
+ const clusters = { read: compiled.read, write: compiled.write };
14
+ const bash = (command: string, cwd?: string): FloorVerdict =>
15
+ compiled.bash === undefined
16
+ ? cast("ask", "unmodelled", "bash:no-policy")
17
+ : explainBash(compiled.bash, command, clusters, which, cwd);
18
+ for (const smoke of compiled.assume) {
19
+ const verdict = bash(smoke.command);
20
+ const actual = verdict.disposition;
21
+ PolicyAssumeError.invariant(
22
+ actual === smoke.expect,
23
+ `assume: expected ${smoke.expect}, got ${actual}: ${smoke.command}`,
24
+ {
25
+ expected: smoke.expect,
26
+ actual,
27
+ command: smoke.command,
28
+ },
29
+ );
30
+ if (smoke.expectClass !== undefined) {
31
+ const classes = classesOf(verdict);
32
+ PolicyAssumeError.invariant(
33
+ classes.includes(smoke.expectClass),
34
+ `assume: expected class ${smoke.expectClass}, got ${classes.join(",") || "none"}: ${smoke.command}`,
35
+ { expected: smoke.expect, actual, command: smoke.command },
36
+ );
37
+ }
38
+ }
39
+ const path = (cluster: "read" | "write") => {
40
+ const term = cluster === "read" ? compiled.read : compiled.write;
41
+ return (raw: string): FloorVerdict =>
42
+ term === undefined ? cast("ask", "unmodelled", `${cluster}:no-policy`) : explainPath(term, raw);
43
+ };
44
+ const explainRead = path("read");
45
+ const explainWrite = path("write");
46
+ return {
47
+ bash: (command: string, cwd?: string) => bash(command, cwd).disposition,
48
+ explainBash: bash,
49
+ read: (raw: string) => explainRead(raw).disposition,
50
+ explainRead,
51
+ write: (raw: string) => explainWrite(raw).disposition,
52
+ explainWrite,
53
+ writeKind: (raw: string) => (compiled.write === undefined ? null : writeRemainderKind(compiled.write, raw)),
54
+ };
55
+ }
56
+
57
+ /** Dist floor. A path-form argv0 stays a path: this binding has no `which`. */
58
+ export const defaultPolicy: Policy = bindPolicy(reasonableDefaults);