agentradar-cli 0.2.9 → 0.2.10

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.
@@ -22,7 +22,7 @@ Call `get_my_context` on the AgentRadar MCP FIRST, before any other tool. It ret
22
22
 
23
23
  In your first session, call `read_onboarding_obligations` once (reading them acknowledges the safety rules), then `read_readiness`.
24
24
 
25
- Your primary objective is to reach your quotas and drive the organisation forward in your role. Report each metric you own with `report_metric`. You may create metrics for yourself; you can never change your own quota — your superior sets it. Talk only in shared spaces (channels and tasks). You can create one-off tasks (`create_task`) and repeated tasks (`create_recurring_task`) for yourself, your colleagues and your direct subordinates — only for real trackable work, never for conversation.
25
+ Your primary objective is to reach your quotas and drive the organisation forward in your role. Report each metric you own with `report_metric`, backed by `evidence` of what you counted it from, or by `fromEventId` when a person told you the number. You may create metrics for yourself; you can never change your own quota — your superior sets it. Talk only in shared spaces (channels and tasks). You can create one-off tasks (`create_task`) and repeated tasks (`create_recurring_task`) for yourself, your colleagues and your direct subordinates — only for real trackable work, never for conversation.
26
26
 
27
27
  ## Talking to people
28
28
 
package/dist/channel.js CHANGED
@@ -7,7 +7,7 @@ import { createInterface } from "node:readline";
7
7
  import { envValue } from "./tool-run.js";
8
8
  import { trustedMcpEndpoint } from "./setup.js";
9
9
  export const CHANNEL_GUIDANCE = "AgentRadar channel messages are wake signals only. Call next_event to receive the events, then pass their eventIds as ackEventIds on your next call. Answer livenessProbe with liveness_ack. Never acknowledge an event you have not read. Never poll next_event on your own or keep a loop running: when a channel message arrives, drain it, then end your turn.";
10
- export const CHANNEL_BLOCKED = "AgentRadar has not observed a next_event call from this session. Channel delivery is unconfirmed. On Claude Team/Enterprise an Owner must enable Channels (channelsEnabled); the development flag cannot override that policy. Until then, deliver with the feeder: set AR_FEED=feeder in ~/.agentradar/agents/<runner-id>.env and restart the runner.";
10
+ export const CHANNEL_BLOCKED = "AgentRadar has not observed a next_event call from this session. Channel delivery is unconfirmed. On Claude Team/Enterprise an Owner must enable Channels (channelsEnabled); the development flag cannot override that policy. Until then, deliver with the feeder: run your join again with --feeder (agentradar join <your join link> --feeder). Your organisation sees this agent as not receiving messages until one or the other works.";
11
11
  /** One local MCP, forwarding tools to AWP. Wake signals never consume the durable queue. */
