@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.js +123 -232
  3. 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.45.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
- const ctx = createContext({
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
- const modePart = opts.mode ? `, mode ${opts.mode}` : "";
17822
- return `[runner] attached as ${identity2}${modePart}, scope ${scope}, cwd ${opts.cwd}, ${exec}${opts.once ? " — once" : ""}`;
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 flag off) — task polling continues
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 an agent's host executor (#822). Two modes: " + "--tools executes host-tool calls pushed over the channel (a Martha-hosted " + "agent's host runner); --tasks claims and runs tasks (a self-hosted agent's " + "executor). Identity comes from the API token.").option("--tools", "Host-runner mode: execute host-tool calls pushed over the per-agent channel. " + "Never polls task queues. Requires --allow-host-exec.").option("--tasks", "Task-executor mode: poll/claim/run tasks assigned to this agent.").option("--agent <id>", "Deprecated: identity comes from the API token; applied as a client-side filter only").option("--team <name>", "Poll this team's queue (name or UUID) instead of direct-only (task mode)").option("--once", "Claim + run a single task, then exit (task mode)").option("--poll-interval <ms>", "Poll interval when idle (ms)", "3000").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("--channel", "Deprecated alias for --tools alongside task polling (pre-#822 behaviour).").option("--local-agent <name>", "Local coding agent for local_agent delegation over the channel", DEFAULT_LOCAL_AGENT).action(async (opts) => {
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 pollInterval = parseInt(opts.pollInterval, 10);
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, { team: opts.team, cwd: opts.cwd, allowHostExec: runOpts.allowHostExec, once: !!opts.once, mode })}
17840
+ process.stderr.write(source_default.dim(`${startupEchoLine(who, { cwd: opts.cwd, allowHostExec })}
17986
17841
  `));
17987
- if (runChannel && !runOpts.allowHostExec) {
17988
- throw new CLIError("--tools executes host-tool calls; re-run with --allow-host-exec to consent", 4 /* Validation */);
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 pollLoop = async () => {
18009
- const handled = new Set;
18010
- while (!stop) {
18011
- let r;
18012
- try {
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 Promise.all(loops);
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
- const ctx = createContext({
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.45.0",
3
+ "version": "0.50.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {