seisin 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -686
- package/package.json +2 -2
- package/src/canary.js +1 -1
- package/src/cli.js +47 -3
- package/src/commands/check.js +68 -12
- package/src/commands/hook.js +1 -1
- package/src/commands/log.js +21 -14
- package/src/commands/requests.js +10 -15
- package/src/commands/review.js +2 -1
- package/src/commands/run.js +331 -245
- package/src/commands/walls.js +12 -5
- package/src/config-fields.js +305 -0
- package/src/config.js +114 -534
- package/src/controls.js +1 -1
- package/src/decide-request.js +37 -0
- package/src/denial-attribution.js +338 -0
- package/src/grants.js +1 -1
- package/src/help.js +6 -4
- package/src/index.js +1 -1
- package/src/inspect/enforce.js +1 -1
- package/src/inspect/files.js +1 -1
- package/src/inspect/home.js +1 -1
- package/src/inspect/inject.js +1 -1
- package/src/intake.js +3 -2
- package/src/keys.js +2 -2
- package/src/kinds.js +19 -0
- package/src/layout.js +1 -1
- package/src/log.js +3 -2
- package/src/mcp.js +1 -1
- package/src/notify-config.js +58 -0
- package/src/notify.js +4 -52
- package/src/owners.js +2 -2
- package/src/requests.js +9 -4
- package/src/rundir.js +2 -2
- package/src/runs.js +29 -10
- package/src/sandboxes/command.js +0 -2
- package/src/sandboxes/index.js +15 -2
- package/src/sandboxes/srt.js +1 -1
- package/src/sandboxes/tartaros.js +34 -6
- package/src/serve.js +11 -22
- package/src/spool.js +1 -1
- package/src/{srt.js → srt-settings.js} +5 -2
- package/src/state.js +25 -0
- package/src/suggest.js +26 -1
- package/src/surface.js +3 -2
- package/src/territory.js +20 -1
- package/src/toml.js +250 -0
- package/src/violations.js +16 -315
- package/src/walls.js +4 -2
- package/ui/index.html +35 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "seisin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "A permission layer for AI coding agents: role-scoped folders and keys, enforced by the OS, and when it denies something it tells you whose file it was.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@anthropic-ai/sandbox-runtime": "0.0.81"
|
|
46
46
|
},
|
|
47
47
|
"optionalDependencies": {
|
|
48
|
-
"@carlostapiacl/tartaros": "0.1.0-alpha.
|
|
48
|
+
"@carlostapiacl/tartaros": "0.1.0-alpha.18"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"test": "node --test test/*.test.js",
|
package/src/canary.js
CHANGED
|
@@ -75,7 +75,7 @@ export async function runCanary({ srt, wrap = null, settingsFile, settings, env,
|
|
|
75
75
|
// exists when the sandbox starts is one of them.
|
|
76
76
|
const insideDir = writable ? join(writable, `.seisin-canary-${nonce}`) : null;
|
|
77
77
|
const inside = insideDir ? join(insideDir, "in") : null;
|
|
78
|
-
// The runtime's shared temp directory (srt.js, SHARED_TMP): granted by the
|
|
78
|
+
// The runtime's shared temp directory (srt-settings.js, SHARED_TMP): granted by the
|
|
79
79
|
// runtime to every sandbox, taken back by seisin. Probed where it exists;
|
|
80
80
|
// where it does not, there is nothing beside which to write.
|
|
81
81
|
const shared = isDir(SHARED_TMP_DIR) ? join(SHARED_TMP_DIR, `.seisin-canary-${nonce}`) : null;
|
package/src/cli.js
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* never know: which role a path belongs to, and therefore who to ask next.
|
|
14
14
|
*/
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
|
-
import { join, dirname } from "node:path";
|
|
16
|
+
import { join, dirname, resolve } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
19
|
import { findConfig, loadConfig } from "./config.js";
|
|
20
20
|
import { CONFIG_NAME } from "./layout.js";
|
|
21
|
+
import { setStateDir } from "./state.js";
|
|
21
22
|
import { C, out, err } from "./render.js";
|
|
22
23
|
import { COMMAND_HELP, flagsOf, renderHelp, renderLimits } from "./help.js";
|
|
23
24
|
import { nearest, checkFlags } from "./suggest.js";
|
|
@@ -36,8 +37,46 @@ import { nearest, checkFlags } from "./suggest.js";
|
|
|
36
37
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
37
38
|
const [, , command, ...argv] = process.argv;
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* `--policy <file>` and `--state-dir <dir>`, for a launcher that keeps both out
|
|
42
|
+
* of the repo it runs in (an orchestrator with a folder per run). The policy is
|
|
43
|
+
* read from the file and rooted at the working directory; the log and the
|
|
44
|
+
* request queue go to the state directory. Both are taken out of seisin's own
|
|
45
|
+
* part of the line before the command reads it (after `--`, or after `run
|
|
46
|
+
* <role> <command>`, the words are the agent's), and both reach everything a
|
|
47
|
+
* run starts through the environment (SEISIN_CONFIG, SEISIN_ROOT,
|
|
48
|
+
* SEISIN_STATE_DIR).
|
|
49
|
+
*/
|
|
50
|
+
function ownEnd() {
|
|
51
|
+
const split = argv.indexOf("--");
|
|
52
|
+
if (split !== -1) return split;
|
|
53
|
+
if (command !== "run") return argv.length;
|
|
54
|
+
let i = 1;
|
|
55
|
+
const values = new Set([...(flagsOf("run")?.values ?? []), "--policy", "--state-dir"]);
|
|
56
|
+
while (i < argv.length && argv[i].startsWith("-")) i += values.has(argv[i]) ? 2 : 1;
|
|
57
|
+
return i;
|
|
58
|
+
}
|
|
59
|
+
function takeGlobal(name) {
|
|
60
|
+
const i = argv.indexOf(name);
|
|
61
|
+
if (i === -1 || i >= ownEnd()) return null;
|
|
62
|
+
const v = argv[i + 1];
|
|
63
|
+
if (v === undefined || v.startsWith("-")) {
|
|
64
|
+
err(`${C.red}seisin:${C.off} ${name} needs a value\n`);
|
|
65
|
+
process.exit(2);
|
|
66
|
+
}
|
|
67
|
+
argv.splice(i, 2);
|
|
68
|
+
return v;
|
|
69
|
+
}
|
|
70
|
+
const policyFlag = takeGlobal("--policy");
|
|
71
|
+
const stateFlag = takeGlobal("--state-dir");
|
|
72
|
+
if (stateFlag) setStateDir(stateFlag);
|
|
73
|
+
const rootFlag = policyFlag ? process.cwd() : null;
|
|
74
|
+
if (policyFlag) {
|
|
75
|
+
process.env.SEISIN_ROOT = rootFlag;
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
const USAGE = `
|
|
40
|
-
${C.b}seisin${C.off}
|
|
79
|
+
${C.b}seisin${C.off}: give each agent its own folders and its own keys
|
|
41
80
|
|
|
42
81
|
seisin run <role> -- <command...> run a command as that role
|
|
43
82
|
seisin run <role> --observe -- <cmd> open the repo and watch — the network stays shut
|
|
@@ -61,14 +100,18 @@ ${C.b}seisin${C.off} — give each agent its own folders and its own keys
|
|
|
61
100
|
seisin mcp an MCP server on stdio — read-only
|
|
62
101
|
|
|
63
102
|
seisin <command> --help lists a command's flags, examples and exit codes.
|
|
103
|
+
Any command takes --policy <file> (the policy, rooted at the working directory) and
|
|
104
|
+
--state-dir <dir> (where the log and the request queue go) for a launcher that keeps
|
|
105
|
+
both outside the repo (docs/runs.md).
|
|
64
106
|
seisin help limits says what seisin cannot do, in any repo.
|
|
65
107
|
|
|
66
|
-
Enforcement comes from
|
|
108
|
+
Enforcement comes from the sandbox (sandbox-runtime by default, or tartaros), which asks the OS.
|
|
67
109
|
seisin decides what to ask for, and says whose file it was when the answer is no.
|
|
68
110
|
`;
|
|
69
111
|
|
|
70
112
|
/** Loads the policy, or explains where to get one. */
|
|
71
113
|
function config() {
|
|
114
|
+
if (policyFlag) return loadConfig(resolve(policyFlag), undefined, { root: rootFlag });
|
|
72
115
|
const path = findConfig();
|
|
73
116
|
if (!path) throw new Error(`no ${CONFIG_NAME} found here or above. Run "seisin init" to write one.`);
|
|
74
117
|
return loadConfig(path);
|
|
@@ -76,6 +119,7 @@ function config() {
|
|
|
76
119
|
|
|
77
120
|
/** The policy here, or null — for a command that can answer without one. */
|
|
78
121
|
function maybeConfig() {
|
|
122
|
+
if (policyFlag) return config();
|
|
79
123
|
const path = findConfig();
|
|
80
124
|
return path ? loadConfig(path) : null;
|
|
81
125
|
}
|
package/src/commands/check.js
CHANGED
|
@@ -3,22 +3,42 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { inspect } from "../inspect.js";
|
|
5
5
|
import { renderReport, out, C } from "../render.js";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { stateDir } from "../state.js";
|
|
9
|
+
|
|
10
|
+
const say = out;
|
|
6
11
|
import { measureSystemLog } from "../violations.js";
|
|
7
12
|
import { join } from "node:path";
|
|
8
|
-
import { settingsFor } from "../srt.js";
|
|
13
|
+
import { settingsFor } from "../srt-settings.js";
|
|
9
14
|
import { weigh } from "../sandboxes/index.js";
|
|
10
15
|
import { runsRoot } from "../rundir.js";
|
|
11
16
|
import { channelName } from "../spool.js";
|
|
12
17
|
|
|
18
|
+
/**
|
|
19
|
+
* What this seisin can be asked to do, for a launcher deciding which flags to
|
|
20
|
+
* pass: one list, instead of a version compared against minimums or a search
|
|
21
|
+
* through `--help`. A name is added when the feature ships and never removed
|
|
22
|
+
* while it works.
|
|
23
|
+
*/
|
|
24
|
+
export const CAPABILITIES = [
|
|
25
|
+
"policy-flag", "state-dir", "run-id", "expect-denied", "agent-flag", "handoff", "handoff-false",
|
|
26
|
+
"browsers", "local-ports", "key-mode-inject", "check-json", "ui-runs",
|
|
27
|
+
];
|
|
28
|
+
|
|
13
29
|
export async function check(config, argv = []) {
|
|
14
30
|
if (argv.includes("--parallel") && !argv.includes("--verify"))
|
|
15
31
|
throw new Error("--parallel is experimental and requires --verify");
|
|
32
|
+
const json = argv.includes("--json");
|
|
16
33
|
const role = argv.find((a) => !a.startsWith("-")) ?? null;
|
|
17
34
|
const report = inspect(config, role, config.path);
|
|
18
|
-
out(renderReport(report, { verbose: argv.includes("--verbose") }));
|
|
19
|
-
if (config.sandbox?.provider) report.sandboxes = sandboxes(config, role);
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
if (!json) out(renderReport(report, { verbose: argv.includes("--verbose") }));
|
|
36
|
+
if (config.sandbox?.provider || json) report.sandboxes = sandboxes(config, role, { quiet: json });
|
|
37
|
+
// What a flag asked for runs in --json too, quietly, and lands in the JSON:
|
|
38
|
+
// skipping it answered "ok" about a check that never ran.
|
|
39
|
+
if (argv.includes("--verify")) report.verify = await verify(config, role, argv.includes("--parallel"), { quiet: json });
|
|
40
|
+
if (argv.includes("--system-log")) report.systemLog = await systemLog({ quiet: json });
|
|
41
|
+
if (json) out(JSON.stringify(asJson(config, report), null, 2) + "\n");
|
|
22
42
|
return report;
|
|
23
43
|
}
|
|
24
44
|
|
|
@@ -30,7 +50,8 @@ export async function check(config, argv = []) {
|
|
|
30
50
|
* it before, with who is writing the most — the only lead to a fix. A flag,
|
|
31
51
|
* because it takes seconds and `check` otherwise answers at once.
|
|
32
52
|
*/
|
|
33
|
-
async function systemLog() {
|
|
53
|
+
async function systemLog({ quiet = false } = {}) {
|
|
54
|
+
const out = quiet ? () => {} : say;
|
|
34
55
|
const m = await measureSystemLog({ seconds: SYSTEM_LOG_SECONDS });
|
|
35
56
|
if (!m.available) {
|
|
36
57
|
out(`\n${C.dim}system log: not measured — ${m.reason}${C.off}\n`);
|
|
@@ -56,16 +77,17 @@ const SYSTEM_LOG_SECONDS = 3;
|
|
|
56
77
|
* too. Printed only when `[sandbox] provider` is set: without it every role
|
|
57
78
|
* runs under srt, as it always has, and there is nothing to choose.
|
|
58
79
|
*/
|
|
59
|
-
export function sandboxes(config, only = null, { platform = process.platform } = {}) {
|
|
80
|
+
export function sandboxes(config, only = null, { platform = process.platform, quiet = false } = {}) {
|
|
81
|
+
const out = quiet ? () => {} : say;
|
|
60
82
|
const sock = join(runsRoot(), "check", channelName(platform));
|
|
61
83
|
const rows = [];
|
|
62
|
-
out(`\n${C.b}sandboxes${C.off} (${config.sandbox
|
|
84
|
+
out(`\n${C.b}sandboxes${C.off} (${(config.sandbox?.provider ?? ["srt"]).join(" → ")})\n`);
|
|
63
85
|
for (const r of Object.values(config.roles)) {
|
|
64
86
|
if (only && r.name !== only) continue;
|
|
65
87
|
let settings;
|
|
66
88
|
try { settings = settingsFor(config, r.name, sock); } catch (e) { rows.push({ role: r.name, chosen: null, error: e.message }); continue; }
|
|
67
89
|
const { chosen, options } = weigh(config, settings, { platform, role: r.name });
|
|
68
|
-
rows.push({ role: r.name, chosen: chosen?.sandbox.name ?? null,
|
|
90
|
+
rows.push({ role: r.name, chosen: chosen?.sandbox.name ?? null, bin: chosen?.bin ?? null,
|
|
69
91
|
options: options.map((o) => ({ name: o.sandbox.name, missing: o.missing, unmet: o.unmet, warnings: o.warnings })) });
|
|
70
92
|
out(chosen ? ` ${C.green}✓${C.off} ${r.name}: ${chosen.sandbox.name}\n` : ` ${C.red}✗${C.off} ${r.name}: no sandbox can hold it\n`);
|
|
71
93
|
// Held, but not the way the role may expect: the sandbox's own words.
|
|
@@ -82,6 +104,34 @@ export function sandboxes(config, only = null, { platform = process.platform } =
|
|
|
82
104
|
return rows;
|
|
83
105
|
}
|
|
84
106
|
|
|
107
|
+
/** `--version`'s first line from a sandbox binary, or null: what a launcher would otherwise parse itself. */
|
|
108
|
+
function versionOf(bin) {
|
|
109
|
+
if (!bin || bin === "(not looked up)") return null;
|
|
110
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 5000 });
|
|
111
|
+
return r.status === 0 ? (r.stdout || "").split("\n")[0].trim() || null : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** `check --json`: the same answer as the text, for a program. */
|
|
115
|
+
function asJson(config, report) {
|
|
116
|
+
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"), "utf8"));
|
|
117
|
+
return {
|
|
118
|
+
seisin: pkg.version,
|
|
119
|
+
platform: process.platform,
|
|
120
|
+
capabilities: CAPABILITIES,
|
|
121
|
+
policy: config.path,
|
|
122
|
+
root: config.root,
|
|
123
|
+
stateDir: stateDir(config.root),
|
|
124
|
+
roles: (report.sandboxes ?? []).map((s) => ({
|
|
125
|
+
role: s.role, sandbox: s.chosen, bin: s.bin ?? null, version: versionOf(s.bin), error: s.error ?? null,
|
|
126
|
+
unmet: (s.options ?? []).filter((o) => o.name !== s.chosen).map((o) => ({ sandbox: o.name, missing: o.missing, unmet: o.unmet })),
|
|
127
|
+
})),
|
|
128
|
+
warnings: report.warnings.map((w) => ({ kind: w.kind, role: w.role ?? null, headline: w.headline ?? w.message ?? null })),
|
|
129
|
+
...(report.verify ? { verify: report.verify.map((v) => ({ role: v.role, ok: v.ok, status: v.status ?? null })) } : {}),
|
|
130
|
+
...(report.systemLog ? { systemLog: { available: report.systemLog.available, dropped: report.systemLog.dropped ?? 0, reason: report.systemLog.reason ?? null } } : {}),
|
|
131
|
+
ok: checkStatus(report) === 0,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
85
135
|
/**
|
|
86
136
|
* The exit status: 1 when a role cannot be enforced as written, or when what a
|
|
87
137
|
* flag asked for failed. `--system-log` is asked for like `--verify` and fails
|
|
@@ -105,15 +155,21 @@ export function checkStatus(report) {
|
|
|
105
155
|
* The one thing `check` executes, which is why it is a flag: without it
|
|
106
156
|
* `check` still runs nothing.
|
|
107
157
|
*/
|
|
108
|
-
async function verify(config, only, parallel = false) {
|
|
158
|
+
async function verify(config, only, parallel = false, { quiet = false } = {}) {
|
|
159
|
+
const out = quiet ? () => {} : say;
|
|
109
160
|
const cli = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
110
161
|
const roles = Object.values(config.roles).filter((r) => (only ? r.name === only : true) && r.verify);
|
|
111
162
|
const results = [];
|
|
112
163
|
if (!roles.length) out(`\n${C.yellow}verify: no role ${only ? `"${only}" ` : ""}declares a verify command${C.off}\n`);
|
|
164
|
+
// The policy check read, passed on by name: one given with --policy (a JSON
|
|
165
|
+
// file outside the repo) is not where the child would look for seisin.toml.
|
|
166
|
+
// The state directory already travels as SEISIN_STATE_DIR.
|
|
167
|
+
const policy = config.path ? ["--policy", config.path] : [];
|
|
113
168
|
const run = async (r) => {
|
|
169
|
+
const argv = [cli, "run", ...policy, r.name, "--agent", "none", "--", ...r.verify];
|
|
114
170
|
const res = parallel
|
|
115
|
-
? await runParallel(process.execPath,
|
|
116
|
-
: spawnSync(process.execPath,
|
|
171
|
+
? await runParallel(process.execPath, argv, config.root)
|
|
172
|
+
: spawnSync(process.execPath, argv, {
|
|
117
173
|
cwd: config.root, encoding: "utf8", timeout: 300_000, env: process.env,
|
|
118
174
|
});
|
|
119
175
|
const ok = res.status === 0;
|
package/src/commands/hook.js
CHANGED
|
@@ -30,7 +30,7 @@ export async function hook(stdin = process.stdin, env = process.env) {
|
|
|
30
30
|
|
|
31
31
|
let config;
|
|
32
32
|
try {
|
|
33
|
-
config = loadConfig(env.SEISIN_CONFIG ?? findConfig());
|
|
33
|
+
config = loadConfig(env.SEISIN_CONFIG ?? findConfig(), undefined, { root: env.SEISIN_ROOT || null });
|
|
34
34
|
} catch {
|
|
35
35
|
return null;
|
|
36
36
|
}
|
package/src/commands/log.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* Reading is the easy half. Writing goes through spool.js when the writer is
|
|
9
9
|
* inside a sandbox, so that the record is not editable by what it records.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { closeSync, openSync, readSync, watch as watchDir } from "node:fs";
|
|
12
|
+
import { StringDecoder } from "node:string_decoder";
|
|
13
|
+
import { lineReader } from "../lines.js";
|
|
12
14
|
import { dirname, relative } from "node:path";
|
|
13
15
|
import { read, logPath, size, verifyLog, LOG_NAME } from "../log.js";
|
|
14
16
|
import { renderEntry, C, out } from "../render.js";
|
|
@@ -120,21 +122,26 @@ export function watch(config, { interval = 500 } = {}) {
|
|
|
120
122
|
let at = size(file);
|
|
121
123
|
out(`\n ${C.dim}watching ${relative(process.cwd(), file)} — ctrl-c to stop${C.off}\n\n`);
|
|
122
124
|
|
|
125
|
+
// One reader across reads: a line that arrives in two pieces is shown once
|
|
126
|
+
// it is whole, instead of being dropped as half-written. Read synchronously,
|
|
127
|
+
// so a filesystem event and the interval never interleave two reads.
|
|
128
|
+
const show = (line) => { try { out(renderEntry(JSON.parse(line))); } catch { /* not a log line */ } };
|
|
129
|
+
let reader = lineReader(show), text = new StringDecoder("utf8");
|
|
123
130
|
const drain = () => {
|
|
124
131
|
const end = size(file);
|
|
125
|
-
if (end
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
});
|
|
132
|
+
if (end < at) { reader = lineReader(show); text = new StringDecoder("utf8"); } // truncated or rotated: start over
|
|
133
|
+
if (end <= at) { at = end; return; }
|
|
134
|
+
const fd = openSync(file, "r");
|
|
135
|
+
try {
|
|
136
|
+
const chunk = Buffer.alloc(end - at);
|
|
137
|
+
readSync(fd, chunk, 0, chunk.length, at);
|
|
138
|
+
at = end;
|
|
139
|
+
reader(text.write(chunk));
|
|
140
|
+
} finally { closeSync(fd); }
|
|
136
141
|
};
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
let dir = null;
|
|
144
|
+
try { dir = watchDir(dirname(file), () => drain()); } catch { /* platform without events */ }
|
|
145
|
+
const timer = setInterval(drain, interval);
|
|
146
|
+
return { stop: () => { clearInterval(timer); dir?.close(); } };
|
|
140
147
|
}
|
package/src/commands/requests.js
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* person acting in a channel the agent does not have. That is the invariant the
|
|
7
7
|
* whole feature rests on — see docs/requests.md.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { queueOf, queueData, shellId, lastAgentOf, fileOnlyNote } from "../requests.js";
|
|
10
10
|
import { describeHandoff } from "../handoff.js";
|
|
11
11
|
import { keyHolders } from "../owners.js";
|
|
12
|
-
import {
|
|
12
|
+
import { decideRequest } from "../decide-request.js";
|
|
13
13
|
import { read, logPath } from "../log.js";
|
|
14
14
|
import { C, out, safe } from "../render.js";
|
|
15
15
|
|
|
@@ -125,14 +125,10 @@ export function grant(config, argv = []) {
|
|
|
125
125
|
// Looked up, applied and settled under one lock — the one the console's
|
|
126
126
|
// decide takes too. Picking before the lock let this and the console both
|
|
127
127
|
// find the same request open and act on it twice.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (only) req = asFileGrant(req);
|
|
133
|
-
refuseIfBarred(current, req);
|
|
134
|
-
return applyGrant(before, req, reason);
|
|
135
|
-
}, { after: () => settle(requestsPath(config.root), req.key, "granted", reason, { only: req.only }) }));
|
|
128
|
+
const { request: req, changed } = underLock(() => decideRequest(config, {
|
|
129
|
+
decision: "granted", reason, only,
|
|
130
|
+
select: (current) => pick(current, which, "grant"),
|
|
131
|
+
}));
|
|
136
132
|
// Said once, beside the grant it is about: a literal file is useless to an
|
|
137
133
|
// agent that saves through a temporary beside it.
|
|
138
134
|
const caveat = only ? fileOnlyNote(req.role, lastAgentOf(read(logPath(config.root)), req.role)) : "";
|
|
@@ -181,11 +177,10 @@ export function decline(config, argv = []) {
|
|
|
181
177
|
|
|
182
178
|
// The policy is not edited, but the lock is still the policy's: it is the one
|
|
183
179
|
// every decision takes, so a decline cannot settle what a grant is applying.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}, { after: () => settle(requestsPath(config.root), req.key, "declined", reason) }));
|
|
180
|
+
const { request: req } = underLock(() => decideRequest(config, {
|
|
181
|
+
decision: "declined", reason, reload: false, includeOnly: false,
|
|
182
|
+
select: (current) => pick(current, argv[0], "decline"),
|
|
183
|
+
}));
|
|
189
184
|
out(
|
|
190
185
|
`\n ${C.yellow}declined${C.off} ${req.role} ✕ ${req.grant}\n` +
|
|
191
186
|
` ${C.dim}${reason || "no reason recorded"}${C.off}\n\n`
|
package/src/commands/review.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* would mean a correct policy cannot go green.
|
|
16
16
|
*/
|
|
17
17
|
import { review } from "../review.js";
|
|
18
|
+
import { positiveInt } from "../suggest.js";
|
|
18
19
|
import { parseSince } from "../views.js";
|
|
19
20
|
import { C, out, safe } from "../render.js";
|
|
20
21
|
import { wired } from "./wire.js";
|
|
@@ -30,7 +31,7 @@ export function reviewCommand(config, argv = []) {
|
|
|
30
31
|
const since = s === -1 ? null : parseSince(argv[s + 1]);
|
|
31
32
|
if (s !== -1 && !since)
|
|
32
33
|
throw new Error(`--since takes a date (2026-10-01) or a duration back from now (7d, 12h, 2w), not "${safe(argv[s + 1] ?? "")}"`);
|
|
33
|
-
const r = review(config, { minDenials: i === -1 ? 3 :
|
|
34
|
+
const r = review(config, { minDenials: i === -1 ? 3 : positiveInt("--min", argv[i + 1]), since });
|
|
34
35
|
// Ten rows per section unless --all: on a real 32-role log the full report
|
|
35
36
|
// was 1,557 lines, which nobody reads.
|
|
36
37
|
const all = argv.includes("--all");
|