agentradar-cli 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,10 +7,11 @@
7
7
  # all env params). It idempotently ensures BOTH halves of an always-on agent are
8
8
  # up, then blocks so KeepAlive/Restart revives the whole thing on any death:
9
9
  #
10
- # 1. a tmux session ($TMUX_SESSION) running the agent:
10
+ # 1. a tmux session ($TMUX_SESSION) running the agent CLI named by $AR_AGENT_HARNESS:
11
11
  # claude --continue $CLAUDE_EXTRA_FLAGS --strict-mcp-config --mcp-config=$MCP_CONFIG
12
- # in $AR_BASE, with a SCRUBBED allowlist env + the macOS user's normal
13
- # Keychain-backed Claude session. Strict MCP flags allow ONLY AgentRadar's config;
12
+ # codex resume --last $CODEX_EXTRA_FLAGS --cd $AR_BASE (MCP confined by $CODEX_HOME)
13
+ # in $AR_BASE, with a SCRUBBED allowlist env + the CLI's own persisted login.
14
+ # Strict MCP flags (Claude) / a per-agent CODEX_HOME (Codex) allow ONLY AgentRadar's config;
14
15
  # 2. the feeder ($FEEDER_JS) OUTSIDE that session, holding $AWP_BEARER, long-
15
16
  # polling next_event and tmux-injecting each event into the pane.
16
17
  #
@@ -20,7 +21,8 @@
20
21
  # starts it on boot → same. Composes with the stateless AWP MCP (#111/#112:
21
22
  # reconnect, no -32001) + `claude --continue` (context resumes) = hands-free revive.
22
23
  #
23
- # HARD RULE: claude.ai OAuth ONLY. Refuses to start if ANTHROPIC_API_KEY is set.
24
+ # HARD RULE (claude harness): claude.ai OAuth ONLY. Refuses to start if
25
+ # ANTHROPIC_API_KEY is set. Codex authenticates only through $CODEX_HOME/auth.json.
24
26
  # =============================================================================
25
27
  set -euo pipefail
26
28
 
@@ -51,7 +53,6 @@ set +a
51
53
  : "${AR_BASE:?AR_BASE required (the agent working dir)}"
52
54
  : "${AWP_MCP_URL:?AWP_MCP_URL required}"
53
55
  : "${AWP_BEARER:?AWP_BEARER required}"
54
- : "${MCP_CONFIG:?MCP_CONFIG required (path to awp-only.json)}"
55
56
  TMUX_SESSION="${TMUX_SESSION:-${AGENT_NAME}}"
56
57
  FEEDER_JS="${FEEDER_JS:-$(cd "$(dirname "$0")/.." && pwd)/feeder.mjs}"
57
58
  PANE_LIVE_SH="${PANE_LIVE_SH:-$(cd "$(dirname "$0")/.." && pwd)/pane-live.sh}"
@@ -68,11 +69,56 @@ BROWSER_APP="${BROWSER_APP:-Google Chrome}" # #150: app to ensure-up fo
68
69
  BROWSER_READY_MAX_SEC="${BROWSER_READY_MAX_SEC:-20}" # #150: bounded wait for the browser proc on cold boot
69
70
  BROWSER_READY_SETTLE_SEC="${BROWSER_READY_SETTLE_SEC:-3}" # #150: settle for extension/native-host after cold launch
70
71
  HEALTH_POLL_SEC="${HEALTH_POLL_SEC:-15}"