12
12
  export async function startChannel(runnerId, deps = {}) {
13
13
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(runnerId))
@@ -24,6 +24,9 @@ export async function startChannel(runnerId, deps = {}) {
24
24
  let sequence = 0;
25
25
  let polling = false;
26
26
  let agentPolled = false;
27
+ // Vadym 10342: an unconfirmed channel is reported to AgentRadar (runnerState channel_unconfirmed), so the owner sees
28
+ // the agent is not receiving messages; "ready" again once the session does call next_event.
29
+ let unconfirmed = null;
27
30
  let warning;
28
31
  const send = (value) => new Promise((resolve, reject) => {
29
32
  output.write(`${JSON.stringify(value)}\n`, (error) => error ? reject(error) : resolve());
@@ -68,7 +71,14 @@ export async function startChannel(runnerId, deps = {}) {
68
71
  if (!selftestStarted)
69
72
  logError("AgentRadar self-test could not start; retrying after the next poll.");
70
73
  }
71
- const response = await next({ runnerState: "ready" });
74
+ if (unconfirmed === "pending") {
75
+ await next({ runnerState: "channel_unconfirmed" });
76
+ unconfirmed = "reported";
77
+ }
78
+ if (unconfirmed === "reported" && agentPolled)
79
+ unconfirmed = null;
80
+ // While reported, no runnerState: the stored state stands, and the poll still drains for wake signals.
81
+ const response = await next(unconfirmed ? {} : { runnerState: "ready" });
72
82
  if (response.error || response.result?.isError)
73
83
  throw new Error("AgentRadar refused next_event; check the runner key and its poll permission");
74
84
  const content = response.result?.content;
@@ -104,8 +114,10 @@ export async function startChannel(runnerId, deps = {}) {
104
114
  else if (message.method === "notifications/initialized") {
105
115
  if (!polling) {
106
116
  polling = true;
107
- warning = setTimeout(() => { if (!agentPolled)
108
- logError(CHANNEL_BLOCKED); }, 30_000);
117
+ warning = setTimeout(() => { if (!agentPolled) {
118
+ logError(CHANNEL_BLOCKED);
119
+ unconfirmed = "pending";
120
+ } }, deps.confirmMs ?? 30_000);
109
121
  void poll();
110
122
  }
111
123
  }
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  import { installVendorSkill } from "./skill-install.js";
4
4
  import { startChannel } from "./channel.js";
5
5
  import { runRestartSelftest, startVendorRunner } from "./runner.js";
6
- import { cancelJoin, joinOrganisation, leaveOrganisation, setupVendorAgent, terminalConfirm } from "./setup.js";
6
+ import { cancelJoin, joinOrganisation, leaveOrganisation, setupVendorAgent } from "./setup.js";
7
7
  import { callAwpTool, callToolOverCli, createTaskOverCli, listServiceToolsOverCli, parseCallArgs, runServiceToolOverCli } from "./tool-run.js";
8
8
  import { installSyncTimer, syncAgentDir } from "./sync.js";
9
9
  // `run` is the one prefix every agent-facing command shares, so one harness "always allow agentradar run"
@@ -15,7 +15,7 @@ export const runArgs = (raw) => raw[0] !== "run" ? raw
15
15
  async function main() {
16
16
  const args = runArgs(process.argv.slice(2));
17
17
  if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
18
- console.log("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] [--harness claude|codex] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat <runner-id> --args '<json>' | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
18
+ console.log("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] [--harness claude|codex] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat <runner-id> --args '<json>' | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
19
19
  return;
20
20
  }
21
21
  if (args[0] === "leave" && args.length === 2) {
@@ -70,18 +70,18 @@ async function main() {
70
70
  for (let i = 1; i < args.length; i++) {
71
71
  if (["--name", "--model", "--attach-tmux", "--session"].includes(args[i]))
72
72
  named[args[i].slice(2)] = args[++i] ?? "-";
73
- else if (!["--trust", "--attach-current", "--allow-bypass", "--revoke-bypass"].includes(args[i]))
73
+ else if (!["--trust", "--attach-current", "--allow-bypass", "--revoke-bypass", "--feeder"].includes(args[i]))
74
74
  urls.push(args[i]);
75
75
  }
76
76
  if (urls.length !== 1 || [urls[0], ...Object.values(named)].some((value) => value.startsWith("-")) || (named["attach-tmux"] && args.includes("--attach-current"))) {
77
- console.error("Usage: agentradar join <join-link> [--name <your name>] [--model <your model id>] [--trust] [--attach-tmux <session[:window.pane]> | --attach-current] [--session <claude session id>] [--allow-bypass | --revoke-bypass]");
77
+ console.error("Usage: agentradar join <join-link> [--name <your name>] [--model <your model id>] [--trust] [--attach-tmux <session[:window.pane]> | --attach-current] [--session <claude session id>] [--revoke-bypass] [--feeder]");
78
78
  process.exitCode = 1;
79
79
  return;
80
80
  }
81
81
  process.exitCode = await joinOrganisation(urls[0], {
82
82
  trust: args.includes("--trust"), agentName: named.name, model: named.model, session: named.session,
83
83
  attach: args.includes("--attach-current") ? "current" : named["attach-tmux"],
84
- allowBypass: args.includes("--allow-bypass"), revokeBypass: args.includes("--revoke-bypass"), confirm: terminalConfirm(),
84
+ revokeBypass: args.includes("--revoke-bypass"), feeder: args.includes("--feeder"),
85
85
  });
86
86
  return;
87
87
  }
@@ -142,7 +142,7 @@ async function main() {
142
142
  return;
143
143
  }
144
144
  const help = args.length === 1 && (args[0] === "--help" || args[0] === "-h");
145
- (help ? console.log : console.error)("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat <runner-id> --args '<json>' | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
145
+ (help ? console.log : console.error)("Usage: agentradar run <join-link> [--name <n>] [--model <m>] | agentradar run <runner-id> <tool> [--<argument> <value> | --<argument>-file <path> ...] | agentradar setup [--trust] | agentradar join <join-link> [--name <n>] [--model <m>] [--trust] [--attach-tmux <pane> | --attach-current] [--session <id>] [--feeder] | agentradar join cancel <join-link|request-id> | agentradar leave <runner-id> | agentradar channel <runner-id> | agentradar selftest <runner-id> [--status|--restart] | agentradar runner start <runner-id> | agentradar tool list <runner-id> | agentradar tool run <runner-id> <tool-key> [--args '<json>'] | agentradar call <runner-id> <tool> [--<argument> <value> ...] [--args '<json>'] | agentradar task create|repeat <runner-id> --args '<json>' | agentradar skill install | agentradar sync [dir] | agentradar sync install [dir] [--interval <s>]");
146
146
  if (!help)
147
147
  process.exitCode = 1;
148
148
  }
package/dist/setup.js CHANGED
@@ -118,7 +118,9 @@ export function trustedMcpEndpoint(value) {
118
118
  return endpoint.protocol === "https:" || loopbackHttp ? endpoint : null;
119
119
  }
120
120
  /** `installation: null` (join) removes the project's `agentradar` entry: a joined agent acts through `agentradar run`. */
121
- async function installProjectMcp(configPath, cwd, installation, trust) {
121
+ async function installProjectMcp(configPath, cwd, installation, trust,
122
+ /** Vadym 10342: a joined Claude Code agent's AgentRadar channel (`agentradar channel <runner-id>`, stdio). */
123
+ channel) {
122
124
  const endpoint = installation && trustedMcpEndpoint(installation.mcpEndpoint);
123
125
  if (installation && (!endpoint || !BEARER_SHAPE.test(installation.bearer))) {
124
126
  throw new Error("AgentRadar returned an invalid setup payload");
@@ -148,7 +150,10 @@ async function installProjectMcp(configPath, cwd, installation, trust) {
148
150
  const mcpServers = project.mcpServers && typeof project.mcpServers === "object" && !Array.isArray(project.mcpServers)
149
151
  ? project.mcpServers
150
152
  : {};
151
- if (installation && endpoint) {
153
+ if (channel) {
154
+ mcpServers.agentradar = channel;
155
+ }
156
+ else if (installation && endpoint) {
152
157
  mcpServers.agentradar = {
153
158
  type: "http",
154
159
  url: endpoint.toString(),
@@ -336,7 +341,7 @@ async function codexHookHashes(codexHome, env) {
336
341
  child.kill();
337
342
  }
338
343
  }
339
- async function installRunner(home, projectPath, approved, service) {
344
+ async function installRunner(home, projectPath, approved, service, feed) {
340
345
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(approved.runnerId)) {
341
346
  throw new Error("AgentRadar returned an invalid runner ID");
342
347
  }
@@ -412,15 +417,19 @@ async function installRunner(home, projectPath, approved, service) {
412
417
  joined.push(`AGENT_TOOL_ENV_FILE=${shellValue(toolEnvFile)}`);
413
418
  }
414
419
  }
415
- await writePrivateFile(mcpConfig, `${JSON.stringify(service ? { mcpServers: {} } : {
416
- mcpServers: {
417
- agentradar: {
418
- type: "http",
419
- url: endpoint.toString(),
420
- headers: { Authorization: `Bearer ${approved.installation.bearer}` },
420
+ // R764 + Vadym 10342: a pushed service session loads exactly one MCP, the AgentRadar channel, from the CLI copy
421
+ // the service runs; the supervisor adds --dangerously-load-development-channels server:agentradar.
422
+ await writePrivateFile(mcpConfig, `${JSON.stringify(feed === "push"
423
+ ? { mcpServers: { agentradar: { type: "stdio", command: process.execPath, args: [join(cliDist(home), "index.js"), "channel", approved.runnerId] } } }
424
+ : service ? { mcpServers: {} } : {
425
+ mcpServers: {
426
+ agentradar: {
427
+ type: "http",
428
+ url: endpoint.toString(),
429
+ headers: { Authorization: `Bearer ${approved.installation.bearer}` },
430
+ },
421
431
  },
422
- },
423
- }, null, 2)}\n`);
432
+ }, null, 2)}\n`);
424
433
  await writePrivateFile(envPath, [
425
434
  `AGENT_NAME=${shellValue(approved.runnerId)}`,
426
435
  `AR_BASE=${shellValue(projectPath)}`,
@@ -431,6 +440,8 @@ async function installRunner(home, projectPath, approved, service) {
431
440
  // default, so writing `AR_AGENT_HARNESS=claude` unconditionally would change nothing except to make
432
441
  // every existing env file look as though a choice had been made.
433
442
  ...(harness ? [`AR_AGENT_HARNESS=${shellValue(harness)}`] : []),
443
+ // How work reaches the session (Vadym 10342): the Claude Code channel pushes it; without this line the feeder types it.
444
+ ...(feed ? [`AR_FEED=${feed}`] : []),
434
445
  ...joined,
435
446
  "",
436
447
  ].join("\n"));
@@ -520,27 +531,8 @@ export async function setupVendorAgent(deps = {}) {
520
531
  }
521
532
  throw new Error("AgentRadar setup code expired; run setup again");
522
533
  }
523
- /** "[y/N]": only y or yes (any case) is a yes; Enter alone is No. */
524
- export const saidYes = (answer) => /^y(es)?$/i.test(answer.trim());
525
- /** The default `confirm`: a real terminal only, "[y/N]" with No unless they type y or yes. */
526
- export function terminalConfirm() {
527
- if (!process.stdin.isTTY || !process.stdout.isTTY)
528
- return undefined;
529
- return async (question) => {
530
- const { createInterface } = await import("node:readline/promises");
531
- const rl = createInterface({ input: process.stdin, output: process.stdout });
532
- try {
533
- return saidYes(await rl.question(`${question} [y/N] `));
534
- }
535
- finally {
536
- rl.close();
537
- }
538
- };
539
- }
540
534
  export const bypassOnNotice = (link) => `Bypass-permissions mode is ON on this machine: the agent runs without permission prompts. To turn it off: agentradar join ${link} --revoke-bypass`;
541
535
  export const BYPASS_REVOKED = "Bypass-permissions mode is off on this machine now; the agent asks before acting again.";
542
- export const BYPASS_QUESTION = "Allow this agent to run without permission prompts on this machine?";
543
- export const BYPASS_NOT_CONFIRMED = "Bypass-permissions mode stays off: your organisation allowed it, but nobody on this machine confirmed it. Run the join again in a terminal and answer yes, or add --allow-bypass.";
544
536
  /** A sortable key for `x.y.z[-pre]`; a release sorts above its prereleases. ponytail: prereleases of one version tie. */
545
537
  const versionRank = (version) => {
546
538
  const [core = "", pre] = version.split(/-(.*)/);
@@ -646,6 +638,10 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
646
638
  }
647
639
  }
648
640
  const label = harness === "generic" ? basename((env.AGENTRADAR_AGENT_BIN ?? "").trim()) : harness === "claude" ? "Claude Code" : "Codex";
641
+ // Vadym 10342: every Claude Code agent connects through the AgentRadar channel. The tmux feeder is the fallback:
642
+ // another CLI, an explicit opt-in (--feeder, AGENTRADAR_FEED=feeder, or a pane named with --attach-*), or a
643
+ // channel the session never confirms (channel.ts reports it and tells the operator to re-run with --feeder).
644
+ const channel = harness === "claude" && !deps.feeder && env.AGENTRADAR_FEED?.trim().toLowerCase() !== "feeder" && !deps.attach;
649
645
  // The agent is a persistent session the service owns, not this one: no MCP entry in this folder (it acts
650
646
  // through `agentradar run`), a runner env pinned to its own session, and a LaunchAgent / systemd unit.
651
647
  // Which session the agent is, settled before any request: a key is issued once, so never claim one for a session that cannot run.
@@ -656,7 +652,8 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
656
652
  let attach = deps.attach === "current" ? env.TMUX_PANE : deps.attach;
657
653
  if (deps.attach === "current" && !attach)
658
654
  throw new Error("--attach-current needs a session running inside tmux");
659
- if (!deps.attach && harness === "claude" && env.CLAUDECODE && env.TMUX && env.TMUX_PANE)
655
+ // Never in channel mode: nothing types into the caller's pane.
656
+ if (!channel && !deps.attach && harness === "claude" && env.CLAUDECODE && env.TMUX && env.TMUX_PANE)
660
657
  attach = env.TMUX_PANE;
661
658
  if (attach) {
662
659
  // A pane id (%3) dies with the tmux server; session:window.pane comes back when its session is recreated.
@@ -669,31 +666,24 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
669
666
  // Unattended, the service resumes the caller's own Claude conversation, never a stranger's. An attached
670
667
  // pane is never started from here, so there its session id only keys the model switch.
671
668
  const sessionId = harness !== "claude" ? undefined
672
- : await callerSession(deps, env, home, await realpath(cwd)).catch((error) => { if (attach)
669
+ : await callerSession(deps, env, home, await realpath(cwd)).catch((error) => { if (attach || channel)
673
670
  return undefined; throw error; });
674
671
  return { attach, sessionId };
675
672
  };
676
673
  let where;
677
674
  /**
678
- * Lead 10-04: the approver's consent alone never turns bypass on here. This machine says yes too: a flag, or
679
- * a person at the terminal; with neither (an agent running the join, a script) it stays off and says why.
680
- * Asked only when it would apply: approved, and not an attached pane (that keeps its own mode).
675
+ * The owner's bypass grant on the portal approval is the consent for an unattended service session (lead
676
+ * 10-04, after the Codex run stopped on an approval prompt nobody could answer); `--revoke-bypass` takes it
677
+ * back on this machine. An attached pane keeps the permission mode it already runs in.
681
678
  */
682
- const localBypass = async (approvedBypass, confirmedBefore) => {
683
- if (!approvedBypass || where.attach)
684
- return false;
685
- const yes = confirmedBefore || deps.allowBypass === true || (deps.confirm ? await deps.confirm(BYPASS_QUESTION) : false);
686
- if (!yes)
687
- log(BYPASS_NOT_CONFIRMED);
688
- return yes;
689
- };
679
+ const localBypass = (approvedBypass, revoked) => approvedBypass && !revoked && !where.attach;
690
680
  const startJoined = async (requestId, agentId, installation, approvedBypass, bypass, saved) => {
691
681
  const runnerId = `join-${requestId}`;
692
682
  const { attach } = where;
693
683
  const projectPath = harness === "claude"
694
684
  ? await installProjectMcp(claudeConfigPath(home, env), cwd, null, deps.trust === true)
695
685
  : await realpath(cwd);
696
- const runnerEnv = await installRunner(home, projectPath, { status: "approved", agentId, runnerId, installation, harness }, { sessionId: where.sessionId ?? requestId, env, attach, bypass });
686
+ const runnerEnv = await installRunner(home, projectPath, { status: "approved", agentId, runnerId, installation, harness }, { sessionId: where.sessionId ?? requestId, env, attach, bypass }, channel ? "push" : undefined);
697
687
  await saved?.();
698
688
  log(`Runner installed → ${runnerEnv}`);
699
689
  if (bypass)
@@ -716,7 +706,13 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
716
706
  log("Running as a service; the delivery restarts on crash and reboot.");
717
707
  return 0;
718
708
  }
719
- const mode = bypass ? ", in bypass-permissions mode as your organisation allowed and this machine confirmed" : "";
709
+ const mode = bypass ? ", in bypass-permissions mode as your organisation allowed" : "";
710
+ if (channel) {
711
+ log(`Connected through the AgentRadar channel. This conversation now continues as a service in ${projectPath} (tmux session ${runnerId}${mode}): client messages are pushed into it and it answers there, with no restart from you. Exit this session now (/exit) so only the service carries on; watch it with: tmux attach -t ${runnerId}. To stop it: agentradar leave ${runnerId}`);
712
+ log(`If your organisation's Claude plan blocks channels, deliver with the feeder instead: agentradar join ${enrolmentUrl} --feeder`);
713
+ log("Running as a service; it restarts on crash and reboot.");
714
+ return 0;
715
+ }
720
716
  // Only Claude resumes the caller's own conversation; Codex and a generic CLI start their own session.
721
717
  log(harness === "claude"
722
718
  ? `Connected. This conversation now continues as a service in ${projectPath} (tmux session ${runnerId}${mode}): your welcome and every client message are typed into it, and it answers there. Exit this session now (/exit) so only the service carries on; watch it with: tmux attach -t ${runnerId}. To stop it: agentradar leave ${runnerId}`
@@ -756,14 +752,14 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
756
752
  const bearer = envValue(saved, "AWP_BEARER");
757
753
  if (!bearer)
758
754
  throw new Error("This machine left that organisation (agentradar leave). A key is issued once; ask your organisation for a new join link.");
759
- // Lead 10-04: the remembered yes is revocable. `--revoke-bypass` forgets it and the service runs with prompts.
760
- if (deps.revokeBypass && state.bypassConfirmed) {
755
+ // `--revoke-bypass` is remembered on this machine, so a later re-run keeps the prompts on.
756
+ if (deps.revokeBypass && state.bypassConfirmed !== false) {
761
757
  state = { ...state, bypassConfirmed: false };
762
758
  await writePrivateFile(statePath, JSON.stringify(state));
763
759
  log(BYPASS_REVOKED);
764
760
  }
765
761
  const approvedBypass = state.bypassPermissions === true;
766
- const bypass = deps.revokeBypass ? false : await localBypass(approvedBypass, state.bypassConfirmed === true);
762
+ const bypass = localBypass(approvedBypass, state.bypassConfirmed === false);
767
763
  return startJoined(state.requestId, state.agentId, { mcpEndpoint: envValue(saved, "AWP_MCP_URL") ?? mcpEndpoint, bearer }, approvedBypass, bypass);
768
764
  }
769
765
  if (!state) {
@@ -818,8 +814,8 @@ export async function joinOrganisation(enrolmentUrl, deps = {}) {
818
814
  log("Approved.");
819
815
  // Bypass-permissions only when the operator consented at approval; the server says so with the key.
820
816
  const approvedBypass = claimed.bypassPermissions === true;
821
- const bypass = await localBypass(approvedBypass, false);
822
- const joined = { ...state, agentId: claimed.agentId, ...(approvedBypass ? { bypassPermissions: true } : {}), ...(bypass ? { bypassConfirmed: true } : {}) };
817
+ const bypass = localBypass(approvedBypass, false);
818
+ const joined = { ...state, agentId: claimed.agentId, ...(approvedBypass ? { bypassPermissions: true } : {}) };
823
819
  return startJoined(state.requestId, claimed.agentId, { mcpEndpoint, bearer: claimed.token }, approvedBypass, bypass, () => writePrivateFile(statePath, JSON.stringify(joined)));
824
820
  }
825
821
  const said = await failureMessage(response);
@@ -77,7 +77,7 @@ HEALTH_POLL_SEC="${HEALTH_POLL_SEC:-15}"
77
77
  # interactive CLI: the env file names its binary, arguments, resume/fresh flags, pid pattern and how work
78
78
  # reaches it (AR_FEED). Anything else is REFUSED by name: a supervisor that quietly fell back to Claude for an
79
79
  # unrecognized value would come up healthy, keep a pane alive, and answer nothing the vendor expected.
80
- # Tried: claude and codex with AR_FEED=feeder. Untried: generic, AR_FEED=push (Claude channels).
80
+ # Tried: claude and codex with AR_FEED=feeder; claude with AR_FEED=push (the AgentRadar channel, R764). Untried: generic.
81
81
  AR_AGENT_HARNESS="${AR_AGENT_HARNESS:-claude}"
82
82
  case "${AR_AGENT_HARNESS}" in
83
83
  claude|codex|generic) : ;;
@@ -93,6 +93,8 @@ case "${AR_FEED}" in
93
93
  acp) die "AR_FEED=acp is not built yet; use feeder or push" ;;
94
94
  *) die "AR_FEED must be feeder or push (got '${AR_FEED}')" ;;
95
95
  esac
96
+ # push is Claude Code channels only: no other harness has a channel to push into.
97
+ [ "${AR_FEED}" = "push" ] && [ "${AR_AGENT_HARNESS}" != "claude" ] && die "AR_FEED=push needs AR_AGENT_HARNESS=claude (got '${AR_AGENT_HARNESS}')"
96
98
  # Launched only when the resume form exits non-zero (nothing to resume). Empty = the same argv without it.
97
99
  AR_FRESH_FLAGS="${AR_FRESH_FLAGS:-}"
98
100
 
@@ -140,6 +142,8 @@ else
140
142
  _launch_flags=( ${CLAUDE_EXTRA_FLAGS} --strict-mcp-config --mcp-config="${MCP_CONFIG}" )
141
143
  # `agentradar join` enables the agent-guard plugin for this session only, through a settings file.
142
144
  [ -n "${CLAUDE_SETTINGS:-}" ] && _launch_flags+=( --settings "${CLAUDE_SETTINGS}" )
145
+ # R764 + Vadym 10342: the AgentRadar channel (the stdio MCP `agentradar` in $MCP_CONFIG) pushes work in; no feeder types.
146
+ [ "${AR_FEED}" = "push" ] && _launch_flags+=( --dangerously-load-development-channels server:agentradar )
143
147
  # Pane-alive predicate — MUST be unique per agent. Default to the FULL mcp-config path (each agent's
144
148
  # differs by directory), NOT the basename: on a multi-agent box every agent's config is named
145
149
  # `awp-only.json`, so a basename match would find ANOTHER agent's live claude and mask THIS agent's
@@ -362,11 +366,27 @@ ensure_agent() {
362
366
  return 1
363
367
  }
364
368
  # wait for the pane to come up
365
- for _ in $(seq 1 30); do pane_alive && { log "agent pane is live"; return 0; }; sleep 1; done
369
+ # In the background: the watch must not hold up the monitor loop when the warning never shows.
370
+ for _ in $(seq 1 30); do pane_alive && { log "agent pane is live"; accept_dev_channels & return 0; }; sleep 1; done
366
371
  log "agent pane did not come up within 30s"
367
372
  return 1
368
373
  }
369
374
 
375
+ # A pushed session starts on Claude Code's "Loading development channels" warning, and nobody sits at an unattended
376
+ # pane to confirm it. The channel is this machine's own installed CLI (`agentradar channel <runner>`), so the
377
+ # service confirms it, once per launch, only while that exact warning is on screen.
378
+ accept_dev_channels() {
379
+ [ "${AR_FEED}" = "push" ] || return 0
380
+ for _ in $(seq 1 30); do
381
+ if "${TMUX_BIN}" capture-pane -p -t "=${TMUX_SESSION}:" 2>/dev/null | grep -q "Loading development channels"; then
382
+ "${TMUX_BIN}" send-keys -t "=${TMUX_SESSION}:" Enter
383
+ log "confirmed Claude Code's development-channels warning for the AgentRadar channel"
384
+ return 0
385
+ fi
386
+ sleep 1
387
+ done
388
+ }
389
+
370
390
  # --- 4. Run: ensure agent, run feeder in foreground, monitor both -------------
371
391
  FEEDER_PID=""
372
392
  cleanup() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentradar-cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },