@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.
- package/LICENSE.md +7 -0
- package/authorize.scm +4 -0
- package/dist/acknowledge.d.ts +70 -0
- package/dist/acknowledge.d.ts.map +1 -0
- package/dist/acknowledge.js +162 -0
- package/dist/acknowledge.js.map +1 -0
- package/dist/authorize.d.ts +8 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +39 -0
- package/dist/authorize.js.map +1 -0
- package/dist/bind.d.ts +6 -0
- package/dist/bind.d.ts.map +1 -0
- package/dist/bind.js +45 -0
- package/dist/bind.js.map +1 -0
- package/dist/check.d.ts +30 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +389 -0
- package/dist/check.js.map +1 -0
- package/dist/classes.d.ts +23 -0
- package/dist/classes.d.ts.map +1 -0
- package/dist/classes.js +139 -0
- package/dist/classes.js.map +1 -0
- package/dist/compile.d.ts +40 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +311 -0
- package/dist/compile.js.map +1 -0
- package/dist/deadline.d.ts +5 -0
- package/dist/deadline.d.ts.map +1 -0
- package/dist/deadline.js +17 -0
- package/dist/deadline.js.map +1 -0
- package/dist/floor.d.ts +9 -0
- package/dist/floor.d.ts.map +1 -0
- package/dist/floor.js +9 -0
- package/dist/floor.js.map +1 -0
- package/dist/glob-expand.d.ts +10 -0
- package/dist/glob-expand.d.ts.map +1 -0
- package/dist/glob-expand.js +132 -0
- package/dist/glob-expand.js.map +1 -0
- package/dist/guards.d.ts +26 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +419 -0
- package/dist/guards.js.map +1 -0
- package/dist/identity.d.ts +11 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +23 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lift.d.ts +4 -0
- package/dist/lift.d.ts.map +1 -0
- package/dist/lift.js +14 -0
- package/dist/lift.js.map +1 -0
- package/dist/path-policy.d.ts +53 -0
- package/dist/path-policy.d.ts.map +1 -0
- package/dist/path-policy.js +248 -0
- package/dist/path-policy.js.map +1 -0
- package/dist/processor.d.ts +71 -0
- package/dist/processor.d.ts.map +1 -0
- package/dist/processor.js +132 -0
- package/dist/processor.js.map +1 -0
- package/dist/reasonable-defaults.d.ts +1448 -0
- package/dist/reasonable-defaults.d.ts.map +1 -0
- package/dist/reasonable-defaults.js +6 -0
- package/dist/reasonable-defaults.js.map +1 -0
- package/dist/reasonable-defaults.json +1662 -0
- package/dist/run-authorization.d.ts +8 -0
- package/dist/run-authorization.d.ts.map +1 -0
- package/dist/run-authorization.js +16 -0
- package/dist/run-authorization.js.map +1 -0
- package/dist/tool-path.d.ts +8 -0
- package/dist/tool-path.d.ts.map +1 -0
- package/dist/tool-path.js +38 -0
- package/dist/tool-path.js.map +1 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/dist/verdict.d.ts +15 -0
- package/dist/verdict.d.ts.map +1 -0
- package/dist/verdict.js +55 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +62 -3
- package/src/acknowledge.ts +206 -0
- package/src/authorize.ts +38 -0
- package/src/bind.ts +58 -0
- package/src/check.ts +552 -0
- package/src/classes.ts +156 -0
- package/src/compile.ts +370 -0
- package/src/deadline.ts +15 -0
- package/src/floor.ts +16 -0
- package/src/glob-expand.ts +137 -0
- package/src/guards.ts +428 -0
- package/src/identity.ts +26 -0
- package/src/index.ts +24 -0
- package/src/lift.ts +15 -0
- package/src/path-policy.ts +280 -0
- package/src/processor.ts +187 -0
- package/src/reasonable-defaults.json +1662 -0
- package/src/reasonable-defaults.ts +8 -0
- package/src/run-authorization.ts +23 -0
- package/src/tool-path.ts +43 -0
- package/src/types.ts +102 -0
- package/src/verdict.ts +63 -0
- package/README.md +0 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type EnvCapability } from "@here.build/arrival";
|
|
2
|
+
export declare function authorize(document: string, options: {
|
|
3
|
+
readonly capability: EnvCapability;
|
|
4
|
+
readonly config?: Readonly<Record<string, unknown>>;
|
|
5
|
+
readonly program?: string;
|
|
6
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
7
|
+
}): Promise<"allow" | "deny" | null>;
|
|
8
|
+
//# sourceMappingURL=run-authorization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-authorization.d.ts","sourceRoot":"","sources":["../src/run-authorization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAI1E,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IACP,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,GACA,OAAO,CAAC,OAAO,GAAG,MAAM,GAAG,IAAI,CAAC,CAUlC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { execState, toJS } from "@here.build/arrival";
|
|
2
|
+
import { authorizationSource } from "./authorize.js";
|
|
3
|
+
export async function authorize(document, options) {
|
|
4
|
+
const state = await execState(authorizationSource(options.program, options.env), {
|
|
5
|
+
capabilities: [options.capability],
|
|
6
|
+
config: { ...options.config, document },
|
|
7
|
+
});
|
|
8
|
+
const value = state.values.at(0);
|
|
9
|
+
if (value === undefined)
|
|
10
|
+
return null;
|
|
11
|
+
const decided = toJS(value);
|
|
12
|
+
if (decided === "allow" || decided === "deny")
|
|
13
|
+
return decided;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=run-authorization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-authorization.js","sourceRoot":"","sources":["../src/run-authorization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAsB,MAAM,qBAAqB,CAAC;AAE1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAErD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAAgB,EAChB,OAKC;IAED,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,mBAAmB,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE;QAC/E,YAAY,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC;QAClC,MAAM,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE;KACxC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,MAAM;QAAE,OAAO,OAAO,CAAC;IAC9D,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Disposition } from "./types.js";
|
|
2
|
+
export declare function normalizeToolPath(raw: string, cwd?: string, home?: string): string;
|
|
3
|
+
/** Strictest dist-floor answer for a model-supplied path, after host normalisation. */
|
|
4
|
+
export declare function checkToolPath(operation: "read" | "write", raw: string, options?: {
|
|
5
|
+
readonly cwd?: string;
|
|
6
|
+
readonly home?: string;
|
|
7
|
+
}): Disposition;
|
|
8
|
+
//# sourceMappingURL=tool-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-path.d.ts","sourceRoot":"","sources":["../src/tool-path.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAsB,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAU5G;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,GAAG,OAAO,EAC3B,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D,WAAW,CAIb"}
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
import { defaultPolicy } from "./bind.js";
|
|
14
|
+
import { lattice } from "./lift.js";
|
|
15
|
+
function slash(text) {
|
|
16
|
+
return text.replaceAll("\\", "/");
|
|
17
|
+
}
|
|
18
|
+
export function normalizeToolPath(raw, cwd = process.cwd(), home = homedir()) {
|
|
19
|
+
let written = slash(raw.trim() === "" ? "." : raw.trim());
|
|
20
|
+
if (written === "~" || written.startsWith("~/"))
|
|
21
|
+
written = `${slash(home)}${written.slice(1)}`;
|
|
22
|
+
const root = path.posix.normalize(slash(cwd.startsWith("/") || /^[a-z]:/i.test(cwd) ? cwd : `/${cwd}`));
|
|
23
|
+
const resolved = path.posix.normalize(written.startsWith("/") ? written : path.posix.join(root, written));
|
|
24
|
+
const trimmed = resolved.length > 1 && resolved.endsWith("/") ? resolved.slice(0, -1) : resolved;
|
|
25
|
+
const rel = path.posix.relative(root, trimmed);
|
|
26
|
+
if (rel === "")
|
|
27
|
+
return ".";
|
|
28
|
+
if (rel === ".." || rel.startsWith("../") || rel.startsWith("/"))
|
|
29
|
+
return trimmed;
|
|
30
|
+
return rel;
|
|
31
|
+
}
|
|
32
|
+
/** Strictest dist-floor answer for a model-supplied path, after host normalisation. */
|
|
33
|
+
export function checkToolPath(operation, raw, options) {
|
|
34
|
+
const prepared = normalizeToolPath(raw, options?.cwd, options?.home);
|
|
35
|
+
const check = operation === "read" ? defaultPolicy.read : defaultPolicy.write;
|
|
36
|
+
return lattice([check(prepared), check(prepared.toLowerCase())]);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=tool-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-path.js","sourceRoot":"","sources":["../src/tool-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,SAAS,KAAK,CAAC,IAAY;IACzB,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,MAAc,OAAO,CAAC,GAAG,EAAE,EAAE,OAAe,OAAO,EAAE;IAClG,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAC1D,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/F,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;IACxG,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAC1G,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACjG,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAC/C,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,GAAG,CAAC;IAC3B,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC;IACjF,OAAO,GAAG,CAAC;AACb,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,aAAa,CAC3B,SAA2B,EAC3B,GAAW,EACX,OAA2D;IAE3D,MAAM,QAAQ,GAAG,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;IACrE,MAAM,KAAK,GAAG,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC;IAC9E,OAAO,OAAO,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;AACnE,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
import type { ClassId } from "./classes.js";
|
|
8
|
+
export type Disposition = "policyApprove" | "ask" | "deny" | "block";
|
|
9
|
+
/** Write-deny remainder intern. Null when the floor is not deny. */
|
|
10
|
+
export type WriteRemainderKind = "protected" | "outside" | "risky";
|
|
11
|
+
export type ArgType = "read_path" | "write_path";
|
|
12
|
+
/** Why an approve became ask without a rule saying so. A lifted vote is never grantable. */
|
|
13
|
+
export type LiftKind = "env" | "xargs" | "nonliteral" | "unparsed" | "overflow" | "readings";
|
|
14
|
+
/** Grant family of one shell unit: program, recognized subcommand spine, `family_args` positionals. */
|
|
15
|
+
export type UnitIdentity = {
|
|
16
|
+
readonly argv0: string;
|
|
17
|
+
readonly spine: readonly string[];
|
|
18
|
+
readonly familyArgs: readonly string[];
|
|
19
|
+
};
|
|
20
|
+
/** One rule's vote, with the class that says why it voted. Only votes at the call's tier are reported. */
|
|
21
|
+
export type FloorVote = {
|
|
22
|
+
readonly disposition: Disposition;
|
|
23
|
+
readonly class: ClassId;
|
|
24
|
+
/** Stable rule identity: `bash:git/push[--force]`, `write_block:.git/hooks`, `lift:env`. */
|
|
25
|
+
readonly ruleId: string;
|
|
26
|
+
/** Shell votes only: the unit that voted. Absent for path and host votes. */
|
|
27
|
+
readonly unit?: UnitIdentity;
|
|
28
|
+
readonly lift?: LiftKind;
|
|
29
|
+
/** Path votes only: the prepared path and the cluster it was judged in. */
|
|
30
|
+
readonly path?: {
|
|
31
|
+
readonly cluster: "read" | "write";
|
|
32
|
+
readonly path: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/** The tier and the votes that decided it. `votes` is empty for `policyApprove`. */
|
|
36
|
+
export type FloorVerdict = {
|
|
37
|
+
readonly disposition: Disposition;
|
|
38
|
+
readonly votes: readonly FloorVote[];
|
|
39
|
+
/**
|
|
40
|
+
* Every shell unit the call runs, at any tier (an approve names none in `votes`). Only a shell verdict
|
|
41
|
+
* has it. An acknowledgement flag on a command family needs it to see an approved call.
|
|
42
|
+
*/
|
|
43
|
+
readonly seen?: readonly UnitIdentity[];
|
|
44
|
+
};
|
|
45
|
+
export type Policy = {
|
|
46
|
+
/** `cwd`: workspace-relative directory the shell runs in; relative path words re-base on it. */
|
|
47
|
+
readonly bash: (command: string, cwd?: string) => Disposition;
|
|
48
|
+
readonly explainBash: (command: string, cwd?: string) => FloorVerdict;
|
|
49
|
+
readonly read: (path: string) => Disposition;
|
|
50
|
+
readonly explainRead: (path: string) => FloorVerdict;
|
|
51
|
+
readonly write: (path: string) => Disposition;
|
|
52
|
+
readonly explainWrite: (path: string) => FloorVerdict;
|
|
53
|
+
/** Write-deny remainder intern. Null when the floor is not deny. */
|
|
54
|
+
readonly writeKind: (path: string) => WriteRemainderKind | null;
|
|
55
|
+
};
|
|
56
|
+
export type FlagConstraint = {
|
|
57
|
+
readonly names: readonly string[];
|
|
58
|
+
readonly vals: readonly string[];
|
|
59
|
+
readonly takesValue: boolean;
|
|
60
|
+
};
|
|
61
|
+
export type Matcher = {
|
|
62
|
+
readonly class?: ClassId;
|
|
63
|
+
readonly flags: Readonly<Record<string, FlagConstraint>>;
|
|
64
|
+
readonly globs: readonly string[];
|
|
65
|
+
readonly toks: readonly string[];
|
|
66
|
+
readonly matchers: readonly Matcher[];
|
|
67
|
+
readonly policy?: Disposition;
|
|
68
|
+
};
|
|
69
|
+
export type CommandNode = {
|
|
70
|
+
readonly policy?: Disposition;
|
|
71
|
+
/** Why this node votes what it votes. Inherited from the nearest ancestor that names one. */
|
|
72
|
+
readonly class?: ClassId;
|
|
73
|
+
/** Leading positionals (after the spine) that belong to the grant family. Does not affect the tier. */
|
|
74
|
+
readonly familyArgs: number;
|
|
75
|
+
readonly argTypes: readonly ArgType[];
|
|
76
|
+
readonly argRestType?: ArgType;
|
|
77
|
+
readonly matchers: readonly Matcher[];
|
|
78
|
+
/** Option whose value is a path (`--output=F`, glued `-oF`). Long names carry their prefixes. */
|
|
79
|
+
readonly flagPaths: Readonly<Record<string, ArgType>>;
|
|
80
|
+
/** Named argument guards (see guards.ts) for what flags and positionals cannot express. */
|
|
81
|
+
readonly guards: readonly string[];
|
|
82
|
+
readonly dash?: CommandNode;
|
|
83
|
+
readonly subcommands: Readonly<Record<string, CommandNode>>;
|
|
84
|
+
};
|
|
85
|
+
/** Classes of the votes that decided the tier. A tier with no voting rule is `unmodelled`. */
|
|
86
|
+
export declare function classesOf(verdict: FloorVerdict): readonly ClassId[];
|
|
87
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
/** Classes of the votes that decided the tier. A tier with no voting rule is `unmodelled`. */
|
|
8
|
+
export function classesOf(verdict) {
|
|
9
|
+
if (verdict.disposition === "policyApprove")
|
|
10
|
+
return [];
|
|
11
|
+
const classes = new Set(verdict.votes.filter((vote) => vote.disposition === verdict.disposition).map((vote) => vote.class));
|
|
12
|
+
return classes.size === 0 ? ["unmodelled"] : [...classes];
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyFH,8FAA8F;AAC9F,MAAM,UAAU,SAAS,CAAC,OAAqB;IAC7C,IAAI,OAAO,CAAC,WAAW,KAAK,eAAe;QAAE,OAAO,EAAE,CAAC;IACvD,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CACnG,CAAC;IACF,OAAO,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
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 } from "./types.js";
|
|
7
|
+
export declare const APPROVE: FloorVerdict;
|
|
8
|
+
export declare function rank(disposition: Disposition): number;
|
|
9
|
+
/** A single vote. An approve carries no vote: it names no class. */
|
|
10
|
+
export declare function cast(disposition: Disposition, cls: ClassId, ruleId: string, extra?: Pick<FloorVote, "unit" | "lift" | "path">): FloorVerdict;
|
|
11
|
+
/** Strictest of the verdicts, with the votes at that tier (deduplicated by rule, class and unit). */
|
|
12
|
+
export declare function join(verdicts: readonly FloorVerdict[]): FloorVerdict;
|
|
13
|
+
/** Same verdict with every vote stamped with a unit (shell units only). */
|
|
14
|
+
export declare function withUnit(verdict: FloorVerdict, unit: FloorVote["unit"]): FloorVerdict;
|
|
15
|
+
//# sourceMappingURL=verdict.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verdict.d.ts","sourceRoot":"","sources":["../src/verdict.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAgB,MAAM,YAAY,CAAC;AAIrF,eAAO,MAAM,OAAO,EAAE,YAA0D,CAAC;AAEjF,wBAAgB,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,MAAM,CAErD;AAED,oEAAoE;AACpE,wBAAgB,IAAI,CAClB,WAAW,EAAE,WAAW,EACxB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,GAChD,YAAY,CAGd;AAED,qGAAqG;AACrG,wBAAgB,IAAI,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,GAAG,YAAY,CAIpE;AA2BD,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,YAAY,CAGrF"}
|
package/dist/verdict.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const RANK = { policyApprove: 0, ask: 1, deny: 2, block: 3 };
|
|
2
|
+
export const APPROVE = { disposition: "policyApprove", votes: [] };
|
|
3
|
+
export function rank(disposition) {
|
|
4
|
+
return RANK[disposition];
|
|
5
|
+
}
|
|
6
|
+
/** A single vote. An approve carries no vote: it names no class. */
|
|
7
|
+
export function cast(disposition, cls, ruleId, extra) {
|
|
8
|
+
if (disposition === "policyApprove")
|
|
9
|
+
return APPROVE;
|
|
10
|
+
return { disposition, votes: [{ disposition, class: cls, ruleId, ...extra }] };
|
|
11
|
+
}
|
|
12
|
+
/** Strictest of the verdicts, with the votes at that tier (deduplicated by rule, class and unit). */
|
|
13
|
+
export function join(verdicts) {
|
|
14
|
+
const joined = joinVotes(verdicts);
|
|
15
|
+
const seen = mergeSeen(verdicts);
|
|
16
|
+
return seen === undefined ? joined : { ...joined, seen };
|
|
17
|
+
}
|
|
18
|
+
function mergeSeen(verdicts) {
|
|
19
|
+
const out = new Map();
|
|
20
|
+
for (const verdict of verdicts)
|
|
21
|
+
for (const unit of verdict.seen ?? [])
|
|
22
|
+
out.set(JSON.stringify(unit), unit);
|
|
23
|
+
return out.size === 0 ? undefined : [...out.values()];
|
|
24
|
+
}
|
|
25
|
+
function joinVotes(verdicts) {
|
|
26
|
+
let top = "policyApprove";
|
|
27
|
+
for (const verdict of verdicts)
|
|
28
|
+
if (rank(verdict.disposition) > rank(top))
|
|
29
|
+
top = verdict.disposition;
|
|
30
|
+
if (top === "policyApprove")
|
|
31
|
+
return APPROVE;
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const votes = [];
|
|
34
|
+
for (const verdict of verdicts) {
|
|
35
|
+
if (verdict.disposition !== top)
|
|
36
|
+
continue;
|
|
37
|
+
for (const vote of verdict.votes) {
|
|
38
|
+
if (vote.disposition !== top)
|
|
39
|
+
continue;
|
|
40
|
+
const key = JSON.stringify([vote.ruleId, vote.class, vote.unit, vote.lift, vote.path]);
|
|
41
|
+
if (seen.has(key))
|
|
42
|
+
continue;
|
|
43
|
+
seen.add(key);
|
|
44
|
+
votes.push(vote);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { disposition: top, votes };
|
|
48
|
+
}
|
|
49
|
+
/** Same verdict with every vote stamped with a unit (shell units only). */
|
|
50
|
+
export function withUnit(verdict, unit) {
|
|
51
|
+
if (unit === undefined || verdict.votes.length === 0)
|
|
52
|
+
return verdict;
|
|
53
|
+
return { disposition: verdict.disposition, votes: verdict.votes.map((vote) => ({ unit, ...vote })) };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=verdict.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verdict.js","sourceRoot":"","sources":["../src/verdict.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inhuman.tools/policy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.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.2.0",
|
|
31
|
+
"@inhuman.tools/policy-bash-disassembler": "^0.2.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
|
+
"@inhuman.tools/env-capability-decision-structured": "^0.0.1",
|
|
39
|
+
"@-internal-/eslint-configs": "0.0.0"
|
|
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
|
+
}
|
package/src/authorize.ts
ADDED
|
@@ -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
|
+
}
|