agent-relay-server 0.99.1 → 0.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +1 -1
- package/package.json +5 -2
- package/public/assets/{MessageBubble-DiorZXyB.js → MessageBubble-BEjK1-bi.js} +3 -3
- package/public/assets/{MessageBubble-DiorZXyB.js.map → MessageBubble-BEjK1-bi.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-DY1p8v4p.js → PlanGraphPanel-AqFEg7Yg.js} +2 -2
- package/public/assets/{PlanGraphPanel-DY1p8v4p.js.map → PlanGraphPanel-AqFEg7Yg.js.map} +1 -1
- package/public/assets/{activity-rIxqUtFC.js → activity-D5EZw4bj.js} +2 -2
- package/public/assets/{activity-rIxqUtFC.js.map → activity-D5EZw4bj.js.map} +1 -1
- package/public/assets/agent-profiles-CSISRm_L.js +2 -0
- package/public/assets/agent-profiles-CSISRm_L.js.map +1 -0
- package/public/assets/agents-szyYnkgD.js +2 -0
- package/public/assets/agents-szyYnkgD.js.map +1 -0
- package/public/assets/{analytics-CUJbpoA7.js → analytics-KzLQfGMO.js} +2 -2
- package/public/assets/{analytics-CUJbpoA7.js.map → analytics-KzLQfGMO.js.map} +1 -1
- package/public/assets/{automation-B0cyCT5l.js → automation-DVSq4A1b.js} +2 -2
- package/public/assets/{automation-B0cyCT5l.js.map → automation-DVSq4A1b.js.map} +1 -1
- package/public/assets/branch-state-badge-DgwkJ2rN.js +2 -0
- package/public/assets/branch-state-badge-DgwkJ2rN.js.map +1 -0
- package/public/assets/{channels-Tg8l4n5J.js → channels-Dh_r7SnM.js} +2 -2
- package/public/assets/{channels-Tg8l4n5J.js.map → channels-Dh_r7SnM.js.map} +1 -1
- package/public/assets/chat-patMKjsi.js +2 -0
- package/public/assets/chat-patMKjsi.js.map +1 -0
- package/public/assets/{connectors-B5wbjyUd.js → connectors-DVKBHNRV.js} +2 -2
- package/public/assets/{connectors-B5wbjyUd.js.map → connectors-DVKBHNRV.js.map} +1 -1
- package/public/assets/{formatted-body-impl-ZwgTH3jh.js → formatted-body-impl-Csr2DuEk.js} +2 -2
- package/public/assets/{formatted-body-impl-ZwgTH3jh.js.map → formatted-body-impl-Csr2DuEk.js.map} +1 -1
- package/public/assets/index-D3woTUwp.js +23 -0
- package/public/assets/index-D3woTUwp.js.map +1 -0
- package/public/assets/{insights-C3mlXZdd.js → insights-D2_Qry06.js} +2 -2
- package/public/assets/{insights-C3mlXZdd.js.map → insights-D2_Qry06.js.map} +1 -1
- package/public/assets/{integrations-sYKtupOq.js → integrations-AW8b4h7T.js} +2 -2
- package/public/assets/{integrations-sYKtupOq.js.map → integrations-AW8b4h7T.js.map} +1 -1
- package/public/assets/maintenance-Dm56u8F9.js +2 -0
- package/public/assets/{maintenance-DL1lnqmG.js.map → maintenance-Dm56u8F9.js.map} +1 -1
- package/public/assets/{managed-agents-7A6WH4Rn.js → managed-agents-DpX-cHsm.js} +2 -2
- package/public/assets/{managed-agents-7A6WH4Rn.js.map → managed-agents-DpX-cHsm.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-THat8xv0.js → markdown-preview-impl-Df6EWwbg.js} +2 -2
- package/public/assets/{markdown-preview-impl-THat8xv0.js.map → markdown-preview-impl-Df6EWwbg.js.map} +1 -1
- package/public/assets/{memory-C349UJe9.js → memory-b2AgvV7H.js} +2 -2
- package/public/assets/{memory-C349UJe9.js.map → memory-b2AgvV7H.js.map} +1 -1
- package/public/assets/{messages-DXoLFj2P.js → messages-C5yDD_oq.js} +2 -2
- package/public/assets/{messages-DXoLFj2P.js.map → messages-C5yDD_oq.js.map} +1 -1
- package/public/assets/{orchestrators-CTXu4jAn.js → orchestrators-D3LecmEw.js} +2 -2
- package/public/assets/{orchestrators-CTXu4jAn.js.map → orchestrators-D3LecmEw.js.map} +1 -1
- package/public/assets/{overview-dWiN6TyN.js → overview-CnGkrBNe.js} +2 -2
- package/public/assets/{overview-dWiN6TyN.js.map → overview-CnGkrBNe.js.map} +1 -1
- package/public/assets/{pairs-KIXektDv.js → pairs-DXUdZOqn.js} +2 -2
- package/public/assets/{pairs-KIXektDv.js.map → pairs-DXUdZOqn.js.map} +1 -1
- package/public/assets/{projects-BPI8z55X.js → projects-1h41atqJ.js} +2 -2
- package/public/assets/{projects-BPI8z55X.js.map → projects-1h41atqJ.js.map} +1 -1
- package/public/assets/{registry-C-Y41yGB.js → registry-BeGP37ot.js} +2 -2
- package/public/assets/{registry-C-Y41yGB.js.map → registry-BeGP37ot.js.map} +1 -1
- package/public/assets/{security-BUymkwuj.js → security-BMh_y43L.js} +2 -2
- package/public/assets/{security-BUymkwuj.js.map → security-BMh_y43L.js.map} +1 -1
- package/public/assets/{select-DV25WRP5.js → select-Cvi5OHz0.js} +2 -2
- package/public/assets/{select-DV25WRP5.js.map → select-Cvi5OHz0.js.map} +1 -1
- package/public/assets/settings-DKSEwIVG.js +6 -0
- package/public/assets/{settings-C1En58bx.js.map → settings-DKSEwIVG.js.map} +1 -1
- package/public/assets/store-Ch-QzxPq.js +9 -0
- package/public/assets/store-Ch-QzxPq.js.map +1 -0
- package/public/assets/{tasks-BkpnKA63.js → tasks-BybIAX-y.js} +2 -2
- package/public/assets/{tasks-BkpnKA63.js.map → tasks-BybIAX-y.js.map} +1 -1
- package/public/assets/{teams-D-UYp7yH.js → teams-CMteOToR.js} +2 -2
- package/public/assets/{teams-D-UYp7yH.js.map → teams-CMteOToR.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-Bif-tkvD.js → terminal-viewer-impl-DanOi7lh.js} +2 -2
- package/public/assets/{terminal-viewer-impl-Bif-tkvD.js.map → terminal-viewer-impl-DanOi7lh.js.map} +1 -1
- package/public/assets/{user-avatar--czLv_AJ.js → user-avatar-DG629icI.js} +2 -2
- package/public/assets/{user-avatar--czLv_AJ.js.map → user-avatar-DG629icI.js.map} +1 -1
- package/public/assets/{work-queue-DN4oTe7w.js → work-queue-DqW80DIy.js} +2 -2
- package/public/assets/{work-queue-DN4oTe7w.js.map → work-queue-DqW80DIy.js.map} +1 -1
- package/public/assets/{workspaces-BolccWZy.js → workspaces-CccVI6lA.js} +2 -2
- package/public/assets/{workspaces-BolccWZy.js.map → workspaces-CccVI6lA.js.map} +1 -1
- package/public/index.html +2 -2
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/hooks/kill-guard.sh +300 -0
- package/runner/plugins/claude/hooks/read-only-bash-guard.sh +38 -140
- package/runner/src/adapter.ts +2 -2
- package/src/agent-death-diagnosis.ts +12 -3
- package/src/bus.ts +6 -45
- package/src/config-store.ts +13 -9
- package/src/db/provider-quota-burn.ts +79 -6
- package/src/issue-tracker/adapter.ts +11 -1
- package/src/issue-tracker/github.ts +83 -2
- package/src/issue-tracker/index.ts +1 -0
- package/src/lifecycle-manager.ts +9 -22
- package/src/provider-state.ts +9 -18
- package/src/provider-timeline-event.ts +70 -0
- package/src/quota-advisory.ts +4 -7
- package/src/quota-band-transition.ts +101 -11
- package/src/routes/agent-sessions.ts +4 -2
- package/src/services/issue-lifecycle.ts +54 -8
- package/src/services/provider-state-audit.ts +3 -3
- package/src/services/rate-limit-resume.ts +6 -4
- package/src/services/register-agent.ts +2 -1
- package/src/settings/registry.ts +34 -0
- package/src/spawn-targets.ts +80 -10
- package/src/sse.ts +5 -4
- package/public/assets/agent-profiles-Cu-VkJm4.js +0 -2
- package/public/assets/agent-profiles-Cu-VkJm4.js.map +0 -1
- package/public/assets/agents-dRHOBDUe.js +0 -2
- package/public/assets/agents-dRHOBDUe.js.map +0 -1
- package/public/assets/branch-state-badge-CNko2_BL.js +0 -2
- package/public/assets/branch-state-badge-CNko2_BL.js.map +0 -1
- package/public/assets/chat-jit-e6S_.js +0 -2
- package/public/assets/chat-jit-e6S_.js.map +0 -1
- package/public/assets/index-GoC6YrKn.js +0 -23
- package/public/assets/index-GoC6YrKn.js.map +0 -1
- package/public/assets/maintenance-DL1lnqmG.js +0 -2
- package/public/assets/settings-C1En58bx.js +0 -6
- package/public/assets/store-pKbf_WYd.js +0 -9
- package/public/assets/store-pKbf_WYd.js.map +0 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared "fleet fratricide" kill-pattern guard (issue #753).
|
|
3
|
+
#
|
|
4
|
+
# Every managed agent's runner runs as
|
|
5
|
+
# bun run .../agent-relay-runner/src/index.ts <provider> ...
|
|
6
|
+
# so a broad process kill such as `pkill -f "src/index.ts"` (or `pkill bun`,
|
|
7
|
+
# `killall node`, ...) matches EVERY sibling agent host-wide and silently SIGTERMs
|
|
8
|
+
# the whole fleet. This is the structural guard so that can't happen again.
|
|
9
|
+
#
|
|
10
|
+
# It blocks broad process-kill commands (pkill / killall, and `kill` with a
|
|
11
|
+
# pattern that leaks a fleet token) whose target would match sibling runners,
|
|
12
|
+
# while still allowing the safe shapes: PID-scoped kills (kill 12345,
|
|
13
|
+
# kill "$SERVER_PID") and pkill -f on a token that is unique to the caller's own
|
|
14
|
+
# throwaway server (its exact test port, or "$PWD/public").
|
|
15
|
+
#
|
|
16
|
+
# DUAL MODE:
|
|
17
|
+
# - sourced: defines fratricide_deny_reason() / fratricide_raw_reason() plus the
|
|
18
|
+
# shell-parsing helpers (trim_shell_text / split_segments / shell_words). The
|
|
19
|
+
# Claude PreToolUse Bash guard sources this and is wired to it.
|
|
20
|
+
# - executed: `kill-guard.sh "<command text>"` prints the guidance + exits 0 when
|
|
21
|
+
# the command is a dangerous broad kill, and exits 1 (silent) when it is safe.
|
|
22
|
+
# This lets non-bash callers reuse the SAME matcher as a subprocess.
|
|
23
|
+
#
|
|
24
|
+
# TODO(#753 follow-up): wire the Codex runner/exec layer to invoke this guard.
|
|
25
|
+
# Codex managed agents have no Claude PreToolUse hook, so the same broad pkill can
|
|
26
|
+
# still fratricide the fleet from a Codex session until that wiring lands. The
|
|
27
|
+
# matcher is intentionally factored here so the Codex side can call it directly
|
|
28
|
+
# (e.g. `bash kill-guard.sh "$command"`) without duplicating the pattern list.
|
|
29
|
+
|
|
30
|
+
shopt -s extglob
|
|
31
|
+
|
|
32
|
+
# Guidance returned on a block — tells the caller how to clean up safely instead.
|
|
33
|
+
FRATRICIDE_PATTERN_GUIDANCE='Blocked a broad process-kill (#753 fleet-fratricide guard): this pattern would match sibling agents'\'' runners, because every managed agent runs "bun run .../agent-relay-runner/src/index.ts". Kill only a PID you own — capture it when you spawn the server (SERVER_PID=$!; kill "$SERVER_PID") — or pkill -f a token that is unique to your own process (your exact test port, or "$PWD/public"). Never pkill/killall on bun, node, src/index.ts, agent-relay, or agent-relay-runner.'
|
|
34
|
+
|
|
35
|
+
trim_shell_text() {
|
|
36
|
+
local value="$1"
|
|
37
|
+
value="${value##+([[:space:]])}"
|
|
38
|
+
value="${value%%+([[:space:]])}"
|
|
39
|
+
printf '%s' "$value"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
split_segments() {
|
|
43
|
+
local text="$1"
|
|
44
|
+
local current="" i len ch next quote="" escaped=0
|
|
45
|
+
SEGMENTS=()
|
|
46
|
+
len=${#text}
|
|
47
|
+
for ((i = 0; i < len; i++)); do
|
|
48
|
+
ch="${text:i:1}"
|
|
49
|
+
if (( escaped )); then
|
|
50
|
+
current+="$ch"
|
|
51
|
+
escaped=0
|
|
52
|
+
continue
|
|
53
|
+
fi
|
|
54
|
+
if [ -n "$quote" ]; then
|
|
55
|
+
current+="$ch"
|
|
56
|
+
if [ "$quote" = '"' ] && [ "$ch" = "\\" ]; then
|
|
57
|
+
escaped=1
|
|
58
|
+
continue
|
|
59
|
+
fi
|
|
60
|
+
if [ "$ch" = "$quote" ]; then
|
|
61
|
+
quote=""
|
|
62
|
+
fi
|
|
63
|
+
continue
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
case "$ch" in
|
|
67
|
+
"'"|'"')
|
|
68
|
+
quote="$ch"
|
|
69
|
+
current+="$ch"
|
|
70
|
+
;;
|
|
71
|
+
"\\")
|
|
72
|
+
escaped=1
|
|
73
|
+
current+="$ch"
|
|
74
|
+
;;
|
|
75
|
+
$'\n'|";")
|
|
76
|
+
SEGMENTS+=("$current")
|
|
77
|
+
current=""
|
|
78
|
+
;;
|
|
79
|
+
"&")
|
|
80
|
+
next="${text:i+1:1}"
|
|
81
|
+
if [ "$next" = "&" ]; then
|
|
82
|
+
((i += 1))
|
|
83
|
+
fi
|
|
84
|
+
SEGMENTS+=("$current")
|
|
85
|
+
current=""
|
|
86
|
+
;;
|
|
87
|
+
"|")
|
|
88
|
+
next="${text:i+1:1}"
|
|
89
|
+
if [ "$next" = "|" ]; then
|
|
90
|
+
((i += 1))
|
|
91
|
+
fi
|
|
92
|
+
SEGMENTS+=("$current")
|
|
93
|
+
current=""
|
|
94
|
+
;;
|
|
95
|
+
*)
|
|
96
|
+
current+="$ch"
|
|
97
|
+
;;
|
|
98
|
+
esac
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
if [ -n "$quote" ] || (( escaped )); then
|
|
102
|
+
return 1
|
|
103
|
+
fi
|
|
104
|
+
SEGMENTS+=("$current")
|
|
105
|
+
return 0
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
shell_words() {
|
|
109
|
+
local text="$1"
|
|
110
|
+
local word="" i len ch quote="" escaped=0 in_word=0
|
|
111
|
+
WORDS=()
|
|
112
|
+
len=${#text}
|
|
113
|
+
for ((i = 0; i < len; i++)); do
|
|
114
|
+
ch="${text:i:1}"
|
|
115
|
+
if (( escaped )); then
|
|
116
|
+
word+="$ch"
|
|
117
|
+
in_word=1
|
|
118
|
+
escaped=0
|
|
119
|
+
continue
|
|
120
|
+
fi
|
|
121
|
+
if [ -n "$quote" ]; then
|
|
122
|
+
if [ "$quote" = '"' ] && [ "$ch" = "\\" ]; then
|
|
123
|
+
escaped=1
|
|
124
|
+
continue
|
|
125
|
+
fi
|
|
126
|
+
if [ "$ch" = "$quote" ]; then
|
|
127
|
+
quote=""
|
|
128
|
+
else
|
|
129
|
+
word+="$ch"
|
|
130
|
+
fi
|
|
131
|
+
in_word=1
|
|
132
|
+
continue
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
case "$ch" in
|
|
136
|
+
[[:space:]])
|
|
137
|
+
if (( in_word )); then
|
|
138
|
+
WORDS+=("$word")
|
|
139
|
+
word=""
|
|
140
|
+
in_word=0
|
|
141
|
+
fi
|
|
142
|
+
;;
|
|
143
|
+
"'"|'"')
|
|
144
|
+
quote="$ch"
|
|
145
|
+
in_word=1
|
|
146
|
+
;;
|
|
147
|
+
"\\")
|
|
148
|
+
escaped=1
|
|
149
|
+
in_word=1
|
|
150
|
+
;;
|
|
151
|
+
*)
|
|
152
|
+
word+="$ch"
|
|
153
|
+
in_word=1
|
|
154
|
+
;;
|
|
155
|
+
esac
|
|
156
|
+
done
|
|
157
|
+
|
|
158
|
+
if [ -n "$quote" ] || (( escaped )); then
|
|
159
|
+
return 1
|
|
160
|
+
fi
|
|
161
|
+
if (( in_word )); then
|
|
162
|
+
WORDS+=("$word")
|
|
163
|
+
fi
|
|
164
|
+
return 0
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# True (0) when text mentions a process-kill verb (pkill / killall / kill).
|
|
168
|
+
_looks_like_kill() {
|
|
169
|
+
if [[ "${1,,}" =~ (^|[^a-z])(pkill|killall|kill)([^a-z]|$) ]]; then
|
|
170
|
+
return 0
|
|
171
|
+
fi
|
|
172
|
+
return 1
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# True (0) when a kill pattern/token would match sibling runners across the fleet.
|
|
176
|
+
# Matches the runner path tokens, and the bare interpreter process names bun/node
|
|
177
|
+
# even when wrapped in pgrep/regex metacharacters such as [b]un, ^node$, bun.* .
|
|
178
|
+
_pattern_hits_fleet() {
|
|
179
|
+
local lower="${1,,}"
|
|
180
|
+
case "$lower" in
|
|
181
|
+
*index.ts*|*agent-relay*)
|
|
182
|
+
return 0
|
|
183
|
+
;;
|
|
184
|
+
esac
|
|
185
|
+
local cleaned="$lower" mc
|
|
186
|
+
for mc in '[' ']' '(' ')' '{' '}' '^' '$' '*' '+' '?' '\' '|'; do
|
|
187
|
+
cleaned="${cleaned//"$mc"/}"
|
|
188
|
+
done
|
|
189
|
+
if [[ "$cleaned" =~ (^|[^a-z0-9])(bun|node)([^a-z0-9]|$) ]]; then
|
|
190
|
+
return 0
|
|
191
|
+
fi
|
|
192
|
+
return 1
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Inspect one shell segment. Prints guidance + returns 0 when it is a dangerous
|
|
196
|
+
# broad kill; returns 1 when the segment is safe or not a kill.
|
|
197
|
+
_fratricide_segment_reason() {
|
|
198
|
+
local raw="$1" segment base i=0 j arg
|
|
199
|
+
segment="$(trim_shell_text "$raw")"
|
|
200
|
+
if [ -z "$segment" ]; then
|
|
201
|
+
return 1
|
|
202
|
+
fi
|
|
203
|
+
if ! shell_words "$segment"; then
|
|
204
|
+
# Unparseable — only worry if it both looks like a kill and leaks a fleet token.
|
|
205
|
+
if _looks_like_kill "$segment" && _pattern_hits_fleet "$segment"; then
|
|
206
|
+
printf '%s' "$FRATRICIDE_PATTERN_GUIDANCE"
|
|
207
|
+
return 0
|
|
208
|
+
fi
|
|
209
|
+
return 1
|
|
210
|
+
fi
|
|
211
|
+
if (( ${#WORDS[@]} == 0 )); then
|
|
212
|
+
return 1
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Skip any leading VAR=value assignments (e.g. SERVER_PID=$!; kill ...).
|
|
216
|
+
while (( i < ${#WORDS[@]} )) && [[ "${WORDS[i]}" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; do
|
|
217
|
+
((i += 1))
|
|
218
|
+
done
|
|
219
|
+
if (( i >= ${#WORDS[@]} )); then
|
|
220
|
+
return 1
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
base="${WORDS[i]##*/}"
|
|
224
|
+
base="${base,,}"
|
|
225
|
+
case "$base" in
|
|
226
|
+
pkill|killall)
|
|
227
|
+
# Pattern-matching kills: block if ANY non-flag operand hits the fleet.
|
|
228
|
+
j=$((i + 1))
|
|
229
|
+
while (( j < ${#WORDS[@]} )); do
|
|
230
|
+
arg="${WORDS[j]}"
|
|
231
|
+
if [[ "$arg" == -* ]]; then
|
|
232
|
+
((j += 1))
|
|
233
|
+
continue
|
|
234
|
+
fi
|
|
235
|
+
if _pattern_hits_fleet "$arg"; then
|
|
236
|
+
printf '%s' "$FRATRICIDE_PATTERN_GUIDANCE"
|
|
237
|
+
return 0
|
|
238
|
+
fi
|
|
239
|
+
((j += 1))
|
|
240
|
+
done
|
|
241
|
+
return 1
|
|
242
|
+
;;
|
|
243
|
+
kill)
|
|
244
|
+
# PID-scoped kills (kill 123, kill "$PID", kill -9 %1) are safe. Block only
|
|
245
|
+
# when an operand leaks a fleet token — e.g. kill $(pgrep -f src/index.ts).
|
|
246
|
+
j=$((i + 1))
|
|
247
|
+
while (( j < ${#WORDS[@]} )); do
|
|
248
|
+
arg="${WORDS[j]}"
|
|
249
|
+
if _pattern_hits_fleet "$arg"; then
|
|
250
|
+
printf '%s' "$FRATRICIDE_PATTERN_GUIDANCE"
|
|
251
|
+
return 0
|
|
252
|
+
fi
|
|
253
|
+
((j += 1))
|
|
254
|
+
done
|
|
255
|
+
return 1
|
|
256
|
+
;;
|
|
257
|
+
*)
|
|
258
|
+
return 1
|
|
259
|
+
;;
|
|
260
|
+
esac
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# Structured matcher: split a full command into segments and check each one.
|
|
264
|
+
# Prints guidance + returns 0 on the first dangerous broad kill; returns 1 if safe.
|
|
265
|
+
fratricide_deny_reason() {
|
|
266
|
+
local text="$1" segment reason
|
|
267
|
+
if ! split_segments "$text"; then
|
|
268
|
+
fratricide_raw_reason "$text"
|
|
269
|
+
return $?
|
|
270
|
+
fi
|
|
271
|
+
local -a segments=("${SEGMENTS[@]}")
|
|
272
|
+
for segment in "${segments[@]}"; do
|
|
273
|
+
if reason="$(_fratricide_segment_reason "$segment")"; then
|
|
274
|
+
printf '%s' "$reason"
|
|
275
|
+
return 0
|
|
276
|
+
fi
|
|
277
|
+
done
|
|
278
|
+
return 1
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Coarse raw-text fallback for when structured parsing is unavailable (no jq, or
|
|
282
|
+
# unparseable shell). Prints guidance + returns 0 only when the text both looks
|
|
283
|
+
# like a kill and leaks a fleet token.
|
|
284
|
+
fratricide_raw_reason() {
|
|
285
|
+
local text="$1"
|
|
286
|
+
if _looks_like_kill "$text" && _pattern_hits_fleet "$text"; then
|
|
287
|
+
printf '%s' "$FRATRICIDE_PATTERN_GUIDANCE"
|
|
288
|
+
return 0
|
|
289
|
+
fi
|
|
290
|
+
return 1
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# Executed directly (not sourced): argv[1] is the command text to inspect.
|
|
294
|
+
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|
295
|
+
if reason="$(fratricide_deny_reason "${1:-}")"; then
|
|
296
|
+
printf '%s\n' "$reason"
|
|
297
|
+
exit 0
|
|
298
|
+
fi
|
|
299
|
+
exit 1
|
|
300
|
+
fi
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
# Real boundaries are Relay token scope, unavailable write tools, and future
|
|
5
5
|
# OS-level enforcement.
|
|
6
6
|
set -euo pipefail
|
|
7
|
-
|
|
7
|
+
HOOK_DIR="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}/hooks"
|
|
8
|
+
source "${HOOK_DIR}/relay-status.sh"
|
|
8
9
|
relay_install_hook_guard read-only-bash-guard
|
|
10
|
+
# Shared fleet-fratricide kill-pattern matcher (#753): trim_shell_text,
|
|
11
|
+
# split_segments, shell_words and fratricide_deny_reason live here.
|
|
12
|
+
source "${HOOK_DIR}/kill-guard.sh"
|
|
9
13
|
|
|
10
14
|
payload="$(cat)"
|
|
11
15
|
|
|
@@ -15,6 +19,38 @@ emit_deny() {
|
|
|
15
19
|
"$(relay_json_escape "$reason")"
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
shopt -s extglob
|
|
23
|
+
|
|
24
|
+
# Extract the Bash command once. jq is strongly preferred; both the fleet-kill
|
|
25
|
+
# guard (all modes) and the read-only allow-list (read-only mode) consume it.
|
|
26
|
+
jq_available=0
|
|
27
|
+
tool_name=""
|
|
28
|
+
command_text=""
|
|
29
|
+
if command -v jq >/dev/null 2>&1; then
|
|
30
|
+
jq_available=1
|
|
31
|
+
tool_name="$(printf '%s' "$payload" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
32
|
+
command_text="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# --- Fleet-fratricide guard: ACTIVE IN ALL APPROVAL MODES (#753) ---
|
|
36
|
+
# A broad pkill/killall/kill can SIGTERM every sibling agent's runner host-wide,
|
|
37
|
+
# so this block runs before (and independent of) the read-only allow-list.
|
|
38
|
+
if (( jq_available )); then
|
|
39
|
+
if [ "${tool_name,,}" = "bash" ] && [ -n "$command_text" ]; then
|
|
40
|
+
if fratricide_reason="$(fratricide_deny_reason "$command_text")"; then
|
|
41
|
+
emit_deny "$fratricide_reason"
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
else
|
|
46
|
+
# No jq: best-effort raw scan so the fleet guard still bites the obvious cases.
|
|
47
|
+
if fratricide_reason="$(fratricide_raw_reason "$payload")"; then
|
|
48
|
+
emit_deny "$fratricide_reason"
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# --- Read-only allow-list: only the read-only approval mode below this point ---
|
|
18
54
|
case "${AGENT_RELAY_APPROVAL:-}" in
|
|
19
55
|
open|guarded)
|
|
20
56
|
exit 0
|
|
@@ -27,40 +63,27 @@ case "${AGENT_RELAY_APPROVAL:-}" in
|
|
|
27
63
|
;;
|
|
28
64
|
esac
|
|
29
65
|
|
|
30
|
-
if !
|
|
66
|
+
if (( ! jq_available )); then
|
|
31
67
|
emit_deny "Read-only Bash guard could not inspect the command because jq is unavailable."
|
|
32
68
|
exit 0
|
|
33
69
|
fi
|
|
34
70
|
|
|
35
|
-
tool_name="$(printf '%s' "$payload" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
36
71
|
if [ "${tool_name,,}" != "bash" ]; then
|
|
37
72
|
exit 0
|
|
38
73
|
fi
|
|
39
74
|
|
|
40
|
-
command_text="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
41
75
|
if [ -z "$command_text" ]; then
|
|
42
76
|
emit_deny "Read-only Bash guard denied an empty Bash command."
|
|
43
77
|
exit 0
|
|
44
78
|
fi
|
|
45
79
|
|
|
46
|
-
shopt -s extglob
|
|
47
|
-
|
|
48
80
|
DENY_REASON=""
|
|
49
|
-
SEGMENTS=()
|
|
50
|
-
WORDS=()
|
|
51
81
|
|
|
52
82
|
deny_segment() {
|
|
53
83
|
DENY_REASON="$1"
|
|
54
84
|
return 1
|
|
55
85
|
}
|
|
56
86
|
|
|
57
|
-
trim_shell_text() {
|
|
58
|
-
local value="$1"
|
|
59
|
-
value="${value##+([[:space:]])}"
|
|
60
|
-
value="${value%%+([[:space:]])}"
|
|
61
|
-
printf '%s' "$value"
|
|
62
|
-
}
|
|
63
|
-
|
|
64
87
|
hard_deny_reason() {
|
|
65
88
|
local text="$1"
|
|
66
89
|
case "$text" in
|
|
@@ -117,131 +140,6 @@ hard_deny_reason() {
|
|
|
117
140
|
return 1
|
|
118
141
|
}
|
|
119
142
|
|
|
120
|
-
split_segments() {
|
|
121
|
-
local text="$1"
|
|
122
|
-
local current="" i len ch next quote="" escaped=0
|
|
123
|
-
SEGMENTS=()
|
|
124
|
-
len=${#text}
|
|
125
|
-
for ((i = 0; i < len; i++)); do
|
|
126
|
-
ch="${text:i:1}"
|
|
127
|
-
if (( escaped )); then
|
|
128
|
-
current+="$ch"
|
|
129
|
-
escaped=0
|
|
130
|
-
continue
|
|
131
|
-
fi
|
|
132
|
-
if [ -n "$quote" ]; then
|
|
133
|
-
current+="$ch"
|
|
134
|
-
if [ "$quote" = '"' ] && [ "$ch" = "\\" ]; then
|
|
135
|
-
escaped=1
|
|
136
|
-
continue
|
|
137
|
-
fi
|
|
138
|
-
if [ "$ch" = "$quote" ]; then
|
|
139
|
-
quote=""
|
|
140
|
-
fi
|
|
141
|
-
continue
|
|
142
|
-
fi
|
|
143
|
-
|
|
144
|
-
case "$ch" in
|
|
145
|
-
"'"|'"')
|
|
146
|
-
quote="$ch"
|
|
147
|
-
current+="$ch"
|
|
148
|
-
;;
|
|
149
|
-
"\\")
|
|
150
|
-
escaped=1
|
|
151
|
-
current+="$ch"
|
|
152
|
-
;;
|
|
153
|
-
$'\n'|";")
|
|
154
|
-
SEGMENTS+=("$current")
|
|
155
|
-
current=""
|
|
156
|
-
;;
|
|
157
|
-
"&")
|
|
158
|
-
next="${text:i+1:1}"
|
|
159
|
-
if [ "$next" = "&" ]; then
|
|
160
|
-
((i += 1))
|
|
161
|
-
fi
|
|
162
|
-
SEGMENTS+=("$current")
|
|
163
|
-
current=""
|
|
164
|
-
;;
|
|
165
|
-
"|")
|
|
166
|
-
next="${text:i+1:1}"
|
|
167
|
-
if [ "$next" = "|" ]; then
|
|
168
|
-
((i += 1))
|
|
169
|
-
fi
|
|
170
|
-
SEGMENTS+=("$current")
|
|
171
|
-
current=""
|
|
172
|
-
;;
|
|
173
|
-
*)
|
|
174
|
-
current+="$ch"
|
|
175
|
-
;;
|
|
176
|
-
esac
|
|
177
|
-
done
|
|
178
|
-
|
|
179
|
-
if [ -n "$quote" ] || (( escaped )); then
|
|
180
|
-
return 1
|
|
181
|
-
fi
|
|
182
|
-
SEGMENTS+=("$current")
|
|
183
|
-
return 0
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
shell_words() {
|
|
187
|
-
local text="$1"
|
|
188
|
-
local word="" i len ch quote="" escaped=0 in_word=0
|
|
189
|
-
WORDS=()
|
|
190
|
-
len=${#text}
|
|
191
|
-
for ((i = 0; i < len; i++)); do
|
|
192
|
-
ch="${text:i:1}"
|
|
193
|
-
if (( escaped )); then
|
|
194
|
-
word+="$ch"
|
|
195
|
-
in_word=1
|
|
196
|
-
escaped=0
|
|
197
|
-
continue
|
|
198
|
-
fi
|
|
199
|
-
if [ -n "$quote" ]; then
|
|
200
|
-
if [ "$quote" = '"' ] && [ "$ch" = "\\" ]; then
|
|
201
|
-
escaped=1
|
|
202
|
-
continue
|
|
203
|
-
fi
|
|
204
|
-
if [ "$ch" = "$quote" ]; then
|
|
205
|
-
quote=""
|
|
206
|
-
else
|
|
207
|
-
word+="$ch"
|
|
208
|
-
fi
|
|
209
|
-
in_word=1
|
|
210
|
-
continue
|
|
211
|
-
fi
|
|
212
|
-
|
|
213
|
-
case "$ch" in
|
|
214
|
-
[[:space:]])
|
|
215
|
-
if (( in_word )); then
|
|
216
|
-
WORDS+=("$word")
|
|
217
|
-
word=""
|
|
218
|
-
in_word=0
|
|
219
|
-
fi
|
|
220
|
-
;;
|
|
221
|
-
"'"|'"')
|
|
222
|
-
quote="$ch"
|
|
223
|
-
in_word=1
|
|
224
|
-
;;
|
|
225
|
-
"\\")
|
|
226
|
-
escaped=1
|
|
227
|
-
in_word=1
|
|
228
|
-
;;
|
|
229
|
-
*)
|
|
230
|
-
word+="$ch"
|
|
231
|
-
in_word=1
|
|
232
|
-
;;
|
|
233
|
-
esac
|
|
234
|
-
done
|
|
235
|
-
|
|
236
|
-
if [ -n "$quote" ] || (( escaped )); then
|
|
237
|
-
return 1
|
|
238
|
-
fi
|
|
239
|
-
if (( in_word )); then
|
|
240
|
-
WORDS+=("$word")
|
|
241
|
-
fi
|
|
242
|
-
return 0
|
|
243
|
-
}
|
|
244
|
-
|
|
245
143
|
validate_git_branch() {
|
|
246
144
|
local i="$1" arg
|
|
247
145
|
while (( i < ${#WORDS[@]} )); do
|
package/runner/src/adapter.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentProfile, LivenessSignal, Message } from "agent-relay-sdk";
|
|
1
|
+
import type { AgentProfile, LivenessSignal, Message, ProviderState } from "agent-relay-sdk";
|
|
2
2
|
import { isRecord } from "agent-relay-sdk";
|
|
3
3
|
import type { LivenessInputs } from "./liveness";
|
|
4
4
|
import type { SessionEvent } from "./session-insights";
|
|
@@ -30,7 +30,7 @@ export interface ProviderStatusEvent {
|
|
|
30
30
|
// runner reconciles them against actual liveness and clears the busy marker if
|
|
31
31
|
// every pid is dead — recovering from a tracked process killed out-of-band.
|
|
32
32
|
pids?: number[];
|
|
33
|
-
providerState?:
|
|
33
|
+
providerState?: ProviderState;
|
|
34
34
|
metadata?: Record<string, unknown>;
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
// deterministic and unit-testable per failure mode. The output rides the #633 terminal event and
|
|
7
7
|
// is persisted as a CrashReport so the WHY survives the reaper.
|
|
8
8
|
|
|
9
|
-
import { extractClaudeModelUnavailableMessage } from "agent-relay-sdk";
|
|
10
9
|
import type { AgentCard, AgentDeathCause, AgentDeathDiagnosis, CrashReport, ManagedSessionExitDiagnostics } from "./types";
|
|
11
10
|
|
|
12
11
|
export interface DeathEvidence {
|
|
@@ -87,6 +86,16 @@ function haystack(ev: DeathEvidence): string {
|
|
|
87
86
|
return parts.join("\n");
|
|
88
87
|
}
|
|
89
88
|
|
|
89
|
+
function terminalModelUnavailableMessage(meta: Record<string, unknown> | undefined): string | undefined {
|
|
90
|
+
const terminalReason = str(meta?.terminalFailureReason);
|
|
91
|
+
const terminalMessage = str(meta?.terminalFailureMessage);
|
|
92
|
+
if (terminalReason === "model-unavailable" && terminalMessage) return terminalMessage;
|
|
93
|
+
const providerState = record(meta, "providerState");
|
|
94
|
+
const stateReason = str(providerState?.reason);
|
|
95
|
+
const stateMessage = str(providerState?.message);
|
|
96
|
+
return stateReason === "model-unavailable" && stateMessage ? stateMessage : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
90
99
|
function logTailOf(ev: DeathEvidence): string[] | undefined {
|
|
91
100
|
if (ev.exit?.logTail?.length) return ev.exit.logTail;
|
|
92
101
|
const lastExit = record(ev.agentMeta, "lastExit");
|
|
@@ -159,8 +168,8 @@ export function classifyAgentDeath(ev: DeathEvidence): AgentDeathDiagnosis {
|
|
|
159
168
|
return verdict("onboarding-gate", fast ? "high" : "medium", "Stuck at a first-run onboarding/trust gate");
|
|
160
169
|
}
|
|
161
170
|
|
|
162
|
-
// 4) provider-crash — CLI panic/segfault/model-unavailable, or a crash signal.
|
|
163
|
-
const modelUnavailable =
|
|
171
|
+
// 4) provider-crash — CLI panic/segfault/adapter-classified model-unavailable, or a crash signal.
|
|
172
|
+
const modelUnavailable = terminalModelUnavailableMessage(ev.agentMeta);
|
|
164
173
|
if (modelUnavailable) {
|
|
165
174
|
pushUnique(evidence, modelUnavailable);
|
|
166
175
|
return verdict("provider-crash", "high", "Provider unavailable (model/CLI error)");
|
package/src/bus.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Server, ServerWebSocket } from "bun";
|
|
2
|
-
import { ValidationError,
|
|
2
|
+
import { ValidationError, getAgent, getDb, heartbeat, markReady, mergeAgentMeta, orphanTasksForAgent, revokeRuntimeTokensForAgent, setStatus, validateAgentSession } from "./db";
|
|
3
3
|
import { getOldestOutboxCursor, getOutboxCursor, replayEvents, type BusEvent } from "./bus-outbox";
|
|
4
4
|
import { projectAgentStatusData } from "./agent-card-projection";
|
|
5
5
|
import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
|
|
@@ -11,6 +11,7 @@ import { registerAgent } from "./services/register-agent";
|
|
|
11
11
|
import { authContextFromBus } from "./services/auth-context";
|
|
12
12
|
import { commandAuthorizationResource, dispatchCommand } from "./services/dispatch-command";
|
|
13
13
|
import { auditProviderStateTransition } from "./services/provider-state-audit";
|
|
14
|
+
import { agentProviderTimelineEvent, auditRunnerTimelineEvent } from "./provider-timeline-event";
|
|
14
15
|
import { ServiceAuthError } from "./services/errors";
|
|
15
16
|
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent, shutdownInputFromBusFrame } from "./services/shutdown-agent";
|
|
16
17
|
import { flushQueuedDirectMessages } from "./services/managed-running";
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
type BusFrame,
|
|
29
30
|
type RegisterFrame,
|
|
30
31
|
} from "agent-relay-sdk/protocol";
|
|
31
|
-
import { errMessage, isRecord
|
|
32
|
+
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
32
33
|
import { getComponentAuth, isComponentAuthorizedFor, isAuthorized, isOriginAllowed, unauthorized } from "./security";
|
|
33
34
|
import type { Command, ComponentToken, ContextState, Message, ProviderCapabilities, QuotaState, RegisterAgentInput, Task } from "./types";
|
|
34
35
|
|
|
@@ -189,10 +190,11 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
|
|
|
189
190
|
}
|
|
190
191
|
const after = getAgent(conn.agentId);
|
|
191
192
|
auditProviderStateTransition(conn.agentId, before, after);
|
|
192
|
-
|
|
193
|
+
const timelineEvent = agentProviderTimelineEvent(after);
|
|
194
|
+
auditRunnerTimelineEvent(conn.agentId, timelineEvent);
|
|
193
195
|
// A real PreCompact/SessionStart hook arrives as a timelineEvent in the
|
|
194
196
|
// merged meta — clears any pending stall watch (stale events ignored).
|
|
195
|
-
noteAgentTimelineEvent(conn.agentId,
|
|
197
|
+
noteAgentTimelineEvent(conn.agentId, timelineEvent);
|
|
196
198
|
emitAgentStatusEvent(conn.agentId);
|
|
197
199
|
// #237 stop-hook → instant ⚪↔🟡 changes detection on the turn-end edge.
|
|
198
200
|
probeWorkspaceOnTurnEnd(before, after);
|
|
@@ -568,47 +570,6 @@ function emitAgentStatusEvent(agentId: string): void {
|
|
|
568
570
|
});
|
|
569
571
|
}
|
|
570
572
|
|
|
571
|
-
function auditRunnerTimelineEvent(agentId: string, timelineEvent: unknown): void {
|
|
572
|
-
if (!isRecord(timelineEvent)) return;
|
|
573
|
-
const metadata = isRecord(timelineEvent.metadata) ? timelineEvent.metadata : {};
|
|
574
|
-
if (metadata.source !== "runner") return;
|
|
575
|
-
const status = stringValue(timelineEvent.status);
|
|
576
|
-
if (!status) return;
|
|
577
|
-
const eventType = stringValue(metadata.eventType) ?? status;
|
|
578
|
-
const timestamp = numberValue(timelineEvent.timestamp) ?? Date.now();
|
|
579
|
-
const id = stringValue(timelineEvent.id) ?? `${eventType}-${timestamp}`;
|
|
580
|
-
const title = stringValue(timelineEvent.title) ?? status.replace(/[._-]+/g, " ");
|
|
581
|
-
const body = stringValue(timelineEvent.body);
|
|
582
|
-
const icon = stringValue(timelineEvent.icon) ?? "ti-activity";
|
|
583
|
-
try {
|
|
584
|
-
const event = createActivityEvent({
|
|
585
|
-
clientId: `runner-timeline-${agentId}-${id}`,
|
|
586
|
-
kind: "state",
|
|
587
|
-
title,
|
|
588
|
-
body,
|
|
589
|
-
meta: agentId,
|
|
590
|
-
icon,
|
|
591
|
-
view: "agents",
|
|
592
|
-
agentId,
|
|
593
|
-
metadata: {
|
|
594
|
-
...metadata,
|
|
595
|
-
eventType,
|
|
596
|
-
timelineStatus: status,
|
|
597
|
-
timelineId: id,
|
|
598
|
-
timelineTimestamp: timestamp,
|
|
599
|
-
},
|
|
600
|
-
});
|
|
601
|
-
emitRelayEvent({
|
|
602
|
-
type: "activity.created",
|
|
603
|
-
source: "server",
|
|
604
|
-
subject: String(event.id),
|
|
605
|
-
data: event as unknown as Record<string, unknown>,
|
|
606
|
-
});
|
|
607
|
-
} catch {
|
|
608
|
-
// Timeline writes must never block bus status updates.
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
573
|
function sendCommandResult(
|
|
613
574
|
ws: BusWebSocket,
|
|
614
575
|
commandId: string,
|