71
- # Pane-alive predicate — MUST be unique per agent. Default to the FULL mcp-config path (each agent's
72
- # differs by directory), NOT the basename: on a multi-agent box every agent's config is named
73
- # `awp-only.json`, so a basename match would find ANOTHER agent's live claude and mask THIS agent's
74
- # death (silent death — the exact failure #145 kills). Override per agent for extra scoping if needed.
75
- AGENT_PID_PATTERN="${AGENT_PID_PATTERN:-mcp-config.*${MCP_CONFIG}}"
72
+
73
+ # --- 2a. R004 HARNESS SELECTION — one documented variable picks the CLI ---------
74
+ # The Bible names two agent harnesses: Claude Code and Codex. `claude` is the default, so every existing
75
+ # agent env file keeps launching exactly what it launched before. Anything that is neither is REFUSED by
76
+ # name: a supervisor that quietly fell back to Claude for an unrecognized value would come up healthy,
77
+ # keep a pane alive, and answer nothing the vendor expected.
78
+ AR_AGENT_HARNESS="${AR_AGENT_HARNESS:-claude}"
79
+ case "${AR_AGENT_HARNESS}" in
80
+ claude|codex) : ;;
81
+ *) die "AR_AGENT_HARNESS must be 'claude' or 'codex' (got '${AR_AGENT_HARNESS}')" ;;
82
+ esac
83
+
84
+ # What actually differs between the two CLIs, measured against `claude` and codex-cli 0.149.1:
85
+ # the binary, the resume argument, how connectors are confined, and the argv the pane can be found by.
86
+ # Everything else below — the env -i allowlist, the 0600 bearer file, the feeder, the bridge, the recovery
87
+ # loop — is shared, because nothing about it is harness-specific.
88
+ if [ "${AR_AGENT_HARNESS}" = "codex" ]; then
89
+ AGENT_BIN="${CODEX_BIN:-codex}"
90
+ # `codex resume --last` continues the most recent recorded session and exits non-zero when there is none,
91
+ # so the launch falls back to a fresh `codex` exactly as the Claude path falls back from `--continue`.
92
+ AR_RESUME_FLAGS="resume --last"
93
+ # 🔴 CODEX HAS NO `--strict-mcp-config`. Its MCP servers are read from $CODEX_HOME/config.toml, so
94
+ # CODEX_HOME *is* the connector-isolation boundary rather than a convenience, and it is REQUIRED. Point it
95
+ # at a per-agent home whose config.toml declares the AgentRadar MCP and nothing else. Failing closed here
96
+ # is the point: letting it default to the vendor's own ~/.codex would hand the client's agent every MCP
97
+ # server that vendor has ever configured — exactly the exposure `--strict-mcp-config` closes on the Claude
98
+ # path, and it would present as a working agent.
99
+ [ -n "${CODEX_HOME:-}" ] || die "AR_AGENT_HARNESS=codex requires CODEX_HOME — a per-agent Codex home whose config.toml declares ONLY the AgentRadar MCP server"
100
+ [ -d "${CODEX_HOME}" ] || die "CODEX_HOME not found: ${CODEX_HOME}"
101
+ CODEX_EXTRA_FLAGS="${CODEX_EXTRA_FLAGS:-}"
102
+ # shellcheck disable=SC2086 # CODEX_EXTRA_FLAGS is intentionally unquoted so it word-splits into flags.
103
+ _launch_flags=( ${CODEX_EXTRA_FLAGS} --cd "${AR_BASE}" )
104
+ # Pane-alive predicate — MUST be unique per agent. `--cd <AR_BASE>` is the per-agent argv token on this
105
+ # harness (CODEX_HOME is an env var, invisible to `ps -o command=`), so it plays the role the full
106
+ # mcp-config path plays for Claude: a pattern that cannot match another agent's live process.
107
+ _agent_pid_pattern_default="${AGENT_BIN}.*--cd ${AR_BASE}"
108
+ else
109
+ AGENT_BIN="${CLAUDE_BIN}"
110
+ AR_RESUME_FLAGS="--continue"
111
+ : "${MCP_CONFIG:?MCP_CONFIG required (path to awp-only.json)}"
112
+ # SC2086: CLAUDE_EXTRA_FLAGS is intentionally UNQUOTED so "--chrome" (etc.) word-splits into flags.
113
+ # shellcheck disable=SC2086
114
+ _launch_flags=( ${CLAUDE_EXTRA_FLAGS} --strict-mcp-config --mcp-config="${MCP_CONFIG}" )
115
+ # Pane-alive predicate — MUST be unique per agent. Default to the FULL mcp-config path (each agent's
116
+ # differs by directory), NOT the basename: on a multi-agent box every agent's config is named
117
+ # `awp-only.json`, so a basename match would find ANOTHER agent's live claude and mask THIS agent's
118
+ # death (silent death — the exact failure #145 kills). Override per agent for extra scoping if needed.
119
+ _agent_pid_pattern_default="mcp-config.*${MCP_CONFIG}"
120
+ fi
121
+ AGENT_PID_PATTERN="${AGENT_PID_PATTERN:-${_agent_pid_pattern_default}}"
76
122
 
77
123
  # Optional per-agent TOOL credentials — a vendor brain's OWN MCP-tool API keys (e.g. an SEO agent's
78
124
  # WEBFLOW_TOKEN / PERPLEXITY_API_KEY). These are ADDED to the scrubbed launch env so the agent's own
@@ -97,7 +143,7 @@ if [ -n "${AGENT_TOOL_ENV_FILE}" ]; then
97
143
  # identity). Reject at parse time (visible warning) AND — belt-and-suspenders — the launch lists
