@aiaiai-pt/martha-cli 0.45.0 → 0.50.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/CHANGELOG.md +30 -0
- package/dist/index.js +123 -232
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.49.0] — 2026-07-15
|
|
8
|
+
|
|
9
|
+
### Added — UMS-gated second-party approval governance (#583 G1+G2)
|
|
10
|
+
|
|
11
|
+
- `martha approvals` now surfaces the server-computed requester, required-group
|
|
12
|
+
snapshot, and whether the current viewer can resolve the case. Resolution
|
|
13
|
+
remains server-authoritative and reports typed group/self-approval failures.
|
|
14
|
+
- `martha policy` and `martha approvals` consistently honor `--api-url`, including
|
|
15
|
+
machine-readable `--json` workflows against non-default Martha deployments.
|
|
16
|
+
|
|
17
|
+
## [0.48.0] — 2026-07-13
|
|
18
|
+
|
|
19
|
+
### Changed — BREAKING: `martha runner` means ONE thing (#714 runner-journey unification)
|
|
20
|
+
- Bare `martha runner` now attaches this machine as a **cloud agent's hands** (the
|
|
21
|
+
previous `--tools` mode): it holds the per-agent channel open and executes
|
|
22
|
+
`tool.invoke` host-tool calls the agent's Martha-hosted reasoning loop parks on
|
|
23
|
+
(chat gates and assigned-task parks, #910). `--tools` is kept as an accepted
|
|
24
|
+
no-op so printed `setup-host-runner` commands stay valid.
|
|
25
|
+
- The startup line now states the model plainly ("this machine is the agent's
|
|
26
|
+
hands … the agent reasons in Martha's cloud loop") — the old "reasoning stays
|
|
27
|
+
Martha-hosted regardless" line was false for the removed task mode.
|
|
28
|
+
|
|
29
|
+
### Removed — the goal-as-`bash -c` task-executor mode
|
|
30
|
+
- `--tasks` (and the pre-#822 `--channel` alias), `--once`, `--team`, `--agent`,
|
|
31
|
+
and `--poll-interval` are **removed**. They ran a claimed task's goal as a raw
|
|
32
|
+
`bash -c` — the runner impersonating an external agent. Invoking any of them
|
|
33
|
+
now fails with migration guidance. **Nothing is stranded:** a self-hosted
|
|
34
|
+
agent is your own program driving the task REST — `martha tasks
|
|
35
|
+
poll/claim/heartbeat/complete` are unchanged.
|
|
36
|
+
|
|
7
37
|
## [0.30.0] — 2026-07-03
|
|
8
38
|
|
|
9
39
|
### Added — #761 runner heartbeat + poll-based cancel
|
package/dist/index.js
CHANGED
|
@@ -11119,7 +11119,7 @@ init_config();
|
|
|
11119
11119
|
init_errors();
|
|
11120
11120
|
|
|
11121
11121
|
// src/version.ts
|
|
11122
|
-
var CLI_VERSION = "0.
|
|
11122
|
+
var CLI_VERSION = "0.50.0";
|
|
11123
11123
|
|
|
11124
11124
|
// src/commands/sessions.ts
|
|
11125
11125
|
function relativeTime(iso) {
|
|
@@ -12249,6 +12249,7 @@ function parseLocalAgentRequests(data) {
|
|
|
12249
12249
|
|
|
12250
12250
|
// src/lib/local-tool-runner.ts
|
|
12251
12251
|
import { createHash as createHash2 } from "node:crypto";
|
|
12252
|
+
init_errors();
|
|
12252
12253
|
|
|
12253
12254
|
// src/lib/local-exec.ts
|
|
12254
12255
|
import { spawn } from "node:child_process";
|
|
@@ -12336,68 +12337,6 @@ async function runHostCommand(command, toolCallId, opts) {
|
|
|
12336
12337
|
function safeName(s) {
|
|
12337
12338
|
return s.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 120) || "task";
|
|
12338
12339
|
}
|
|
12339
|
-
async function startHostCommand(command, toolCallId, opts) {
|
|
12340
|
-
const dir = runnerDir(opts.cwd);
|
|
12341
|
-
await fs5.mkdir(dir, { recursive: true });
|
|
12342
|
-
const logPath = path4.join(dir, `${safeName(toolCallId)}.log`);
|
|
12343
|
-
await fs5.writeFile(logPath, "");
|
|
12344
|
-
const fd = openSync(logPath, "a");
|
|
12345
|
-
const cap = opts.maxOutputBytes ?? MAX_OUTPUT;
|
|
12346
|
-
const readLog = async () => {
|
|
12347
|
-
try {
|
|
12348
|
-
const buf = await fs5.readFile(logPath);
|
|
12349
|
-
return buf.subarray(-cap).toString("utf8");
|
|
12350
|
-
} catch {
|
|
12351
|
-
return "";
|
|
12352
|
-
}
|
|
12353
|
-
};
|
|
12354
|
-
const child = spawn(command, {
|
|
12355
|
-
shell: true,
|
|
12356
|
-
cwd: opts.cwd,
|
|
12357
|
-
detached: true,
|
|
12358
|
-
stdio: ["ignore", fd, fd]
|
|
12359
|
-
});
|
|
12360
|
-
closeSync(fd);
|
|
12361
|
-
let exited = false;
|
|
12362
|
-
const done = new Promise((resolve, reject) => {
|
|
12363
|
-
let settled = false;
|
|
12364
|
-
child.on("error", (e) => {
|
|
12365
|
-
if (settled)
|
|
12366
|
-
return;
|
|
12367
|
-
settled = true;
|
|
12368
|
-
exited = true;
|
|
12369
|
-
reject(e);
|
|
12370
|
-
});
|
|
12371
|
-
child.on("close", async (code) => {
|
|
12372
|
-
if (settled)
|
|
12373
|
-
return;
|
|
12374
|
-
settled = true;
|
|
12375
|
-
exited = true;
|
|
12376
|
-
resolve({
|
|
12377
|
-
stdout: await readLog(),
|
|
12378
|
-
stderr: "",
|
|
12379
|
-
exit_code: code ?? -1,
|
|
12380
|
-
log_path: logPath
|
|
12381
|
-
});
|
|
12382
|
-
});
|
|
12383
|
-
});
|
|
12384
|
-
const kill = (graceMs = 5000) => {
|
|
12385
|
-
if (exited || !child.pid)
|
|
12386
|
-
return;
|
|
12387
|
-
try {
|
|
12388
|
-
process.kill(-child.pid, "SIGTERM");
|
|
12389
|
-
} catch {}
|
|
12390
|
-
const escalate = setTimeout(() => {
|
|
12391
|
-
if (exited || !child.pid)
|
|
12392
|
-
return;
|
|
12393
|
-
try {
|
|
12394
|
-
process.kill(-child.pid, "SIGKILL");
|
|
12395
|
-
} catch {}
|
|
12396
|
-
}, graceMs);
|
|
12397
|
-
escalate.unref?.();
|
|
12398
|
-
};
|
|
12399
|
-
return { pid: child.pid, done, kill };
|
|
12400
|
-
}
|
|
12401
12340
|
function mintExecSecret() {
|
|
12402
12341
|
return randomBytes2(32).toString("hex");
|
|
12403
12342
|
}
|
|
@@ -13393,7 +13332,7 @@ async function resolveLocalAgent(sessionId, toolCallId, req, state) {
|
|
|
13393
13332
|
}
|
|
13394
13333
|
async function claimGate(sessionId, toolCallId, state, transport) {
|
|
13395
13334
|
if (state.claimed.has(toolCallId))
|
|
13396
|
-
return;
|
|
13335
|
+
return true;
|
|
13397
13336
|
const secret = state.secrets.get(toolCallId) ?? mintExecSecret();
|
|
13398
13337
|
const secretHash = createHash2("sha256").update(secret).digest("hex");
|
|
13399
13338
|
state.secrets.set(toolCallId, secret);
|
|
@@ -13401,9 +13340,16 @@ async function claimGate(sessionId, toolCallId, state, transport) {
|
|
|
13401
13340
|
try {
|
|
13402
13341
|
await transport.claim(toolCallId, secretHash);
|
|
13403
13342
|
state.claimed.add(toolCallId);
|
|
13343
|
+
return true;
|
|
13404
13344
|
} catch (e) {
|
|
13345
|
+
if (e instanceof MarthaAPIError && e.status === 409) {
|
|
13346
|
+
process.stderr.write(source_default.yellow(`[local-exec] gate ${toolCallId} is not pending (already resolved or timed out) — skipping execution
|
|
13347
|
+
`));
|
|
13348
|
+
return false;
|
|
13349
|
+
}
|
|
13405
13350
|
process.stderr.write(source_default.yellow(`[local-exec] failed to claim gate for ${toolCallId}: ${String(e)}
|
|
13406
13351
|
`));
|
|
13352
|
+
return true;
|
|
13407
13353
|
}
|
|
13408
13354
|
}
|
|
13409
13355
|
async function postToolResult(toolCallId, result, label, transport, execSecret) {
|
|
@@ -13420,7 +13366,8 @@ async function handleToolStatusData(ctx, sessionId, data, state, transport) {
|
|
|
13420
13366
|
if (state.handled.has(id))
|
|
13421
13367
|
continue;
|
|
13422
13368
|
state.handled.add(id);
|
|
13423
|
-
await claimGate(sessionId, id, state, transport)
|
|
13369
|
+
if (!await claimGate(sessionId, id, state, transport))
|
|
13370
|
+
continue;
|
|
13424
13371
|
const result = await resolveHostExec(sessionId, id, req, state);
|
|
13425
13372
|
await postToolResult(id, result, "host_exec", transport, state.secrets.get(id));
|
|
13426
13373
|
}
|
|
@@ -13429,7 +13376,8 @@ async function handleToolStatusData(ctx, sessionId, data, state, transport) {
|
|
|
13429
13376
|
if (state.handled.has(id))
|
|
13430
13377
|
continue;
|
|
13431
13378
|
state.handled.add(id);
|
|
13432
|
-
await claimGate(sessionId, id, state, transport)
|
|
13379
|
+
if (!await claimGate(sessionId, id, state, transport))
|
|
13380
|
+
continue;
|
|
13433
13381
|
const result = await resolveHostOutput(req, state);
|
|
13434
13382
|
await postToolResult(id, result, "host_output", transport, state.secrets.get(id));
|
|
13435
13383
|
}
|
|
@@ -13438,7 +13386,8 @@ async function handleToolStatusData(ctx, sessionId, data, state, transport) {
|
|
|
13438
13386
|
if (state.handled.has(id))
|
|
13439
13387
|
continue;
|
|
13440
13388
|
state.handled.add(id);
|
|
13441
|
-
await claimGate(sessionId, id, state, transport)
|
|
13389
|
+
if (!await claimGate(sessionId, id, state, transport))
|
|
13390
|
+
continue;
|
|
13442
13391
|
const result = await resolveHostKill(req, state);
|
|
13443
13392
|
await postToolResult(id, result, "host_kill", transport, state.secrets.get(id));
|
|
13444
13393
|
}
|
|
@@ -13447,7 +13396,8 @@ async function handleToolStatusData(ctx, sessionId, data, state, transport) {
|
|
|
13447
13396
|
if (state.handled.has(id))
|
|
13448
13397
|
continue;
|
|
13449
13398
|
state.handled.add(id);
|
|
13450
|
-
await claimGate(sessionId, id, state, transport)
|
|
13399
|
+
if (!await claimGate(sessionId, id, state, transport))
|
|
13400
|
+
continue;
|
|
13451
13401
|
const result = await resolveHostScreenshot(ctx, sessionId, req, state);
|
|
13452
13402
|
await postToolResult(id, result, "host_screenshot", transport, state.secrets.get(id));
|
|
13453
13403
|
}
|
|
@@ -13456,7 +13406,8 @@ async function handleToolStatusData(ctx, sessionId, data, state, transport) {
|
|
|
13456
13406
|
if (state.handled.has(id))
|
|
13457
13407
|
continue;
|
|
13458
13408
|
state.handled.add(id);
|
|
13459
|
-
await claimGate(sessionId, id, state, transport)
|
|
13409
|
+
if (!await claimGate(sessionId, id, state, transport))
|
|
13410
|
+
continue;
|
|
13460
13411
|
const result = await resolveHostBrowser(ctx, sessionId, req, state);
|
|
13461
13412
|
await postToolResult(id, result, "host_browser", transport, state.secrets.get(id));
|
|
13462
13413
|
}
|
|
@@ -13465,7 +13416,8 @@ async function handleToolStatusData(ctx, sessionId, data, state, transport) {
|
|
|
13465
13416
|
if (state.handled.has(id))
|
|
13466
13417
|
continue;
|
|
13467
13418
|
state.handled.add(id);
|
|
13468
|
-
await claimGate(sessionId, id, state, transport)
|
|
13419
|
+
if (!await claimGate(sessionId, id, state, transport))
|
|
13420
|
+
continue;
|
|
13469
13421
|
const result = await resolveHostFile(req, state);
|
|
13470
13422
|
await postToolResult(id, result, req.action, transport, state.secrets.get(id));
|
|
13471
13423
|
}
|
|
@@ -13474,7 +13426,8 @@ async function handleToolStatusData(ctx, sessionId, data, state, transport) {
|
|
|
13474
13426
|
if (state.handled.has(id))
|
|
13475
13427
|
continue;
|
|
13476
13428
|
state.handled.add(id);
|
|
13477
|
-
await claimGate(sessionId, id, state, transport)
|
|
13429
|
+
if (!await claimGate(sessionId, id, state, transport))
|
|
13430
|
+
continue;
|
|
13478
13431
|
const result = await resolveLocalAgent(sessionId, id, req, state);
|
|
13479
13432
|
await postToolResult(id, result, "local_agent", transport, state.secrets.get(id));
|
|
13480
13433
|
}
|
|
@@ -17030,13 +16983,11 @@ init_errors();
|
|
|
17030
16983
|
function registerApprovalCommands(program2) {
|
|
17031
16984
|
const cmd = program2.command("approvals").description("Manage approval cases");
|
|
17032
16985
|
function getCtx() {
|
|
17033
|
-
|
|
16986
|
+
return createContext({
|
|
17034
16987
|
profileOverride: program2.opts().profile,
|
|
16988
|
+
apiUrlOverride: program2.opts().apiUrl,
|
|
17035
16989
|
verbose: program2.opts().verbose
|
|
17036
16990
|
});
|
|
17037
|
-
if (program2.opts().apiUrl)
|
|
17038
|
-
ctx.profile.api_url = program2.opts().apiUrl;
|
|
17039
|
-
return ctx;
|
|
17040
16991
|
}
|
|
17041
16992
|
function isJson() {
|
|
17042
16993
|
return !!program2.opts().json;
|
|
@@ -17718,9 +17669,6 @@ ${items.length} task(s) available`));
|
|
|
17718
17669
|
// src/commands/runner.ts
|
|
17719
17670
|
import os3 from "node:os";
|
|
17720
17671
|
init_errors();
|
|
17721
|
-
function sleep5(ms) {
|
|
17722
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
17723
|
-
}
|
|
17724
17672
|
function hostLabel() {
|
|
17725
17673
|
try {
|
|
17726
17674
|
return os3.hostname() || undefined;
|
|
@@ -17734,92 +17682,24 @@ function sleepUnref(ms) {
|
|
|
17734
17682
|
t.unref?.();
|
|
17735
17683
|
});
|
|
17736
17684
|
}
|
|
17737
|
-
var DEFAULT_LEASE_S = 300;
|
|
17738
17685
|
var ACK_INTERVAL_MS = 5000;
|
|
17739
|
-
function heartbeatIntervalMs(leaseTimeoutS) {
|
|
17740
|
-
const lease = typeof leaseTimeoutS === "number" && leaseTimeoutS > 0 ? leaseTimeoutS : DEFAULT_LEASE_S;
|
|
17741
|
-
return Math.min(lease / 3, 60) * 1000;
|
|
17742
|
-
}
|
|
17743
|
-
async function runOnce(ctx, opts, handled) {
|
|
17744
|
-
const params = { limit: String(opts.pollLimit ?? 5) };
|
|
17745
|
-
if (opts.team)
|
|
17746
|
-
params.team_id = opts.team;
|
|
17747
|
-
const tasks = await ctx.api.get("/api/tasks/poll", { params });
|
|
17748
|
-
const candidates = Array.isArray(tasks) ? tasks : [];
|
|
17749
|
-
const matchesAgent = (t) => !opts.agent || t.assigned_agent_id === opts.agent || t.agent_definition_id === opts.agent;
|
|
17750
|
-
const task = candidates.find((t) => (t.status ?? "open") === "open" && !handled?.has(t.id) && matchesAgent(t));
|
|
17751
|
-
if (!task)
|
|
17752
|
-
return { ran: false };
|
|
17753
|
-
if (!opts.allowHostExec) {
|
|
17754
|
-
process.stderr.write(source_default.yellow(`
|
|
17755
|
-
[runner] refused task ${task.id} — re-run with --allow-host-exec to allow host execution
|
|
17756
|
-
`));
|
|
17757
|
-
return { ran: false, taskId: task.id, status: "refused" };
|
|
17758
|
-
}
|
|
17759
|
-
handled?.add(task.id);
|
|
17760
|
-
const claimed = await ctx.api.post(`/api/tasks/${encodeURIComponent(task.id)}/claim`, {});
|
|
17761
|
-
const goal = claimed.goal ?? task.goal ?? "";
|
|
17762
|
-
const intervalMs = heartbeatIntervalMs(claimed.lease_timeout_s ?? DEFAULT_LEASE_S);
|
|
17763
|
-
process.stderr.write(source_default.dim(`
|
|
17764
|
-
[runner] claimed ${task.id} — running: ${goal} (heartbeat every ${Math.round(intervalMs / 1000)}s)
|
|
17765
|
-
`));
|
|
17766
|
-
let result;
|
|
17767
|
-
let cancelled = false;
|
|
17768
|
-
try {
|
|
17769
|
-
const handle = await startHostCommand(goal, `task-${task.id}`, {
|
|
17770
|
-
cwd: opts.cwd
|
|
17771
|
-
});
|
|
17772
|
-
const done = handle.done.catch((e) => ({
|
|
17773
|
-
stdout: "",
|
|
17774
|
-
stderr: String(e),
|
|
17775
|
-
exit_code: -1
|
|
17776
|
-
}));
|
|
17777
|
-
for (;; ) {
|
|
17778
|
-
const raced = await Promise.race([
|
|
17779
|
-
done,
|
|
17780
|
-
sleepUnref(intervalMs).then(() => null)
|
|
17781
|
-
]);
|
|
17782
|
-
if (raced !== null) {
|
|
17783
|
-
result = raced;
|
|
17784
|
-
break;
|
|
17785
|
-
}
|
|
17786
|
-
let reply;
|
|
17787
|
-
try {
|
|
17788
|
-
reply = await ctx.api.post(`/api/tasks/${encodeURIComponent(task.id)}/heartbeat`, {});
|
|
17789
|
-
} catch (e) {
|
|
17790
|
-
process.stderr.write(source_default.yellow(`[runner] heartbeat failed for ${task.id} (job unaffected): ${String(e)}
|
|
17791
|
-
`));
|
|
17792
|
-
continue;
|
|
17793
|
-
}
|
|
17794
|
-
if (reply && reply.signal === "cancel") {
|
|
17795
|
-
process.stderr.write(source_default.yellow(`[runner] cancel signal for ${task.id} — killing process tree
|
|
17796
|
-
`));
|
|
17797
|
-
handle.kill();
|
|
17798
|
-
result = await done;
|
|
17799
|
-
cancelled = true;
|
|
17800
|
-
break;
|
|
17801
|
-
}
|
|
17802
|
-
}
|
|
17803
|
-
} catch (e) {
|
|
17804
|
-
result = { stdout: "", stderr: String(e), exit_code: -1 };
|
|
17805
|
-
}
|
|
17806
|
-
const exit_code = result.exit_code;
|
|
17807
|
-
const stdout = result.stdout || result.stderr || "";
|
|
17808
|
-
const status = cancelled ? "cancelled" : exit_code === 0 ? "completed" : "failed";
|
|
17809
|
-
await ctx.api.post(`/api/tasks/${encodeURIComponent(task.id)}/complete`, {
|
|
17810
|
-
outcome: cancelled ? { stdout, exit_code, cancelled: true } : { stdout, exit_code },
|
|
17811
|
-
status
|
|
17812
|
-
});
|
|
17813
|
-
process.stderr.write(source_default.dim(`[runner] completed ${task.id} — ${status} (exit ${exit_code})
|
|
17814
|
-
`));
|
|
17815
|
-
return { ran: true, taskId: task.id, status, exitCode: exit_code };
|
|
17816
|
-
}
|
|
17817
17686
|
function startupEchoLine(who, opts) {
|
|
17818
17687
|
const identity2 = who ? who.kind === "agent" ? `agent ${who.agent_name ?? "?"} (${(who.agent_id ?? "").slice(0, 8)}…)` : `${who.kind} ${who.sub ?? ""}`.trim() : "identity unknown (whoami unavailable)";
|
|
17819
|
-
const scope = opts.team ? `team:${opts.team}` : "direct";
|
|
17820
17688
|
const exec = opts.allowHostExec ? "host exec allowed" : "host exec refused (pass --allow-host-exec)";
|
|
17821
|
-
|
|
17822
|
-
|
|
17689
|
+
return `[runner] attached as ${identity2} — this machine is the agent's hands (host executor; the agent reasons in Martha's cloud loop), cwd ${opts.cwd}, ${exec}`;
|
|
17690
|
+
}
|
|
17691
|
+
function rejectRemovedTaskModeFlags(opts) {
|
|
17692
|
+
const used = [
|
|
17693
|
+
opts.tasks && "--tasks",
|
|
17694
|
+
opts.channel && "--channel",
|
|
17695
|
+
opts.once && "--once",
|
|
17696
|
+
opts.team && "--team",
|
|
17697
|
+
opts.agent && "--agent",
|
|
17698
|
+
opts.pollInterval && "--poll-interval"
|
|
17699
|
+
].filter(Boolean);
|
|
17700
|
+
if (used.length === 0)
|
|
17701
|
+
return;
|
|
17702
|
+
throw new CLIError(`${used.join(", ")} ${used.length > 1 ? "were" : "was"} removed: ` + "`martha runner` now means ONE thing — attach this machine as a cloud " + "agent's hands (host executor). It no longer polls or runs tasks. A " + "self-hosted agent is your own program driving the task REST " + "(`martha tasks poll/claim/heartbeat/complete`) and the peer channel.", 4 /* Validation */);
|
|
17823
17703
|
}
|
|
17824
17704
|
function shouldFlushAck(pendingAckId, lastAckedId, lastAckAt, now, force, intervalMs) {
|
|
17825
17705
|
if (!pendingAckId || pendingAckId === lastAckedId)
|
|
@@ -17879,7 +17759,7 @@ async function runChannelLoop(ctx, state, abort) {
|
|
|
17879
17759
|
if (abort.signal.aborted)
|
|
17880
17760
|
break;
|
|
17881
17761
|
if (err instanceof MarthaAPIError && err.status === 404) {
|
|
17882
|
-
process.stderr.write(source_default.dim(`[runner] channel unavailable (server
|
|
17762
|
+
process.stderr.write(source_default.dim(`[runner] channel unavailable (server CHANNEL_ENABLED off, or old server) — nothing to attach to; exiting
|
|
17883
17763
|
`));
|
|
17884
17764
|
return;
|
|
17885
17765
|
}
|
|
@@ -17945,59 +17825,25 @@ async function runChannelLoop(ctx, state, abort) {
|
|
|
17945
17825
|
}
|
|
17946
17826
|
}
|
|
17947
17827
|
function registerRunnerCommand(program2) {
|
|
17948
|
-
program2.command("runner").description("Attach this machine as
|
|
17828
|
+
program2.command("runner").description("Attach this machine as a cloud agent's hands (#714): hold the per-agent " + "channel open and execute host-tool calls the agent's Martha-hosted " + "reasoning loop parks on (chat gates and assigned-task parks). Identity " + "comes from the API token (the agent's runner key). The runner never " + "reasons and never runs tasks — self-hosted agents drive the task REST " + "(`martha tasks poll/claim/complete`) with their own code.").option("--tools", "Accepted no-op: hands mode is the only mode (kept so printed " + "setup-host-runner commands stay valid).").option("--cwd <dir>", "Scoped workspace for host execution", process.cwd()).option("--allow-host-exec", "Consent to run host commands (required to execute)").option("--yes", "Deprecated alias for --allow-host-exec").option("--local-agent <name>", "Local coding agent for local_agent delegation over the channel", DEFAULT_LOCAL_AGENT).option("--tasks", "REMOVED (#714): the runner no longer runs tasks").option("--channel", "REMOVED (#714): deprecated pre-#822 alias").option("--once", "REMOVED (#714): belonged to the task-executor mode").option("--team <name>", "REMOVED (#714): belonged to the task-executor mode").option("--agent <id>", "REMOVED (#714): identity comes from the API token").option("--poll-interval <ms>", "REMOVED (#714): the runner no longer polls").action(async (opts) => {
|
|
17829
|
+
rejectRemovedTaskModeFlags(opts);
|
|
17949
17830
|
const ctx = createContext({
|
|
17950
17831
|
profileOverride: program2.opts().profile,
|
|
17951
17832
|
verbose: program2.opts().verbose,
|
|
17952
17833
|
apiUrlOverride: program2.opts().apiUrl
|
|
17953
17834
|
});
|
|
17954
|
-
const
|
|
17955
|
-
if (isNaN(pollInterval) || pollInterval < 250) {
|
|
17956
|
-
throw new CLIError("--poll-interval must be >= 250 (ms)", 4 /* Validation */);
|
|
17957
|
-
}
|
|
17958
|
-
if (opts.agent) {
|
|
17959
|
-
process.stderr.write(source_default.yellow(`[runner] --agent is deprecated — identity comes from the API token; the value is applied as a client-side filter only
|
|
17960
|
-
`));
|
|
17961
|
-
}
|
|
17962
|
-
const runOpts = {
|
|
17963
|
-
agent: opts.agent,
|
|
17964
|
-
team: opts.team,
|
|
17965
|
-
cwd: opts.cwd,
|
|
17966
|
-
allowHostExec: !!(opts.allowHostExec || opts.yes)
|
|
17967
|
-
};
|
|
17968
|
-
let runTasks;
|
|
17969
|
-
let runChannel;
|
|
17970
|
-
if (opts.tools || opts.tasks) {
|
|
17971
|
-
runTasks = !!opts.tasks;
|
|
17972
|
-
runChannel = !!opts.tools;
|
|
17973
|
-
} else if (opts.channel) {
|
|
17974
|
-
runTasks = true;
|
|
17975
|
-
runChannel = true;
|
|
17976
|
-
} else {
|
|
17977
|
-
runTasks = true;
|
|
17978
|
-
runChannel = false;
|
|
17979
|
-
}
|
|
17980
|
-
const mode = runTasks && runChannel ? "tools+tasks" : runChannel ? "tools" : "tasks";
|
|
17835
|
+
const allowHostExec = !!(opts.allowHostExec || opts.yes);
|
|
17981
17836
|
let who;
|
|
17982
17837
|
try {
|
|
17983
17838
|
who = await ctx.api.get("/api/tasks/whoami");
|
|
17984
17839
|
} catch {}
|
|
17985
|
-
process.stderr.write(source_default.dim(`${startupEchoLine(who, {
|
|
17840
|
+
process.stderr.write(source_default.dim(`${startupEchoLine(who, { cwd: opts.cwd, allowHostExec })}
|
|
17986
17841
|
`));
|
|
17987
|
-
if (
|
|
17988
|
-
throw new CLIError("
|
|
17842
|
+
if (!allowHostExec) {
|
|
17843
|
+
throw new CLIError("the runner executes host-tool calls; re-run with --allow-host-exec to consent", 4 /* Validation */);
|
|
17989
17844
|
}
|
|
17990
|
-
if (opts.once) {
|
|
17991
|
-
if (!runTasks) {
|
|
17992
|
-
throw new CLIError("--once runs a single TASK; it has no meaning in --tools (host-runner) mode", 4 /* Validation */);
|
|
17993
|
-
}
|
|
17994
|
-
await runOnce(ctx, runOpts);
|
|
17995
|
-
return;
|
|
17996
|
-
}
|
|
17997
|
-
let stop = false;
|
|
17998
17845
|
const abort = new AbortController;
|
|
17999
17846
|
const onSignal = () => {
|
|
18000
|
-
stop = true;
|
|
18001
17847
|
abort.abort();
|
|
18002
17848
|
process.stderr.write(`
|
|
18003
17849
|
[runner] stopping…
|
|
@@ -18005,34 +17851,13 @@ function registerRunnerCommand(program2) {
|
|
|
18005
17851
|
};
|
|
18006
17852
|
process.on("SIGINT", onSignal);
|
|
18007
17853
|
process.on("SIGTERM", onSignal);
|
|
18008
|
-
const
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
r = await runOnce(ctx, runOpts, handled);
|
|
18014
|
-
} catch (e) {
|
|
18015
|
-
process.stderr.write(source_default.red(`[runner] error: ${String(e)}
|
|
18016
|
-
`));
|
|
18017
|
-
r = { ran: false };
|
|
18018
|
-
}
|
|
18019
|
-
if (!r.ran)
|
|
18020
|
-
await sleep5(pollInterval);
|
|
18021
|
-
}
|
|
18022
|
-
};
|
|
18023
|
-
const loops = [];
|
|
18024
|
-
if (runTasks)
|
|
18025
|
-
loops.push(pollLoop());
|
|
18026
|
-
if (runChannel) {
|
|
18027
|
-
const state = createLocalExecState({
|
|
18028
|
-
cwd: opts.cwd,
|
|
18029
|
-
autoYes: runOpts.allowHostExec,
|
|
18030
|
-
localAgent: opts.localAgent ?? DEFAULT_LOCAL_AGENT
|
|
18031
|
-
});
|
|
18032
|
-
loops.push(runChannelLoop(ctx, state, abort));
|
|
18033
|
-
}
|
|
17854
|
+
const state = createLocalExecState({
|
|
17855
|
+
cwd: opts.cwd,
|
|
17856
|
+
autoYes: allowHostExec,
|
|
17857
|
+
localAgent: opts.localAgent ?? DEFAULT_LOCAL_AGENT
|
|
17858
|
+
});
|
|
18034
17859
|
try {
|
|
18035
|
-
await
|
|
17860
|
+
await runChannelLoop(ctx, state, abort);
|
|
18036
17861
|
} finally {
|
|
18037
17862
|
process.off("SIGINT", onSignal);
|
|
18038
17863
|
process.off("SIGTERM", onSignal);
|
|
@@ -21089,13 +20914,11 @@ function assertOneOf(value, allowed, flag) {
|
|
|
21089
20914
|
function registerPolicyCommands(program2) {
|
|
21090
20915
|
const cmd = program2.command("policy").description("Manage the tenant's invoke-time capability policy");
|
|
21091
20916
|
function getCtx() {
|
|
21092
|
-
|
|
20917
|
+
return createContext({
|
|
21093
20918
|
profileOverride: program2.opts().profile,
|
|
20919
|
+
apiUrlOverride: program2.opts().apiUrl,
|
|
21094
20920
|
verbose: program2.opts().verbose
|
|
21095
20921
|
});
|
|
21096
|
-
if (program2.opts().apiUrl)
|
|
21097
|
-
ctx.profile.api_url = program2.opts().apiUrl;
|
|
21098
|
-
return ctx;
|
|
21099
20922
|
}
|
|
21100
20923
|
const isJson = () => !!program2.opts().json;
|
|
21101
20924
|
cmd.command("show").description("Show the current policy: settings + rules").action(async () => {
|
|
@@ -21381,6 +21204,8 @@ ${typed.hint}
|
|
|
21381
21204
|
// src/commands/authz.ts
|
|
21382
21205
|
var DEFAULT_ENTITY = "document_collection";
|
|
21383
21206
|
var DEFAULT_ACTION = "read";
|
|
21207
|
+
var APPROVAL_ENTITY = "approval";
|
|
21208
|
+
var RESOLVE_ACTION = "resolve";
|
|
21384
21209
|
function registerAuthzCommands(program2) {
|
|
21385
21210
|
function getCtx() {
|
|
21386
21211
|
const ctx = createContext({
|
|
@@ -21395,6 +21220,7 @@ function registerAuthzCommands(program2) {
|
|
|
21395
21220
|
registerCollections(program2, getCtx, isJson);
|
|
21396
21221
|
registerAccess(program2, getCtx, isJson);
|
|
21397
21222
|
registerGroups(program2, getCtx, isJson);
|
|
21223
|
+
registerApprovers(program2, getCtx, isJson);
|
|
21398
21224
|
}
|
|
21399
21225
|
function renderGrantsTable(resp) {
|
|
21400
21226
|
if (resp.grants.length === 0) {
|
|
@@ -21652,6 +21478,71 @@ function registerGroups(program2, getCtx, isJson) {
|
|
|
21652
21478
|
}
|
|
21653
21479
|
});
|
|
21654
21480
|
}
|
|
21481
|
+
function registerApprovers(program2, getCtx, isJson) {
|
|
21482
|
+
const cmd = program2.command("approvers").description("Manage who may resolve approval cases in the tenant (UMS approval/resolve). " + "Noun-first sugar over `access` on the approval entity.");
|
|
21483
|
+
cmd.command("add <group>").description("Make a group a tenant approver (its members can resolve approvals)").option("--tenant <tenant>", "Target tenant (super-admin only)").action(async (group, opts) => {
|
|
21484
|
+
const ctx = getCtx();
|
|
21485
|
+
const json = isJson();
|
|
21486
|
+
try {
|
|
21487
|
+
await grant(ctx, {
|
|
21488
|
+
granteeType: "group",
|
|
21489
|
+
granteeId: group,
|
|
21490
|
+
entityType: APPROVAL_ENTITY,
|
|
21491
|
+
action: RESOLVE_ACTION,
|
|
21492
|
+
objectId: null,
|
|
21493
|
+
tenant: opts.tenant
|
|
21494
|
+
});
|
|
21495
|
+
emitOk(json, {
|
|
21496
|
+
status: "approver_added",
|
|
21497
|
+
entity_type: APPROVAL_ENTITY,
|
|
21498
|
+
action: RESOLVE_ACTION,
|
|
21499
|
+
grantee: `group:${group}`,
|
|
21500
|
+
text: `Group ${group} can now resolve approvals in this tenant.`
|
|
21501
|
+
});
|
|
21502
|
+
} catch (err) {
|
|
21503
|
+
emitAuthzError(err, json);
|
|
21504
|
+
}
|
|
21505
|
+
});
|
|
21506
|
+
cmd.command("remove <group>").description("Remove a group's tenant approver binding").option("--tenant <tenant>", "Target tenant (super-admin only)").action(async (group, opts) => {
|
|
21507
|
+
const ctx = getCtx();
|
|
21508
|
+
const json = isJson();
|
|
21509
|
+
try {
|
|
21510
|
+
await revoke(ctx, {
|
|
21511
|
+
granteeType: "group",
|
|
21512
|
+
granteeId: group,
|
|
21513
|
+
entityType: APPROVAL_ENTITY,
|
|
21514
|
+
action: RESOLVE_ACTION,
|
|
21515
|
+
objectId: null,
|
|
21516
|
+
tenant: opts.tenant
|
|
21517
|
+
});
|
|
21518
|
+
emitOk(json, {
|
|
21519
|
+
status: "approver_removed",
|
|
21520
|
+
grantee: `group:${group}`,
|
|
21521
|
+
text: `Group ${group} can no longer resolve approvals in this tenant.`
|
|
21522
|
+
});
|
|
21523
|
+
} catch (err) {
|
|
21524
|
+
emitAuthzError(err, json);
|
|
21525
|
+
}
|
|
21526
|
+
});
|
|
21527
|
+
cmd.command("list").description("Show which groups may resolve approvals in the tenant").option("--tenant <tenant>", "Target tenant (super-admin only)").action(async (opts) => {
|
|
21528
|
+
const ctx = getCtx();
|
|
21529
|
+
const json = isJson();
|
|
21530
|
+
try {
|
|
21531
|
+
const resp = await listGrants(ctx, {
|
|
21532
|
+
entityType: APPROVAL_ENTITY,
|
|
21533
|
+
action: RESOLVE_ACTION,
|
|
21534
|
+
tenant: opts.tenant
|
|
21535
|
+
});
|
|
21536
|
+
if (json) {
|
|
21537
|
+
console.log(JSON.stringify(resp, null, 2));
|
|
21538
|
+
} else {
|
|
21539
|
+
console.log(renderGrantsTable(resp));
|
|
21540
|
+
}
|
|
21541
|
+
} catch (err) {
|
|
21542
|
+
emitAuthzError(err, json);
|
|
21543
|
+
}
|
|
21544
|
+
});
|
|
21545
|
+
}
|
|
21655
21546
|
function emitOk(json, payload) {
|
|
21656
21547
|
if (json) {
|
|
21657
21548
|
const rest = { ...payload };
|