@inhuman.tools/policy 0.0.0-stage → 0.2.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 (106) hide show
  1. package/LICENSE.md +7 -0
  2. package/authorize.scm +4 -0
  3. package/dist/acknowledge.d.ts +70 -0
  4. package/dist/acknowledge.d.ts.map +1 -0
  5. package/dist/acknowledge.js +162 -0
  6. package/dist/acknowledge.js.map +1 -0
  7. package/dist/authorize.d.ts +8 -0
  8. package/dist/authorize.d.ts.map +1 -0
  9. package/dist/authorize.js +39 -0
  10. package/dist/authorize.js.map +1 -0
  11. package/dist/bind.d.ts +6 -0
  12. package/dist/bind.d.ts.map +1 -0
  13. package/dist/bind.js +45 -0
  14. package/dist/bind.js.map +1 -0
  15. package/dist/check.d.ts +30 -0
  16. package/dist/check.d.ts.map +1 -0
  17. package/dist/check.js +389 -0
  18. package/dist/check.js.map +1 -0
  19. package/dist/classes.d.ts +23 -0
  20. package/dist/classes.d.ts.map +1 -0
  21. package/dist/classes.js +139 -0
  22. package/dist/classes.js.map +1 -0
  23. package/dist/compile.d.ts +40 -0
  24. package/dist/compile.d.ts.map +1 -0
  25. package/dist/compile.js +311 -0
  26. package/dist/compile.js.map +1 -0
  27. package/dist/deadline.d.ts +5 -0
  28. package/dist/deadline.d.ts.map +1 -0
  29. package/dist/deadline.js +17 -0
  30. package/dist/deadline.js.map +1 -0
  31. package/dist/floor.d.ts +9 -0
  32. package/dist/floor.d.ts.map +1 -0
  33. package/dist/floor.js +9 -0
  34. package/dist/floor.js.map +1 -0
  35. package/dist/glob-expand.d.ts +10 -0
  36. package/dist/glob-expand.d.ts.map +1 -0
  37. package/dist/glob-expand.js +132 -0
  38. package/dist/glob-expand.js.map +1 -0
  39. package/dist/guards.d.ts +26 -0
  40. package/dist/guards.d.ts.map +1 -0
  41. package/dist/guards.js +419 -0
  42. package/dist/guards.js.map +1 -0
  43. package/dist/identity.d.ts +11 -0
  44. package/dist/identity.d.ts.map +1 -0
  45. package/dist/identity.js +23 -0
  46. package/dist/identity.js.map +1 -0
  47. package/dist/index.d.ts +8 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +8 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/lift.d.ts +4 -0
  52. package/dist/lift.d.ts.map +1 -0
  53. package/dist/lift.js +14 -0
  54. package/dist/lift.js.map +1 -0
  55. package/dist/path-policy.d.ts +53 -0
  56. package/dist/path-policy.d.ts.map +1 -0
  57. package/dist/path-policy.js +248 -0
  58. package/dist/path-policy.js.map +1 -0
  59. package/dist/processor.d.ts +71 -0
  60. package/dist/processor.d.ts.map +1 -0
  61. package/dist/processor.js +132 -0
  62. package/dist/processor.js.map +1 -0
  63. package/dist/reasonable-defaults.d.ts +1448 -0
  64. package/dist/reasonable-defaults.d.ts.map +1 -0
  65. package/dist/reasonable-defaults.js +6 -0
  66. package/dist/reasonable-defaults.js.map +1 -0
  67. package/dist/reasonable-defaults.json +1662 -0
  68. package/dist/run-authorization.d.ts +8 -0
  69. package/dist/run-authorization.d.ts.map +1 -0
  70. package/dist/run-authorization.js +16 -0
  71. package/dist/run-authorization.js.map +1 -0
  72. package/dist/tool-path.d.ts +8 -0
  73. package/dist/tool-path.d.ts.map +1 -0
  74. package/dist/tool-path.js +38 -0
  75. package/dist/tool-path.js.map +1 -0
  76. package/dist/types.d.ts +87 -0
  77. package/dist/types.d.ts.map +1 -0
  78. package/dist/types.js +14 -0
  79. package/dist/types.js.map +1 -0
  80. package/dist/verdict.d.ts +15 -0
  81. package/dist/verdict.d.ts.map +1 -0
  82. package/dist/verdict.js +55 -0
  83. package/dist/verdict.js.map +1 -0
  84. package/package.json +62 -3
  85. package/src/acknowledge.ts +206 -0
  86. package/src/authorize.ts +38 -0
  87. package/src/bind.ts +58 -0
  88. package/src/check.ts +552 -0
  89. package/src/classes.ts +156 -0
  90. package/src/compile.ts +370 -0
  91. package/src/deadline.ts +15 -0
  92. package/src/floor.ts +16 -0
  93. package/src/glob-expand.ts +137 -0
  94. package/src/guards.ts +428 -0
  95. package/src/identity.ts +26 -0
  96. package/src/index.ts +24 -0
  97. package/src/lift.ts +15 -0
  98. package/src/path-policy.ts +280 -0
  99. package/src/processor.ts +187 -0
  100. package/src/reasonable-defaults.json +1662 -0
  101. package/src/reasonable-defaults.ts +8 -0
  102. package/src/run-authorization.ts +23 -0
  103. package/src/tool-path.ts +43 -0
  104. package/src/types.ts +102 -0
  105. package/src/verdict.ts +63 -0
  106. package/README.md +0 -3
@@ -0,0 +1,8 @@
1
+ /** Dist floor. Sole home is `reasonable-defaults.json`. The string is the session file body. */
2
+
3
+ import defaults from "./reasonable-defaults.json" with { type: "json" };
4
+
5
+ export const reasonableDefaults = defaults;
6
+
7
+ /** Text of {@link reasonableDefaults} for `policyProgram`. `bindPolicy` takes the object. */
8
+ export const reasonableDefaultsSource: string = JSON.stringify(reasonableDefaults);
@@ -0,0 +1,23 @@
1
+ import { execState, toJS, type EnvCapability } from "@here.build/arrival";
2
+
3
+ import { authorizationSource } from "./authorize.js";
4
+
5
+ export async function authorize(
6
+ document: string,
7
+ options: {
8
+ readonly capability: EnvCapability;
9
+ readonly config?: Readonly<Record<string, unknown>>;
10
+ readonly program?: string;
11
+ readonly env?: NodeJS.ProcessEnv;
12
+ },
13
+ ): Promise<"allow" | "deny" | null> {
14
+ const state = await execState(authorizationSource(options.program, options.env), {
15
+ capabilities: [options.capability],
16
+ config: { ...options.config, document },
17
+ });
18
+ const value = state.values.at(0);
19
+ if (value === undefined) return null;
20
+ const decided = toJS(value);
21
+ if (decided === "allow" || decided === "deny") return decided;
22
+ return null;
23
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Host path preparation for the file floor, for hosts that hand the model's path
3
+ * string straight to the adapter. Mirrors the harness's `workspacePath`: resolve
4
+ * against the working directory, collapse `.` / `..`, make in-root paths relative,
5
+ * leave extra-root paths absolute (so a miss is `deny`, never `..` ask).
6
+ * Lexical only: no realpath, so a symlink inside the tree is not followed.
7
+ * The floor matches case-sensitively, so `checkToolPath` also asks about the
8
+ * lower-cased path and keeps the stricter answer (`.GIT/hooks` is `.git/hooks`
9
+ * on APFS and NTFS).
10
+ */
11
+ import { homedir } from "node:os";
12
+ import path from "node:path";
13
+
14
+ import { defaultPolicy } from "./bind.js";
15
+ import { lattice } from "./lift.js";
16
+ import type { Disposition } from "./types.js";
17
+
18
+ function slash(text: string): string {
19
+ return text.replaceAll("\\", "/");
20
+ }
21
+
22
+ export function normalizeToolPath(raw: string, cwd: string = process.cwd(), home: string = homedir()): string {
23
+ let written = slash(raw.trim() === "" ? "." : raw.trim());
24
+ if (written === "~" || written.startsWith("~/")) written = `${slash(home)}${written.slice(1)}`;
25
+ const root = path.posix.normalize(slash(cwd.startsWith("/") || /^[a-z]:/i.test(cwd) ? cwd : `/${cwd}`));
26
+ const resolved = path.posix.normalize(written.startsWith("/") ? written : path.posix.join(root, written));
27
+ const trimmed = resolved.length > 1 && resolved.endsWith("/") ? resolved.slice(0, -1) : resolved;
28
+ const rel = path.posix.relative(root, trimmed);
29
+ if (rel === "") return ".";
30
+ if (rel === ".." || rel.startsWith("../") || rel.startsWith("/")) return trimmed;
31
+ return rel;
32
+ }
33
+
34
+ /** Strictest dist-floor answer for a model-supplied path, after host normalisation. */
35
+ export function checkToolPath(
36
+ operation: "read" | "write",
37
+ raw: string,
38
+ options?: { readonly cwd?: string; readonly home?: string },
39
+ ): Disposition {
40
+ const prepared = normalizeToolPath(raw, options?.cwd, options?.home);
41
+ const check = operation === "read" ? defaultPolicy.read : defaultPolicy.write;
42
+ return lattice([check(prepared), check(prepared.toLowerCase())]);
43
+ }
package/src/types.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Floor carriers. `policyApprove` is the positive vote. Stored `approve` and `allow` parse as it.
3
+ * `block` is unliftable. `deny` is overridable by descending arms.
4
+ * File effects (`read_path` / `write_path`) join on their own lattice, apart from bash
5
+ * shape — they cannot lift `ask`.
6
+ */
7
+
8
+ import type { ClassId } from "./classes.js";
9
+
10
+ export type Disposition = "policyApprove" | "ask" | "deny" | "block";
11
+
12
+ /** Write-deny remainder intern. Null when the floor is not deny. */
13
+ export type WriteRemainderKind = "protected" | "outside" | "risky";
14
+
15
+ export type ArgType = "read_path" | "write_path";
16
+
17
+ /** Why an approve became ask without a rule saying so. A lifted vote is never grantable. */
18
+ export type LiftKind = "env" | "xargs" | "nonliteral" | "unparsed" | "overflow" | "readings";
19
+
20
+ /** Grant family of one shell unit: program, recognized subcommand spine, `family_args` positionals. */
21
+ export type UnitIdentity = {
22
+ readonly argv0: string;
23
+ readonly spine: readonly string[];
24
+ readonly familyArgs: readonly string[];
25
+ };
26
+
27
+ /** One rule's vote, with the class that says why it voted. Only votes at the call's tier are reported. */
28
+ export type FloorVote = {
29
+ readonly disposition: Disposition;
30
+ readonly class: ClassId;
31
+ /** Stable rule identity: `bash:git/push[--force]`, `write_block:.git/hooks`, `lift:env`. */
32
+ readonly ruleId: string;
33
+ /** Shell votes only: the unit that voted. Absent for path and host votes. */
34
+ readonly unit?: UnitIdentity;
35
+ readonly lift?: LiftKind;
36
+ /** Path votes only: the prepared path and the cluster it was judged in. */
37
+ readonly path?: { readonly cluster: "read" | "write"; readonly path: string };
38
+ };
39
+
40
+ /** The tier and the votes that decided it. `votes` is empty for `policyApprove`. */
41
+ export type FloorVerdict = {
42
+ readonly disposition: Disposition;
43
+ readonly votes: readonly FloorVote[];
44
+ /**
45
+ * Every shell unit the call runs, at any tier (an approve names none in `votes`). Only a shell verdict
46
+ * has it. An acknowledgement flag on a command family needs it to see an approved call.
47
+ */
48
+ readonly seen?: readonly UnitIdentity[];
49
+ };
50
+
51
+ export type Policy = {
52
+ /** `cwd`: workspace-relative directory the shell runs in; relative path words re-base on it. */
53
+ readonly bash: (command: string, cwd?: string) => Disposition;
54
+ readonly explainBash: (command: string, cwd?: string) => FloorVerdict;
55
+ readonly read: (path: string) => Disposition;
56
+ readonly explainRead: (path: string) => FloorVerdict;
57
+ readonly write: (path: string) => Disposition;
58
+ readonly explainWrite: (path: string) => FloorVerdict;
59
+ /** Write-deny remainder intern. Null when the floor is not deny. */
60
+ readonly writeKind: (path: string) => WriteRemainderKind | null;
61
+ };
62
+
63
+ export type FlagConstraint = {
64
+ readonly names: readonly string[];
65
+ readonly vals: readonly string[];
66
+ readonly takesValue: boolean;
67
+ };
68
+
69
+ export type Matcher = {
70
+ readonly class?: ClassId;
71
+ readonly flags: Readonly<Record<string, FlagConstraint>>;
72
+ readonly globs: readonly string[];
73
+ readonly toks: readonly string[];
74
+ readonly matchers: readonly Matcher[];
75
+ readonly policy?: Disposition;
76
+ };
77
+
78
+ export type CommandNode = {
79
+ readonly policy?: Disposition;
80
+ /** Why this node votes what it votes. Inherited from the nearest ancestor that names one. */
81
+ readonly class?: ClassId;
82
+ /** Leading positionals (after the spine) that belong to the grant family. Does not affect the tier. */
83
+ readonly familyArgs: number;
84
+ readonly argTypes: readonly ArgType[];
85
+ readonly argRestType?: ArgType;
86
+ readonly matchers: readonly Matcher[];
87
+ /** Option whose value is a path (`--output=F`, glued `-oF`). Long names carry their prefixes. */
88
+ readonly flagPaths: Readonly<Record<string, ArgType>>;
89
+ /** Named argument guards (see guards.ts) for what flags and positionals cannot express. */
90
+ readonly guards: readonly string[];
91
+ readonly dash?: CommandNode;
92
+ readonly subcommands: Readonly<Record<string, CommandNode>>;
93
+ };
94
+
95
+ /** Classes of the votes that decided the tier. A tier with no voting rule is `unmodelled`. */
96
+ export function classesOf(verdict: FloorVerdict): readonly ClassId[] {
97
+ if (verdict.disposition === "policyApprove") return [];
98
+ const classes = new Set(
99
+ verdict.votes.filter((vote) => vote.disposition === verdict.disposition).map((vote) => vote.class),
100
+ );
101
+ return classes.size === 0 ? ["unmodelled"] : [...classes];
102
+ }
package/src/verdict.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Votes on the strict lattice. `join` is `lattice` that keeps the rules which decided the tier:
3
+ * the result is the strictest disposition and the union of the votes at exactly that disposition.
4
+ */
5
+ import type { ClassId } from "./classes.js";
6
+ import type { Disposition, FloorVerdict, FloorVote, UnitIdentity } from "./types.js";
7
+
8
+ const RANK: Readonly<Record<Disposition, number>> = { policyApprove: 0, ask: 1, deny: 2, block: 3 };
9
+
10
+ export const APPROVE: FloorVerdict = { disposition: "policyApprove", votes: [] };
11
+
12
+ export function rank(disposition: Disposition): number {
13
+ return RANK[disposition];
14
+ }
15
+
16
+ /** A single vote. An approve carries no vote: it names no class. */
17
+ export function cast(
18
+ disposition: Disposition,
19
+ cls: ClassId,
20
+ ruleId: string,
21
+ extra?: Pick<FloorVote, "unit" | "lift" | "path">,
22
+ ): FloorVerdict {
23
+ if (disposition === "policyApprove") return APPROVE;
24
+ return { disposition, votes: [{ disposition, class: cls, ruleId, ...extra }] };
25
+ }
26
+
27
+ /** Strictest of the verdicts, with the votes at that tier (deduplicated by rule, class and unit). */
28
+ export function join(verdicts: readonly FloorVerdict[]): FloorVerdict {
29
+ const joined = joinVotes(verdicts);
30
+ const seen = mergeSeen(verdicts);
31
+ return seen === undefined ? joined : { ...joined, seen };
32
+ }
33
+
34
+ function mergeSeen(verdicts: readonly FloorVerdict[]): readonly UnitIdentity[] | undefined {
35
+ const out = new Map<string, UnitIdentity>();
36
+ for (const verdict of verdicts) for (const unit of verdict.seen ?? []) out.set(JSON.stringify(unit), unit);
37
+ return out.size === 0 ? undefined : [...out.values()];
38
+ }
39
+
40
+ function joinVotes(verdicts: readonly FloorVerdict[]): FloorVerdict {
41
+ let top: Disposition = "policyApprove";
42
+ for (const verdict of verdicts) if (rank(verdict.disposition) > rank(top)) top = verdict.disposition;
43
+ if (top === "policyApprove") return APPROVE;
44
+ const seen = new Set<string>();
45
+ const votes: FloorVote[] = [];
46
+ for (const verdict of verdicts) {
47
+ if (verdict.disposition !== top) continue;
48
+ for (const vote of verdict.votes) {
49
+ if (vote.disposition !== top) continue;
50
+ const key = JSON.stringify([vote.ruleId, vote.class, vote.unit, vote.lift, vote.path]);
51
+ if (seen.has(key)) continue;
52
+ seen.add(key);
53
+ votes.push(vote);
54
+ }
55
+ }
56
+ return { disposition: top, votes };
57
+ }
58
+
59
+ /** Same verdict with every vote stamped with a unit (shell units only). */
60
+ export function withUnit(verdict: FloorVerdict, unit: FloorVote["unit"]): FloorVerdict {
61
+ if (unit === undefined || verdict.votes.length === 0) return verdict;
62
+ return { disposition: verdict.disposition, votes: verdict.votes.map((vote) => ({ unit, ...vote })) };
63
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.