98
144
  # the reserved allowlist AFTER TOOL_ENV_ARGS so `env` last-wins keeps reserved vars authoritative.
99
145
  case "${_tline%%=*}" in
100
- HOME|PATH|USER|LOGNAME|SHELL|TERM|COLORTERM|LANG|LC_ALL|LC_CTYPE|TMPDIR|NODE_OPTIONS|CLAUDE_CONFIG_DIR|CLAUDE_CODE_OAUTH_TOKEN|AR_OAUTH_TOKEN_FILE|AR_TOOL_ENV_FILE|ANTHROPIC_API_KEY|AWP_BEARER|AWP_BEARER_FILE|AWP_MCP_URL|TMUX_SESSION|AGENT_PID_PATTERN)
146
+ HOME|PATH|USER|LOGNAME|SHELL|TERM|COLORTERM|LANG|LC_ALL|LC_CTYPE|TMPDIR|NODE_OPTIONS|CLAUDE_CONFIG_DIR|CLAUDE_CODE_OAUTH_TOKEN|CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC|CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF|CODEX_HOME|AR_RESUME_FLAGS|AR_OAUTH_TOKEN_FILE|AR_TOOL_ENV_FILE|ANTHROPIC_API_KEY|AWP_BEARER|AWP_BEARER_FILE|AWP_MCP_URL|TMUX_SESSION|AGENT_PID_PATTERN)
101
147
  log "WARN ignoring reserved key '${_tline%%=*}' in ${AGENT_TOOL_ENV_FILE} (cannot override supervisor/agent env)"; continue ;;
102
148
  esac
103
149
  TOOL_ENV_ARGS+=("${_tline}")
@@ -110,11 +156,18 @@ fi
110
156
  # env -i below still scrubs everything else; strict MCP flags, not CLAUDE_CONFIG_DIR, isolate connectors.
111
157
  unset CLAUDE_CONFIG_DIR CLAUDE_CODE_OAUTH_TOKEN
112
158
 
113
- # HARD RULE — OAuth only, never a pay-as-you-go API key.
114
- [ -z "${ANTHROPIC_API_KEY:-}" ] || die "ANTHROPIC_API_KEY is set — refuse to start (claude.ai OAuth only)"
159
+ # HARD RULE — OAuth only, never a pay-as-you-go API key. Scoped to the Claude harness because that is where
160
+ # the variable decides anything: `claude` prefers ANTHROPIC_API_KEY over the subscription session, so its
161
+ # mere presence changes who pays. `codex` (>= 0.122) ignores OPENAI_API_KEY entirely and reads credentials
162
+ # only from $CODEX_HOME/auth.json, so there is no equivalent env var to refuse — a Codex vendor chooses
163
+ # subscription or API-key billing by how that home was logged in, and a refusal here would guard nothing
164
+ # while blocking boxes that carry the variable for unrelated work.
165
+ if [ "${AR_AGENT_HARNESS}" = "claude" ]; then
166
+ [ -z "${ANTHROPIC_API_KEY:-}" ] || die "ANTHROPIC_API_KEY is set — refuse to start (claude.ai OAuth only)"
167
+ fi
115
168
 
116
169
  command -v "${TMUX_BIN}" >/dev/null 2>&1 || die "tmux not found (${TMUX_BIN})"
117
- command -v "${CLAUDE_BIN}" >/dev/null 2>&1 || die "claude CLI not found (${CLAUDE_BIN})"
170
+ command -v "${AGENT_BIN}" >/dev/null 2>&1 || die "${AR_AGENT_HARNESS} CLI not found (${AGENT_BIN})"
118
171
  [ -f "${FEEDER_JS}" ] || die "feeder not found: ${FEEDER_JS}"
119
172
  [ -f "${PANE_LIVE_SH}" ] || die "pane liveness check not found: ${PANE_LIVE_SH}"
120
173
  [ -d "${AR_BASE}" ] || die "AR_BASE dir not found: ${AR_BASE}"
