agentradar-cli 0.1.9 → 0.2.1
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/dist/agentradar-vendor/SKILL.md +6 -0
- package/dist/channel.js +150 -0
- package/dist/index.js +113 -10
- package/dist/runner.js +434 -8
- package/dist/setup.js +360 -20
- package/dist/skill-install.js +2 -0
- package/dist/tool-run.js +300 -0
- package/dist/vendor-runner/feeder.mjs +237 -30
- package/dist/vendor-runner/session-frame.mjs +37 -3
- package/dist/vendor-runner/supervisor/agentradar-agent@.service.template +31 -0
- package/dist/vendor-runner/supervisor/com.agentradar.agent.plist.template +62 -0
- package/dist/vendor-runner/supervisor/install-agent.sh +94 -0
- package/dist/vendor-runner/supervisor/supervise-agent.sh +138 -37
- package/package.json +4 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# AgentRadar durable-supervisor installer (M3.6 H4 / #92).
|
|
4
|
+
# Renders the launchd LaunchAgent (macOS) or systemd unit (Linux) for ONE agent
|
|
5
|
+
# from the template + its env file. STAGE-ONLY by default: it writes the unit to a
|
|
6
|
+
# staging dir and PRINTS the load command — it does NOT load/start anything (so it
|
|
7
|
+
# can never disrupt a running agent). Pass --load to actually install+load.
|
|
8
|
+
#
|
|
9
|
+
# ./install-agent.sh <agent-name> [--load]
|
|
10
|
+
#
|
|
11
|
+
# The unit runs `agentradar runner start <agent-name>` (the AgentRadar CLI, found on PATH, or NODE_BIN +
|
|
12
|
+
# AGENTRADAR_JS when `agentradar join` calls this with the CLI that is running it), which reads
|
|
13
|
+
# ~/.agentradar/agents/<agent-name>.env and runs the agent's supervisor plus one per client hire.
|
|
14
|
+
#
|
|
15
|
+
# Idempotent: re-rendering overwrites the staged unit; --load reloads it.
|
|
16
|
+
# =============================================================================
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
20
|
+
AGENT_NAME="${1:-}"
|
|
21
|
+
[ -n "${AGENT_NAME}" ] || { echo "usage: $0 <agent-name> [--load]" >&2; exit 2; }
|
|
22
|
+
shift || true
|
|
23
|
+
|
|
24
|
+
ENV_FILE="${HOME}/.agentradar/agents/${AGENT_NAME}.env"
|
|
25
|
+
DO_LOAD=0
|
|
26
|
+
while [ $# -gt 0 ]; do
|
|
27
|
+
case "$1" in
|
|
28
|
+
--load) DO_LOAD=1; shift ;;
|
|
29
|
+
*) echo "unknown arg: $1" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
[ -f "${ENV_FILE}" ] || { echo "env file not found: ${ENV_FILE} (copy agent.env.example, fill it, chmod 600)" >&2; exit 1; }
|
|
33
|
+
|
|
34
|
+
# Pull the few values we need to render the unit (without exporting secrets globally).
|
|
35
|
+
# `agentradar setup` writes values single-quoted; the quotes are shell syntax, not part of the value.
|
|
36
|
+
get() { grep -E "^${1}=" "${ENV_FILE}" | tail -1 | cut -d= -f2- | sed -e "s/^'\\(.*\\)'\$/\\1/" ; }
|
|
37
|
+
AR_BASE="$(get AR_BASE)"; [ -n "${AR_BASE}" ] || { echo "AR_BASE missing in ${ENV_FILE}" >&2; exit 1; }
|
|
38
|
+
# launchd and systemd start with a bare PATH, so the unit names node and the CLI by absolute path.
|
|
39
|
+
NODE_BIN="${NODE_BIN:-$(command -v node)}" || { echo "node not found on PATH" >&2; exit 1; }
|
|
40
|
+
if [ -z "${AGENTRADAR_JS:-}" ]; then
|
|
41
|
+
AGENTRADAR_BIN="$(command -v agentradar)" || { echo "agentradar CLI not found on PATH (npm i -g agentradar-cli)" >&2; exit 1; }
|
|
42
|
+
AGENTRADAR_JS="$("${NODE_BIN}" -e 'process.stdout.write(require("fs").realpathSync(process.argv[1]))' "${AGENTRADAR_BIN}")"
|
|
43
|
+
fi
|
|
44
|
+
# A pnpm or Volta shim resolves to a shell script, which node cannot run.
|
|
45
|
+
case "${AGENTRADAR_JS}" in *.js) ;; *) echo "agentradar on PATH is not a JavaScript file (${AGENTRADAR_JS}); install it with npm i -g agentradar-cli" >&2; exit 1 ;; esac
|
|
46
|
+
LOG_DIR="${HOME}/.agentradar/logs"; mkdir -p "${LOG_DIR}"
|
|
47
|
+
STAGE_DIR="${HOME}/.agentradar/staged"; mkdir -p "${STAGE_DIR}"
|
|
48
|
+
|
|
49
|
+
uname_s="$(uname -s)"
|
|
50
|
+
if [ "${uname_s}" = "Darwin" ]; then
|
|
51
|
+
LABEL="com.agentradar.agent.${AGENT_NAME}"
|
|
52
|
+
OUT="${STAGE_DIR}/${LABEL}.plist"
|
|
53
|
+
EXTRA_ENV=""
|
|
54
|
+
[ -n "${TMUX_TMPDIR:-}" ] && EXTRA_ENV="<key>TMUX_TMPDIR</key><string>${TMUX_TMPDIR}</string>"
|
|
55
|
+
sed -e "s#__AGENT_NAME__#${AGENT_NAME}#g" \
|
|
56
|
+
-e "s#__HOME__#${HOME}#g" \
|
|
57
|
+
-e "s#__PATH__#${PATH}#g" \
|
|
58
|
+
-e "s#__EXTRA_ENV__#${EXTRA_ENV}#g" \
|
|
59
|
+
-e "s#__NODE__#${NODE_BIN}#g" \
|
|
60
|
+
-e "s#__AGENTRADAR_JS__#${AGENTRADAR_JS}#g" \
|
|
61
|
+
-e "s#__AR_BASE__#${AR_BASE}#g" \
|
|
62
|
+
-e "s#__LOG_DIR__#${LOG_DIR}#g" \
|
|
63
|
+
"${HERE}/com.agentradar.agent.plist.template" > "${OUT}"
|
|
64
|
+
echo "STAGED (not loaded): ${OUT}"
|
|
65
|
+
if [ "${DO_LOAD}" = "1" ]; then
|
|
66
|
+
mkdir -p "${HOME}/Library/LaunchAgents"
|
|
67
|
+
install -m 644 "${OUT}" "${HOME}/Library/LaunchAgents/${LABEL}.plist"
|
|
68
|
+
launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true
|
|
69
|
+
launchctl bootstrap "gui/$(id -u)" "${HOME}/Library/LaunchAgents/${LABEL}.plist"
|
|
70
|
+
echo "LOADED: ${LABEL}"
|
|
71
|
+
else
|
|
72
|
+
echo "To load: install -m 644 '${OUT}' ~/Library/LaunchAgents/${LABEL}.plist && launchctl load ~/Library/LaunchAgents/${LABEL}.plist"
|
|
73
|
+
fi
|
|
74
|
+
else
|
|
75
|
+
UNIT="${STAGE_DIR}/agentradar-agent@.service"
|
|
76
|
+
sed -e "s#__RUNNER__#${NODE_BIN} ${AGENTRADAR_JS} runner start#g" \
|
|
77
|
+
-e "s#__PATH__#${PATH}#g" \
|
|
78
|
+
"${HERE}/agentradar-agent@.service.template" > "${UNIT}"
|
|
79
|
+
echo "STAGED (not loaded): ${UNIT}"
|
|
80
|
+
if [ "${DO_LOAD}" = "1" ]; then
|
|
81
|
+
mkdir -p "${HOME}/.config/systemd/user"
|
|
82
|
+
install -m 644 "${UNIT}" "${HOME}/.config/systemd/user/agentradar-agent@.service"
|
|
83
|
+
systemctl --user daemon-reload
|
|
84
|
+
systemctl --user enable --now "agentradar-agent@${AGENT_NAME}"
|
|
85
|
+
# Linger starts the user manager at boot, so the agent comes back without anyone logging in.
|
|
86
|
+
if loginctl enable-linger "${USER:-$(id -un)}" 2>/dev/null; then
|
|
87
|
+
echo "LOADED: agentradar-agent@${AGENT_NAME} (linger on: starts at boot)"
|
|
88
|
+
else
|
|
89
|
+
echo "LOADED: agentradar-agent@${AGENT_NAME} (could not enable linger; run 'loginctl enable-linger ${USER:-$(id -un)}' for boot-survival)"
|
|
90
|
+
fi
|
|
91
|
+
else
|
|
92
|
+
echo "To load: install -m 644 '${UNIT}' ~/.config/systemd/user/ && systemctl --user daemon-reload && systemctl --user enable --now agentradar-agent@${AGENT_NAME} (+ loginctl enable-linger ${USER})"
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
@@ -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
|
-
#
|
|
13
|
-
#
|
|
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
|
|
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
|
|
|
@@ -29,9 +31,10 @@ die() { log "FATAL: $*"; exit 1; }
|
|
|
29
31
|
|
|
30
32
|
# A launchd LaunchAgent (macOS) / systemd unit gives a MINIMAL PATH (typically /usr/bin:/bin) that
|
|
31
33
|
# lacks Homebrew — where tmux / claude / node live on a dev box. Without this, `command -v tmux` fails
|
|
32
|
-
# on boot and the supervisor crash-loops ("tmux not found").
|
|
34
|
+
# on boot and the supervisor crash-loops ("tmux not found"). APPEND the common tool dirs (Apple-Silicon
|
|
33
35
|
# + Intel Homebrew) so the unit finds them; this PATH also flows to the scrubbed agent launch below.
|
|
34
|
-
|
|
36
|
+
# Appended, not prepended: `agentradar join` pins the vendor's own PATH into the unit, and that order wins.
|
|
37
|
+
export PATH="${PATH:-/usr/bin:/bin}:/opt/homebrew/bin:/usr/local/bin"
|
|
35
38
|
|
|
36
39
|
# --- 1. Load the per-agent env ------------------------------------------------
|
|
37
40
|
# Env file: $1, or $AGENT_ENV, or ~/.agentradar/agents/<name>.env
|
|
@@ -51,7 +54,6 @@ set +a
|
|
|
51
54
|
: "${AR_BASE:?AR_BASE required (the agent working dir)}"
|
|
52
55
|
: "${AWP_MCP_URL:?AWP_MCP_URL required}"
|
|
53
56
|
: "${AWP_BEARER:?AWP_BEARER required}"
|
|
54
|
-
: "${MCP_CONFIG:?MCP_CONFIG required (path to awp-only.json)}"
|
|
55
57
|
TMUX_SESSION="${TMUX_SESSION:-${AGENT_NAME}}"
|
|
56
58
|
FEEDER_JS="${FEEDER_JS:-$(cd "$(dirname "$0")/.." && pwd)/feeder.mjs}"
|
|
57
59
|
PANE_LIVE_SH="${PANE_LIVE_SH:-$(cd "$(dirname "$0")/.." && pwd)/pane-live.sh}"
|
|
@@ -68,11 +70,80 @@ BROWSER_APP="${BROWSER_APP:-Google Chrome}" # #150: app to ensure-up fo
|
|
|
68
70
|
BROWSER_READY_MAX_SEC="${BROWSER_READY_MAX_SEC:-20}" # #150: bounded wait for the browser proc on cold boot
|
|
69
71
|
BROWSER_READY_SETTLE_SEC="${BROWSER_READY_SETTLE_SEC:-3}" # #150: settle for extension/native-host after cold launch
|
|
70
72
|
HEALTH_POLL_SEC="${HEALTH_POLL_SEC:-15}"
|
|
71
|
-
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
|
|
73
|
+
|
|
74
|
+
# --- 2a. R004 HARNESS SELECTION — one documented variable picks the CLI ---------
|
|
75
|
+
# The Bible names two agent harnesses: Claude Code and Codex. `claude` is the default, so every existing
|
|
76
|
+
# agent env file keeps launching exactly what it launched before. `generic` is the slot for any other
|
|
77
|
+
# interactive CLI: the env file names its binary, arguments, resume/fresh flags, pid pattern and how work
|
|
78
|
+
# reaches it (AR_FEED). Anything else is REFUSED by name: a supervisor that quietly fell back to Claude for an
|
|
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), self-poll.
|
|
81
|
+
AR_AGENT_HARNESS="${AR_AGENT_HARNESS:-claude}"
|
|
82
|
+
case "${AR_AGENT_HARNESS}" in
|
|
83
|
+
claude|codex|generic) : ;;
|
|
84
|
+
*) die "AR_AGENT_HARNESS must be 'claude', 'codex' or 'generic' (got '${AR_AGENT_HARNESS}')" ;;
|
|
85
|
+
esac
|
|
86
|
+
# How work reaches the session. feeder: the feeder polls next_event and types each event into the pane.
|
|
87
|
+
# push: the session's own MCP pushes it (Claude channels). self-poll: the agent polls for itself. Neither
|
|
88
|
+
# of the last two starts a feeder. ACP is named so a vendor asking for it is told plainly it is not built.
|
|
89
|
+
AR_FEED="${AR_FEED:-feeder}"
|
|
90
|
+
case "${AR_FEED}" in
|
|
91
|
+
feeder|push|self-poll) : ;;
|
|
92
|
+
acp) die "AR_FEED=acp is not built yet; use feeder, push or self-poll" ;;
|
|
93
|
+
*) die "AR_FEED must be feeder, push or self-poll (got '${AR_FEED}')" ;;
|
|
94
|
+
esac
|
|
95
|
+
# Launched only when the resume form exits non-zero (nothing to resume). Empty = the same argv without it.
|
|
96
|
+
AR_FRESH_FLAGS="${AR_FRESH_FLAGS:-}"
|
|
97
|
+
|
|
98
|
+
# What actually differs between the two CLIs, measured against `claude` and codex-cli 0.149.1:
|
|
99
|
+
# the binary, the resume argument, how connectors are confined, and the argv the pane can be found by.
|
|
100
|
+
# Everything else below — the env -i allowlist, the 0600 bearer file, the feeder, the bridge, the recovery
|
|
101
|
+
# loop — is shared, because nothing about it is harness-specific.
|
|
102
|
+
if [ "${AR_AGENT_HARNESS}" = "codex" ]; then
|
|
103
|
+
AGENT_BIN="${CODEX_BIN:-codex}"
|
|
104
|
+
# `codex resume --last` continues the most recent recorded session and exits non-zero when there is none,
|
|
105
|
+
# so the launch falls back to a fresh `codex` exactly as the Claude path falls back from `--continue`.
|
|
106
|
+
AR_RESUME_FLAGS="${AR_RESUME_FLAGS:-resume --last}"
|
|
107
|
+
# 🔴 CODEX HAS NO `--strict-mcp-config`. Its MCP servers are read from $CODEX_HOME/config.toml, so
|
|
108
|
+
# CODEX_HOME *is* the connector-isolation boundary rather than a convenience, and it is REQUIRED. Point it
|
|
109
|
+
# at a per-agent home whose config.toml declares the AgentRadar MCP and nothing else. Failing closed here
|
|
110
|
+
# is the point: letting it default to the vendor's own ~/.codex would hand the client's agent every MCP
|
|
111
|
+
# server that vendor has ever configured — exactly the exposure `--strict-mcp-config` closes on the Claude
|
|
112
|
+
# path, and it would present as a working agent.
|
|
113
|
+
[ -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"
|
|
114
|
+
[ -d "${CODEX_HOME}" ] || die "CODEX_HOME not found: ${CODEX_HOME}"
|
|
115
|
+
CODEX_EXTRA_FLAGS="${CODEX_EXTRA_FLAGS:-}"
|
|
116
|
+
# shellcheck disable=SC2086 # CODEX_EXTRA_FLAGS is intentionally unquoted so it word-splits into flags.
|
|
117
|
+
_launch_flags=( ${CODEX_EXTRA_FLAGS} --cd "${AR_BASE}" )
|
|
118
|
+
# Pane-alive predicate — MUST be unique per agent. `--cd <AR_BASE>` is the per-agent argv token on this
|
|
119
|
+
# harness (CODEX_HOME is an env var, invisible to `ps -o command=`), so it plays the role the full
|
|
120
|
+
# mcp-config path plays for Claude: a pattern that cannot match another agent's live process.
|
|
121
|
+
_agent_pid_pattern_default="${AGENT_BIN}.*--cd ${AR_BASE}"
|
|
122
|
+
elif [ "${AR_AGENT_HARNESS}" = "generic" ]; then
|
|
123
|
+
: "${AR_AGENT_BIN:?AR_AGENT_HARNESS=generic requires AR_AGENT_BIN (the CLI to run)}"
|
|
124
|
+
# No default pattern exists for a CLI we know nothing about, and a loose one would mask this agent's death.
|
|
125
|
+
: "${AGENT_PID_PATTERN:?AR_AGENT_HARNESS=generic requires AGENT_PID_PATTERN (a ps pattern unique to this agent)}"
|
|
126
|
+
AGENT_BIN="${AR_AGENT_BIN}"
|
|
127
|
+
AR_RESUME_FLAGS="${AR_RESUME_FLAGS:-}"
|
|
128
|
+
# shellcheck disable=SC2086 # AR_AGENT_ARGS word-splits into flags, like the extra flags above.
|
|
129
|
+
_launch_flags=( ${AR_AGENT_ARGS:-} )
|
|
130
|
+
_agent_pid_pattern_default=""
|
|
131
|
+
else
|
|
132
|
+
AGENT_BIN="${CLAUDE_BIN}"
|
|
133
|
+
# `agentradar join` pins `--resume <id>` / `--session-id <id>` so the service never resumes the vendor's
|
|
134
|
+
# own conversation in the same folder; an env file that sets nothing keeps `--continue`.
|
|
135
|
+
AR_RESUME_FLAGS="${AR_RESUME_FLAGS:---continue}"
|
|
136
|
+
: "${MCP_CONFIG:?MCP_CONFIG required (path to awp-only.json)}"
|
|
137
|
+
# SC2086: CLAUDE_EXTRA_FLAGS is intentionally UNQUOTED so "--chrome" (etc.) word-splits into flags.
|
|
138
|
+
# shellcheck disable=SC2086
|
|
139
|
+
_launch_flags=( ${CLAUDE_EXTRA_FLAGS} --strict-mcp-config --mcp-config="${MCP_CONFIG}" )
|
|
140
|
+
# Pane-alive predicate — MUST be unique per agent. Default to the FULL mcp-config path (each agent's
|
|
141
|
+
# differs by directory), NOT the basename: on a multi-agent box every agent's config is named
|
|
142
|
+
# `awp-only.json`, so a basename match would find ANOTHER agent's live claude and mask THIS agent's
|
|
143
|
+
# death (silent death — the exact failure #145 kills). Override per agent for extra scoping if needed.
|
|
144
|
+
_agent_pid_pattern_default="mcp-config.*${MCP_CONFIG}"
|
|
145
|
+
fi
|
|
146
|
+
AGENT_PID_PATTERN="${AGENT_PID_PATTERN:-${_agent_pid_pattern_default}}"
|
|
76
147
|
|
|
77
148
|
# Optional per-agent TOOL credentials — a vendor brain's OWN MCP-tool API keys (e.g. an SEO agent's
|
|
78
149
|
# WEBFLOW_TOKEN / PERPLEXITY_API_KEY). These are ADDED to the scrubbed launch env so the agent's own
|
|
@@ -97,7 +168,7 @@ if [ -n "${AGENT_TOOL_ENV_FILE}" ]; then
|
|
|
97
168
|
# identity). Reject at parse time (visible warning) AND — belt-and-suspenders — the launch lists
|
|
98
169
|
# the reserved allowlist AFTER TOOL_ENV_ARGS so `env` last-wins keeps reserved vars authoritative.
|
|
99
170
|
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)
|
|
171
|
+
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_FRESH_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
172
|
log "WARN ignoring reserved key '${_tline%%=*}' in ${AGENT_TOOL_ENV_FILE} (cannot override supervisor/agent env)"; continue ;;
|
|
102
173
|
esac
|
|
103
174
|
TOOL_ENV_ARGS+=("${_tline}")
|
|
@@ -108,13 +179,25 @@ fi
|
|
|
108
179
|
# Interactive Claude must use the user's persisted Keychain-backed login. An isolated config or an
|
|
109
180
|
# environment OAuth token selects a different auth contract and can put the TUI back into onboarding.
|
|
110
181
|
# env -i below still scrubs everything else; strict MCP flags, not CLAUDE_CONFIG_DIR, isolate connectors.
|
|
111
|
-
|
|
182
|
+
# Exception: `agentradar join` (it sets AR_CLI_RUNNER_ID) records the CLAUDE_CONFIG_DIR the vendor's own
|
|
183
|
+
# claude was logged in under, and the service must use that same login. Older env files keep the unset.
|
|
184
|
+
[ -n "${AR_CLI_RUNNER_ID:-}" ] || unset CLAUDE_CONFIG_DIR
|
|
185
|
+
unset CLAUDE_CODE_OAUTH_TOKEN
|
|
186
|
+
_claude_cfg=()
|
|
187
|
+
[ -n "${CLAUDE_CONFIG_DIR:-}" ] && _claude_cfg=( CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR}" )
|
|
112
188
|
|
|
113
|
-
# HARD RULE — OAuth only, never a pay-as-you-go API key.
|
|
114
|
-
|
|
189
|
+
# HARD RULE — OAuth only, never a pay-as-you-go API key. Scoped to the Claude harness because that is where
|
|
190
|
+
# the variable decides anything: `claude` prefers ANTHROPIC_API_KEY over the subscription session, so its
|
|
191
|
+
# mere presence changes who pays. `codex` (>= 0.122) ignores OPENAI_API_KEY entirely and reads credentials
|
|
192
|
+
# only from $CODEX_HOME/auth.json, so there is no equivalent env var to refuse — a Codex vendor chooses
|
|
193
|
+
# subscription or API-key billing by how that home was logged in, and a refusal here would guard nothing
|
|
194
|
+
# while blocking boxes that carry the variable for unrelated work.
|
|
195
|
+
if [ "${AR_AGENT_HARNESS}" = "claude" ]; then
|
|
196
|
+
[ -z "${ANTHROPIC_API_KEY:-}" ] || die "ANTHROPIC_API_KEY is set — refuse to start (claude.ai OAuth only)"
|
|
197
|
+
fi
|
|
115
198
|
|
|
116
199
|
command -v "${TMUX_BIN}" >/dev/null 2>&1 || die "tmux not found (${TMUX_BIN})"
|
|
117
|
-
command -v "${
|
|
200
|
+
command -v "${AGENT_BIN}" >/dev/null 2>&1 || die "${AR_AGENT_HARNESS} CLI not found (${AGENT_BIN})"
|
|
118
201
|
[ -f "${FEEDER_JS}" ] || die "feeder not found: ${FEEDER_JS}"
|
|
119
202
|
[ -f "${PANE_LIVE_SH}" ] || die "pane liveness check not found: ${PANE_LIVE_SH}"
|
|
120
203
|
[ -d "${AR_BASE}" ] || die "AR_BASE dir not found: ${AR_BASE}"
|
|
@@ -210,17 +293,20 @@ ensure_agent() {
|
|
|
210
293
|
# 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
294
|
# AR_TOOL_ENV_FILE, which the wrapper reads+exports before exec — so no tool VALUE appears on the env -i
|
|
212
295
|
# argv / tmux pane_start_command. Reserved keys were already rejected at tool-file parse time (2b).
|
|
213
|
-
log "starting agent in tmux '${TMUX_SESSION}' (
|
|
214
|
-
# SC2086: CLAUDE_EXTRA_FLAGS is intentionally UNQUOTED so "--chrome" (etc.) word-splits into flags.
|
|
296
|
+
log "starting agent (${AR_AGENT_HARNESS}) in tmux '${TMUX_SESSION}' (tool-env file: ${_TOOL_ENV_FILE:-none})"
|
|
215
297
|
# _tool_env uses the ${arr[@]+"${arr[@]}"} form so an EMPTY array is safe under `set -u` (bash 3.2).
|
|
216
298
|
# First-launch handling (#153/H4 durable-feeder): `claude --continue` EXITS 1 when the cwd has NO prior
|
|
217
299
|
# conversation (for example, a fresh engagement folder) → the pane dies → the supervisor
|
|
218
|
-
# crash-loops. So try
|
|
219
|
-
# session) when there's nothing to resume. Self-healing: once a conversation exists,
|
|
300
|
+
# crash-loops. So try the resume form, and FALL BACK to a fresh session (which stays up AND creates the
|
|
301
|
+
# session) when there's nothing to resume. Self-healing: once a conversation exists, resume picks it up;
|
|
220
302
|
# 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
|
|
222
|
-
#
|
|
223
|
-
|
|
303
|
+
# verified — so the `||` fallback is reliable, and `codex resume --last` behaves the same way.)
|
|
304
|
+
# Only a resume that fails within 30 s falls back. A live session that crashes or is killed later ends the
|
|
305
|
+
# pane instead, and the watchdog resumes it — a fresh session there would drop the conversation.
|
|
306
|
+
# R004: the resume argument is the ONE harness-specific token inside the wrapper, so it arrives as
|
|
307
|
+
# AR_RESUME_FLAGS ("--continue" or "resume --last") and is word-split there — the same idiom this launch
|
|
308
|
+
# already uses for the extra flags. The wrapper copies it to a local and unsets it before exec so it does
|
|
309
|
+
# not linger in the agent's environment.
|
|
224
310
|
# #166: pass the tool creds as a FILE PATH (AR_TOOL_ENV_FILE), never inline KEY=VALUE on env -i.
|
|
225
311
|
_tool_env=()
|
|
226
312
|
[ -n "${_TOOL_ENV_FILE}" ] && _tool_env=( AR_TOOL_ENV_FILE="${_TOOL_ENV_FILE}" )
|
|
@@ -239,6 +325,10 @@ ensure_agent() {
|
|
|
239
325
|
# visible once there is colour to look at. LC_ALL stays empty on purpose: an empty LC_ALL does not
|
|
240
326
|
# override, so LC_CTYPE governs and a vendor can still set LANG per agent.
|
|
241
327
|
#
|
|
328
|
+
# Channels: CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF=1 keeps the tengu_harbor flag readable from the
|
|
329
|
+
# disk cache even if telemetry is off, and it and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC are reserved so a
|
|
330
|
+
# tool file cannot turn channels off. See docs/claude-code-channels-gates.md.
|
|
331
|
+
#
|
|
242
332
|
# 🔴 This comment block lives ABOVE the command, not inside it. A `#` comment between two
|
|
243
333
|
# `\`-continued lines TERMINATES the command at its unescaped newline: tmux would receive only the
|
|
244
334
|
# args up to SHELL=…, the pane would start with no `claude` at all, and the `TERM=… claude` tail
|
|
@@ -251,9 +341,10 @@ ensure_agent() {
|
|
|
251
341
|
TERM="${AR_AGENT_TERM:-tmux-256color}" COLORTERM="${AR_AGENT_COLORTERM:-truecolor}" \
|
|
252
342
|
LANG="${LANG:-en_US.UTF-8}" LC_ALL="${LC_ALL:-}" LC_CTYPE="${AR_AGENT_LC_CTYPE:-en_US.UTF-8}" \
|
|
253
343
|
TMPDIR="${TMPDIR:-/tmp}" NODE_OPTIONS="${NODE_OPTIONS:-}" \
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
344
|
+
CODEX_HOME="${CODEX_HOME:-}" AR_RESUME_FLAGS="${AR_RESUME_FLAGS}" AR_FRESH_FLAGS="${AR_FRESH_FLAGS}" \
|
|
345
|
+
DISABLE_AUTOUPDATER=1 CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF=1 \
|
|
346
|
+
${_claude_cfg[@]+"${_claude_cfg[@]}"} ${_tool_env[@]+"${_tool_env[@]}"} \
|
|
347
|
+
/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_FRESH_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:-}"; FF="${AR_FRESH_FLAGS:-}"; unset AR_RESUME_FLAGS AR_FRESH_FLAGS; CB="$1"; shift; S=$(date +%s); "$CB" $RF "$@" || [ $(( $(date +%s) - S )) -ge 30 ] || exec "$CB" $FF "$@"' sh "${AGENT_BIN}" "${_launch_flags[@]}" || {
|
|
257
348
|
log "tmux could not start agent session '${TMUX_SESSION}'"
|
|
258
349
|
return 1
|
|
259
350
|
}
|
|
@@ -271,20 +362,30 @@ cleanup() {
|
|
|
271
362
|
# #152: remove the staged 0600 secret files on exit.
|
|
272
363
|
[ -n "${_SECRETS_DIR:-}" ] && rm -rf "${_SECRETS_DIR}" 2>/dev/null || true
|
|
273
364
|
}
|
|
274
|
-
trap cleanup EXIT
|
|
365
|
+
trap cleanup EXIT
|
|
366
|
+
# A TERM handler that returns lets bash carry on looping: launchd/systemd stop (and `agentradar leave`) would
|
|
367
|
+
# wait out their kill timeout. Exit, and the EXIT trap cleans up.
|
|
368
|
+
trap 'exit 143' INT TERM
|
|
275
369
|
|
|
276
370
|
until ensure_agent; do
|
|
277
371
|
log "agent start failed — retrying in ${HEALTH_POLL_SEC}s"
|
|
278
372
|
sleep "${HEALTH_POLL_SEC}"
|
|
279
373
|
done
|
|
280
374
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
#
|
|
284
|
-
AWP_MCP_URL
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
375
|
+
if [ "${AR_FEED}" = "feeder" ]; then
|
|
376
|
+
log "starting feeder (${FEEDER_JS}) → session '${TMUX_SESSION}'"
|
|
377
|
+
# #152: pass AWP_BEARER_FILE (a 0600 PATH), NEVER the raw AWP_BEARER — the feeder reads the file, so the
|
|
378
|
+
# bearer is not in the feeder process's environment (`ps eww`) or argv. AWP_MCP_URL is not a secret.
|
|
379
|
+
# AR_TOOL_ENV_FILE: the login probe must see the same gateway env the pane runs with.
|
|
380
|
+
AWP_MCP_URL="${AWP_MCP_URL}" AWP_BEARER_FILE="${AWP_BEARER_FILE}" TMUX_SESSION="${TMUX_SESSION}" \
|
|
381
|
+
AGENT_PID_PATTERN="${AGENT_PID_PATTERN}" AR_AGENT_HARNESS="${AR_AGENT_HARNESS}" AGENT_BIN="${AGENT_BIN}" \
|
|
382
|
+
AR_TOOL_ENV_FILE="${_TOOL_ENV_FILE}" \
|
|
383
|
+
node "${FEEDER_JS}" &
|
|
384
|
+
FEEDER_PID=$!
|
|
385
|
+
log "feeder pid ${FEEDER_PID}"
|
|
386
|
+
else
|
|
387
|
+
log "no feeder (AR_FEED=${AR_FEED}): the session receives its own work"
|
|
388
|
+
fi
|
|
288
389
|
|
|
289
390
|
# #133 M4 — start the optional feeder bridge (best-effort, isolated). Same 0600 bearer FILE + session env
|
|
290
391
|
# as the feeder; NEVER the raw bearer. Its failure NEVER affects the feeder/agent (nice-to-have terminal).
|
|
@@ -311,12 +412,12 @@ start_bridge
|
|
|
311
412
|
# next_event heartbeat cannot paint the runner Live during recovery. Restart the bridge after a new pane is
|
|
312
413
|
# created because tmux pipe-pane was bound to the dead pane.
|
|
313
414
|
while true; do
|
|
314
|
-
if ! kill -0 "${FEEDER_PID}" 2>/dev/null; then
|
|
415
|
+
if [ -n "${FEEDER_PID}" ] && ! kill -0 "${FEEDER_PID}" 2>/dev/null; then
|
|
315
416
|
log "feeder exited — exiting so the service manager restarts the whole unit"
|
|
316
417
|
exit 1
|
|
317
418
|
fi
|
|
318
419
|
if ! pane_alive; then
|
|
319
|
-
log "agent
|
|
420
|
+
log "agent pane died — recovering it in place"
|
|
320
421
|
if ensure_agent; then
|
|
321
422
|
if [ -n "${BRIDGE_PID}" ]; then
|
|
322
423
|
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
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Secure AgentRadar vendor setup and skill installer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,9 @@
|
|
|
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",
|
|
14
16
|
"dist/sync.js",
|
|
15
17
|
"dist/vendor-runner/**",
|
|
16
18
|
"dist/agentradar-vendor/SKILL.md",
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
"access": "public"
|
|
22
24
|
},
|
|
23
25
|
"scripts": {
|
|
24
|
-
"build": "tsc && mkdir -p dist/agentradar-vendor dist/vendor-runner/supervisor && cp ../../skills/agentradar-vendor/SKILL.md dist/agentradar-vendor/SKILL.md && cp ../../templates/vendor-runner/feeder.mjs ../../templates/vendor-runner/pane-live.sh ../../templates/vendor-runner/session-frame.mjs ../../templates/vendor-runner/terminal-bridge.mjs dist/vendor-runner/ && cp ../../templates/vendor-runner/supervisor/supervise-agent.sh dist/vendor-runner/supervisor/ && chmod +x dist/index.js dist/vendor-runner/pane-live.sh dist/vendor-runner/supervisor/supervise-agent.sh",
|
|
26
|
+
"build": "tsc && mkdir -p dist/agentradar-vendor dist/vendor-runner/supervisor && cp ../../skills/agentradar-vendor/SKILL.md dist/agentradar-vendor/SKILL.md && cp ../../templates/vendor-runner/feeder.mjs ../../templates/vendor-runner/pane-live.sh ../../templates/vendor-runner/session-frame.mjs ../../templates/vendor-runner/terminal-bridge.mjs dist/vendor-runner/ && cp ../../templates/vendor-runner/supervisor/supervise-agent.sh ../../templates/vendor-runner/supervisor/install-agent.sh ../../templates/vendor-runner/supervisor/com.agentradar.agent.plist.template ../../templates/vendor-runner/supervisor/agentradar-agent@.service.template dist/vendor-runner/supervisor/ && chmod +x dist/index.js dist/vendor-runner/pane-live.sh dist/vendor-runner/supervisor/supervise-agent.sh dist/vendor-runner/supervisor/install-agent.sh",
|
|
25
27
|
"clean": "rm -rf dist",
|
|
26
28
|
"prepack": "pnpm run build",
|
|
27
29
|
"typecheck": "tsc --noEmit",
|