@@ -210,17 +263,18 @@ ensure_agent() {
210
263
  # The agent's own tool creds (from AGENT_TOOL_ENV_FILE) are NOT inlined here anymore (#166): they were staged to a 0600 file in 2b and are passed only as the PATH
211
264
  # AR_TOOL_ENV_FILE, which the wrapper reads+exports before exec — so no tool VALUE appears on the env -i
212
265
  # argv / tmux pane_start_command. Reserved keys were already rejected at tool-file parse time (2b).
213
- log "starting agent in tmux '${TMUX_SESSION}' (flags: ${CLAUDE_EXTRA_FLAGS:-none}; tool-env file: ${_TOOL_ENV_FILE:-none})"
214
- # SC2086: CLAUDE_EXTRA_FLAGS is intentionally UNQUOTED so "--chrome" (etc.) word-splits into flags.
266
+ log "starting agent (${AR_AGENT_HARNESS}) in tmux '${TMUX_SESSION}' (tool-env file: ${_TOOL_ENV_FILE:-none})"
215
267
  # _tool_env uses the ${arr[@]+"${arr[@]}"} form so an EMPTY array is safe under `set -u` (bash 3.2).
216
268
  # First-launch handling (#153/H4 durable-feeder): `claude --continue` EXITS 1 when the cwd has NO prior
217
269
  # conversation (for example, a fresh engagement folder) → the pane dies → the supervisor
218
- # crash-loops. So try `--continue`, and FALL BACK to a fresh `claude` (which stays up AND creates the
219
- # session) when there's nothing to resume. Self-healing: once a conversation exists, --continue resumes it;
270
+ # crash-loops. So try the resume form, and FALL BACK to a fresh session (which stays up AND creates the
271
+ # session) when there's nothing to resume. Self-healing: once a conversation exists, resume picks it up;
220
272
  # if it's ever lost, we cleanly start fresh instead of exit-looping. (--continue exits 1 on no-session,
221
- # verified — so the `||` fallback is reliable.) The `/bin/sh -c` wrapper keeps EXTRA_FLAGS word-splitting.
222
- # shellcheck disable=SC2086
223
- _launch_flags=( ${CLAUDE_EXTRA_FLAGS} --strict-mcp-config --mcp-config="${MCP_CONFIG}" )
273
+ # verified — so the `||` fallback is reliable, and `codex resume --last` behaves the same way.)
274
+ # R004: the resume argument is the ONE harness-specific token inside the wrapper, so it arrives as
275
+ # AR_RESUME_FLAGS ("--continue" or "resume --last") and is word-split there — the same idiom this launch
276
+ # already uses for the extra flags. The wrapper copies it to a local and unsets it before exec so it does
277
+ # not linger in the agent's environment.
224
278
  # #166: pass the tool creds as a FILE PATH (AR_TOOL_ENV_FILE), never inline KEY=VALUE on env -i.
225
279
  _tool_env=()
226
280
  [ -n "${_TOOL_ENV_FILE}" ] && _tool_env=( AR_TOOL_ENV_FILE="${_TOOL_ENV_FILE}" )
@@ -239,6 +293,10 @@ ensure_agent() {
239
293
  # visible once there is colour to look at. LC_ALL stays empty on purpose: an empty LC_ALL does not
240
294
  # override, so LC_CTYPE governs and a vendor can still set LANG per agent.
241
295
  #
296
+ # Channels: CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF=1 keeps the tengu_harbor flag readable from the
297
+ # disk cache even if telemetry is off, and it and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC are reserved so a
298
+ # tool file cannot turn channels off. See docs/claude-code-channels-gates.md.
299
+ #
242
300
  # 🔴 This comment block lives ABOVE the command, not inside it. A `#` comment between two
243
301
  # `\`-continued lines TERMINATES the command at its unescaped newline: tmux would receive only the
244
302
  # args up to SHELL=…, the pane would start with no `claude` at all, and the `TERM=… claude` tail
@@ -251,9 +309,10 @@ ensure_agent() {
251
309
  TERM="${AR_AGENT_TERM:-tmux-256color}" COLORTERM="${AR_AGENT_COLORTERM:-truecolor}" \
252
310
  LANG="${LANG:-en_US.UTF-8}" LC_ALL="${LC_ALL:-}" LC_CTYPE="${AR_AGENT_LC_CTYPE:-en_US.UTF-8}" \
253
311
  TMPDIR="${TMPDIR:-/tmp}" NODE_OPTIONS="${NODE_OPTIONS:-}" \
254
- DISABLE_AUTOUPDATER=1 \
312
+ CODEX_HOME="${CODEX_HOME:-}" AR_RESUME_FLAGS="${AR_RESUME_FLAGS}" \
313
+ DISABLE_AUTOUPDATER=1 CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF=1 \
255
314
  ${_tool_env[@]+"${_tool_env[@]}"} \
256
- /bin/sh -c 'if [ -n "${AR_TOOL_ENV_FILE:-}" ]; then while IFS= read -r _l || [ -n "$_l" ]; do case "$_l" in ""|\#*) continue ;; esac; _k=${_l%%=*}; _v=${_l#*=}; case "$_k" in HOME|PATH|USER|LOGNAME|SHELL|TERM|COLORTERM|LANG|LC_ALL|LC_CTYPE|TMPDIR|NODE_OPTIONS|CLAUDE_CONFIG_DIR|CLAUDE_CODE_OAUTH_TOKEN|AR_OAUTH_TOKEN_FILE|AR_TOOL_ENV_FILE|ANTHROPIC_API_KEY|AWP_BEARER|AWP_BEARER_FILE|AWP_MCP_URL|TMUX_SESSION|AGENT_PID_PATTERN) continue ;; esac; export "$_k=$_v"; done < "$AR_TOOL_ENV_FILE"; unset AR_TOOL_ENV_FILE _l _k _v; fi; CB="$1"; shift; "$CB" --continue "$@" || exec "$CB" "$@"' sh "${CLAUDE_BIN}" "${_launch_flags[@]}" || {
315
+ /bin/sh -c 'if [ -n "${AR_TOOL_ENV_FILE:-}" ]; then while IFS= read -r _l || [ -n "$_l" ]; do case "$_l" in ""|\#*) continue ;; esac; _k=${_l%%=*}; _v=${_l#*=}; case "$_k" in HOME|PATH|USER|LOGNAME|SHELL|TERM|COLORTERM|LANG|LC_ALL|LC_CTYPE|TMPDIR|NODE_OPTIONS|CLAUDE_CONFIG_DIR|CLAUDE_CODE_OAUTH_TOKEN|CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC|CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF|CODEX_HOME|AR_RESUME_FLAGS|AR_OAUTH_TOKEN_FILE|AR_TOOL_ENV_FILE|ANTHROPIC_API_KEY|AWP_BEARER|AWP_BEARER_FILE|AWP_MCP_URL|TMUX_SESSION|AGENT_PID_PATTERN) continue ;; esac; export "$_k=$_v"; done < "$AR_TOOL_ENV_FILE"; unset AR_TOOL_ENV_FILE _l _k _v; fi; RF="${AR_RESUME_FLAGS:-}"; unset AR_RESUME_FLAGS; CB="$1"; shift; "$CB" $RF "$@" || exec "$CB" "$@"' sh "${AGENT_BIN}" "${_launch_flags[@]}" || {
257
316
  log "tmux could not start agent session '${TMUX_SESSION}'"
258
317
  return 1
259
318
  }
@@ -282,7 +341,8 @@ log "starting feeder (${FEEDER_JS}) → session '${TMUX_SESSION}'"
282
341
  # #152: pass AWP_BEARER_FILE (a 0600 PATH), NEVER the raw AWP_BEARER — the feeder reads the file, so the
283
342
  # bearer is not in the feeder process's environment (`ps eww`) or argv. AWP_MCP_URL is not a secret.
284
343
  AWP_MCP_URL="${AWP_MCP_URL}" AWP_BEARER_FILE="${AWP_BEARER_FILE}" TMUX_SESSION="${TMUX_SESSION}" \
285
- AGENT_PID_PATTERN="${AGENT_PID_PATTERN}" CLAUDE_BIN="${CLAUDE_BIN}" node "${FEEDER_JS}" &
344
+ AGENT_PID_PATTERN="${AGENT_PID_PATTERN}" AR_AGENT_HARNESS="${AR_AGENT_HARNESS}" AGENT_BIN="${AGENT_BIN}" \
345
+ node "${FEEDER_JS}" &
286
346
  FEEDER_PID=$!
287
347
  log "feeder pid ${FEEDER_PID}"
288
348
 
@@ -316,7 +376,7 @@ while true; do
316
376
  exit 1
317
377
  fi
318
378
  if ! pane_alive; then
319
- log "agent claude pane died — recovering it in place"
379
+ log "agent pane died — recovering it in place"
320
380
  if ensure_agent; then
321
381
  if [ -n "${BRIDGE_PID}" ]; then
322
382
  kill "${BRIDGE_PID}" 2>/dev/null || true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentradar-cli",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Secure AgentRadar vendor setup and skill installer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,10 @@
10
10
  "dist/index.js",
11
11
  "dist/setup.js",
12
12
  "dist/runner.js",
13
+ "dist/channel.js",
13
14
  "dist/skill-install.js",
15
+ "dist/tool-run.js",
16
+ "dist/sync.js",
14
17
  "dist/vendor-runner/**",
15
18
  "dist/agentradar-vendor/SKILL.md",
16
19
  "README.md"