@mmerterden/multi-agent-pipeline 13.3.0 → 13.5.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/CHANGELOG.md +131 -0
- package/install/_mcp-register.mjs +125 -0
- package/install/codex.mjs +4 -38
- package/install/copilot.mjs +8 -0
- package/install/index.mjs +8 -3
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/setup/SKILL.md +2 -1
- package/pipeline/lib/account-resolver.sh +15 -4
- package/pipeline/lib/credential-inventory.sh +374 -0
- package/pipeline/lib/credential-store-resolver.sh +34 -5
- package/pipeline/lib/fetch-confluence.sh +27 -5
- package/pipeline/lib/fetch-crashlytics.sh +27 -5
- package/pipeline/lib/fetch-figma-annotations.sh +18 -4
- package/pipeline/lib/fetch-fortify.sh +27 -5
- package/pipeline/lib/fetch-graylog.sh +27 -5
- package/pipeline/lib/figma-mcp-refresh.sh +35 -11
- package/pipeline/lib/figma-screenshot.sh +18 -7
- package/pipeline/lib/issue-fetcher.sh +19 -4
- package/pipeline/lib/post-pr-review.sh +27 -4
- package/pipeline/lib/repo-cache.sh +19 -4
- package/pipeline/lib/vercel-deploy.sh +15 -4
- package/pipeline/multi-agent-refs/features/external-context-injection.md +44 -3
- package/pipeline/multi-agent-refs/keychain.md +63 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +51 -18
- package/pipeline/multi-agent-refs/rules.md +3 -3
- package/pipeline/scripts/audit-log.sh +10 -4
- package/pipeline/scripts/keychain-save.sh +27 -5
- package/pipeline/scripts/phase-tracker.sh +26 -2
- package/pipeline/scripts/phase0-exit-gate.mjs +52 -0
- package/pipeline/scripts/uninstall.mjs +24 -5
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +19 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# credential-inventory.sh - what the pipeline can reach right now, and what it cannot.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS
|
|
5
|
+
#
|
|
6
|
+
# A run asked the user to paste a Crashlytics stack trace by hand while a Firebase
|
|
7
|
+
# service-account JSON sat in the Keychain, mapped as `firebase`, fully valid. Nothing
|
|
8
|
+
# was broken - the pipeline simply never asked itself "do I already hold a credential
|
|
9
|
+
# that answers this?" before asking the user. From the user's side that is worse than a
|
|
10
|
+
# failure: they had supplied the key precisely so this would not happen.
|
|
11
|
+
#
|
|
12
|
+
# One command, so there is no excuse for asking blind. Run it before any question that
|
|
13
|
+
# requests data an external system holds, and let the answer shape the question:
|
|
14
|
+
#
|
|
15
|
+
# - credential present -> ask for the *pointer* (the issue URL), not the payload,
|
|
16
|
+
# and say you will fetch it
|
|
17
|
+
# - credential missing -> say which logical key is unmapped and offer the Save Flow
|
|
18
|
+
# - credential dead -> say it is expired and offer to refresh it
|
|
19
|
+
#
|
|
20
|
+
# SAFETY
|
|
21
|
+
#
|
|
22
|
+
# Values are never printed, never stored in a variable that reaches stdout, and never
|
|
23
|
+
# logged. Presence is probed through `credential-store.sh get` with stdout discarded, so
|
|
24
|
+
# the secret goes to /dev/null and only the exit status is read.
|
|
25
|
+
#
|
|
26
|
+
# Usage:
|
|
27
|
+
# credential-inventory.sh # human-readable table
|
|
28
|
+
# credential-inventory.sh --json # machine-readable, for state files
|
|
29
|
+
# credential-inventory.sh --probe # also verify each configured service answers
|
|
30
|
+
# credential-inventory.sh --key firebase # single logical key, exit 0 present / 1 not
|
|
31
|
+
#
|
|
32
|
+
# PRESENT IS NOT THE SAME AS WORKING
|
|
33
|
+
#
|
|
34
|
+
# Without --probe this reports what is configured. With --probe each configured
|
|
35
|
+
# credential makes one cheap authenticated request, and the verdict distinguishes the
|
|
36
|
+
# three failures that need three different user actions:
|
|
37
|
+
#
|
|
38
|
+
# reachable the service answered and accepted the credential
|
|
39
|
+
# auth-rejected 401/403 - the credential is dead, the user must refresh it
|
|
40
|
+
# unreachable no response at all - on a corporate host, almost always the VPN
|
|
41
|
+
# no-host-configured a token exists but its host was never recorded in preferences
|
|
42
|
+
# well-formed structurally valid, liveness not checkable without more input
|
|
43
|
+
# not-probeable no cheap probe exists for this credential type
|
|
44
|
+
#
|
|
45
|
+
# Only configured credentials are probed. An unmapped key is a capability the user chose
|
|
46
|
+
# not to enable, not a problem to report.
|
|
47
|
+
#
|
|
48
|
+
# Exit codes: 0 = inventory produced (or the queried key is present), 1 = queried key
|
|
49
|
+
# absent, 3 = usage error.
|
|
50
|
+
|
|
51
|
+
set -uo pipefail
|
|
52
|
+
|
|
53
|
+
PREFS="${MULTI_AGENT_PREFS:-$HOME/.claude/multi-agent-preferences.json}"
|
|
54
|
+
|
|
55
|
+
STORE=""
|
|
56
|
+
for c in "$HOME/.claude/lib/credential-store.sh" "$HOME/.copilot/lib/credential-store.sh" \
|
|
57
|
+
"$HOME/.codex/lib/credential-store.sh" "$(dirname "$0")/credential-store.sh"; do
|
|
58
|
+
[ -f "$c" ] && { STORE="$c"; break; }
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
MODE="table"
|
|
62
|
+
QUERY=""
|
|
63
|
+
PROBE=0
|
|
64
|
+
TIMEOUT="${MULTI_AGENT_PROBE_TIMEOUT:-6}"
|
|
65
|
+
while [ $# -gt 0 ]; do
|
|
66
|
+
case "$1" in
|
|
67
|
+
--json) MODE="json"; shift ;;
|
|
68
|
+
--probe) PROBE=1; shift ;;
|
|
69
|
+
--key) QUERY="${2:-}"; shift 2 || shift ;;
|
|
70
|
+
-h|--help)
|
|
71
|
+
echo "usage: $0 [--json] [--probe] [--key <logical-key>]" >&2; exit 3 ;;
|
|
72
|
+
*) echo "ERR: unexpected arg $1" >&2; exit 3 ;;
|
|
73
|
+
esac
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
pref() { # pref <dotted.path> -> value or empty
|
|
77
|
+
[ -f "$PREFS" ] || return 0
|
|
78
|
+
python3 - "$PREFS" "$1" <<'PYPREF' 2>/dev/null
|
|
79
|
+
import json, sys
|
|
80
|
+
try:
|
|
81
|
+
d = json.load(open(sys.argv[1]))
|
|
82
|
+
except Exception:
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
for k in sys.argv[2].split("."):
|
|
85
|
+
if not isinstance(d, dict):
|
|
86
|
+
sys.exit(0)
|
|
87
|
+
d = d.get(k)
|
|
88
|
+
if d is None:
|
|
89
|
+
sys.exit(0)
|
|
90
|
+
print(d if not isinstance(d, (dict, list)) else "")
|
|
91
|
+
PYPREF
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# An authenticated request whose credential never appears in argv.
|
|
95
|
+
#
|
|
96
|
+
# `curl -H "Authorization: Bearer $TOK"` puts the secret in the process command line,
|
|
97
|
+
# where any `ps` on the machine can read it. `--config -` takes both the header and the
|
|
98
|
+
# URL from stdin instead, so the token exists only in the pipe. Same reason the Vercel
|
|
99
|
+
# wrapper refuses a `--token=` argv.
|
|
100
|
+
#
|
|
101
|
+
# probe_http <url> <header-name> <header-value> -> prints the HTTP status
|
|
102
|
+
probe_http() {
|
|
103
|
+
local url="$1" hname="$2" hval="$3" out
|
|
104
|
+
# No `|| echo 000` here: curl already writes `000` through write-out when it never
|
|
105
|
+
# got a response, and exits non-zero as well. Appending produced `000000`, which fell
|
|
106
|
+
# through classify_status into a bogus `unexpected-000000` verdict for what was simply
|
|
107
|
+
# a closed VPN.
|
|
108
|
+
out=$(printf 'url = "%s"\nheader = "%s: %s"\nsilent\noutput = "/dev/null"\nwrite-out = "%%{http_code}"\nmax-time = %s\n' \
|
|
109
|
+
"$url" "$hname" "$hval" "$TIMEOUT" | curl --config - 2>/dev/null)
|
|
110
|
+
printf '%s' "${out:-000}"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# probe_basic <url> <user> <pass> -> prints the HTTP status
|
|
114
|
+
# probe_basic_hdr <url> <user> <pass> <header-name> <header-value> -> HTTP status
|
|
115
|
+
probe_basic_hdr() {
|
|
116
|
+
local url="$1" u="$2" pw="$3" hname="$4" hval="$5" out
|
|
117
|
+
out=$(printf 'url = "%s"\nuser = "%s:%s"\nheader = "%s: %s"\nsilent\noutput = "/dev/null"\nwrite-out = "%%{http_code}"\nmax-time = %s\n' \
|
|
118
|
+
"$url" "$u" "$pw" "$hname" "$hval" "$TIMEOUT" | curl --config - 2>/dev/null)
|
|
119
|
+
printf '%s' "${out:-000}"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
probe_basic() {
|
|
123
|
+
local url="$1" u="$2" p="$3" out
|
|
124
|
+
out=$(printf 'url = "%s"\nuser = "%s:%s"\nsilent\noutput = "/dev/null"\nwrite-out = "%%{http_code}"\nmax-time = %s\n' \
|
|
125
|
+
"$url" "$u" "$p" "$TIMEOUT" | curl --config - 2>/dev/null)
|
|
126
|
+
printf '%s' "${out:-000}"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Map an HTTP status onto the vocabulary the pipeline reasons about. `000` is curl's
|
|
130
|
+
# "never got a response" - DNS failure, refused connection, timeout - which on a
|
|
131
|
+
# corporate host is almost always a closed VPN, and is a completely different user
|
|
132
|
+
# action from a rejected credential.
|
|
133
|
+
classify_status() {
|
|
134
|
+
case "$1" in
|
|
135
|
+
2??) echo "reachable" ;;
|
|
136
|
+
# A redirect means the host answered. Corporate services commonly bounce an
|
|
137
|
+
# unauthenticated API call to an SSO login page, which classify as auth-rejected
|
|
138
|
+
# rather than reachable: the request did not get its data.
|
|
139
|
+
30?) echo "auth-rejected" ;;
|
|
140
|
+
401|403) echo "auth-rejected" ;;
|
|
141
|
+
404) echo "reachable" ;; # endpoint answered; the probe path may just not exist
|
|
142
|
+
000|"") echo "unreachable" ;;
|
|
143
|
+
5??) echo "server-error" ;;
|
|
144
|
+
# Anything that is not a three-digit status means the probe itself misbehaved.
|
|
145
|
+
# Saying so beats inventing a service verdict from a malformed value.
|
|
146
|
+
*) echo "probe-error" ;;
|
|
147
|
+
esac
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Reachability for one logical key. Never prints a credential; the value is piped
|
|
151
|
+
# straight into curl's stdin config and discarded.
|
|
152
|
+
#
|
|
153
|
+
# Hosts always come from preferences - a self-hosted Jira / Bitbucket / Confluence /
|
|
154
|
+
# Fortify / Graylog address is deployment-specific and must never be baked in. Only
|
|
155
|
+
# genuinely global public APIs are named literally. Host keys live at
|
|
156
|
+
# `global.hosts.<service>` per prefs.schema.json - an earlier version of this probe
|
|
157
|
+
# guessed `global.<service>Host` and reported no-host-configured for every
|
|
158
|
+
# self-hosted service the user had actually configured.
|
|
159
|
+
probe_one() {
|
|
160
|
+
local key="$1" tok host status
|
|
161
|
+
tok=$(bash "$STORE" get "$key" 2>/dev/null) || { echo "no-credential"; return; }
|
|
162
|
+
[ -n "$tok" ] || { echo "no-credential"; return; }
|
|
163
|
+
|
|
164
|
+
case "$key" in
|
|
165
|
+
jira)
|
|
166
|
+
host=$(pref global.hosts.jira)
|
|
167
|
+
[ -n "$host" ] || { echo "no-host-configured"; return; }
|
|
168
|
+
status=$(probe_http "https://${host}/rest/api/2/myself" "Authorization" "Bearer $tok") ;;
|
|
169
|
+
confluence)
|
|
170
|
+
host=$(pref global.hosts.confluence)
|
|
171
|
+
[ -n "$host" ] || { echo "no-host-configured"; return; }
|
|
172
|
+
status=$(probe_http "https://${host}/rest/api/user/current" "Authorization" "Bearer $tok") ;;
|
|
173
|
+
bitbucket_token)
|
|
174
|
+
host=$(pref global.hosts.bitbucket)
|
|
175
|
+
[ -n "$host" ] || { echo "no-host-configured"; return; }
|
|
176
|
+
local bbuser bbkey
|
|
177
|
+
bbkey=$(pref global.keychainMapping.bitbucket_user)
|
|
178
|
+
bbuser=$([ -n "$bbkey" ] && bash "$STORE" get bitbucket_user 2>/dev/null || echo "")
|
|
179
|
+
if [ -n "$bbuser" ]; then
|
|
180
|
+
status=$(probe_basic "https://${host}/rest/api/1.0/repos?limit=1" "$bbuser" "$tok")
|
|
181
|
+
else
|
|
182
|
+
status=$(probe_http "https://${host}/rest/api/1.0/repos?limit=1" "Authorization" "Bearer $tok")
|
|
183
|
+
fi ;;
|
|
184
|
+
github)
|
|
185
|
+
status=$(probe_http "https://api.github.com/user" "Authorization" "Bearer $tok") ;;
|
|
186
|
+
figma)
|
|
187
|
+
status=$(probe_http "https://api.figma.com/v1/me" "X-Figma-Token" "$tok") ;;
|
|
188
|
+
figma_mcp)
|
|
189
|
+
# An OAuth token for the Figma MCP server, not a REST PAT. Sending it to
|
|
190
|
+
# api.figma.com returns 403 for a perfectly healthy token, which is worse than
|
|
191
|
+
# not probing: it would send the user to regenerate something that works.
|
|
192
|
+
# Liveness for this one belongs to figma-mcp-refresh.sh, which owns the grant.
|
|
193
|
+
printf '%s' "$tok" | grep -q . && echo "not-probeable" || echo "malformed"
|
|
194
|
+
return ;;
|
|
195
|
+
npm)
|
|
196
|
+
status=$(probe_http "https://registry.npmjs.org/-/whoami" "Authorization" "Bearer $tok") ;;
|
|
197
|
+
fortify)
|
|
198
|
+
host=$(pref global.hosts.fortify)
|
|
199
|
+
[ -n "$host" ] || { echo "no-host-configured"; return; }
|
|
200
|
+
# Shapes copied from fetch-fortify.sh, which already works against SSC: the API
|
|
201
|
+
# lives under /ssc/, and SSC accepts either a Bearer token or its own
|
|
202
|
+
# `FortifyToken <base64>` scheme. Probing the wrong base path returned a 302 to
|
|
203
|
+
# the login page and read as a broken credential.
|
|
204
|
+
status=$(probe_http "https://${host}/ssc/api/v1/projects?limit=1" "Authorization" "Bearer $tok")
|
|
205
|
+
if [ "$(classify_status "$status")" != "reachable" ]; then
|
|
206
|
+
status=$(probe_http "https://${host}/ssc/api/v1/projects?limit=1" \
|
|
207
|
+
"Authorization" "FortifyToken $(printf '%s' "$tok" | base64 | tr -d '\n')")
|
|
208
|
+
fi ;;
|
|
209
|
+
graylog)
|
|
210
|
+
host=$(pref global.hosts.graylog)
|
|
211
|
+
[ -n "$host" ] || { echo "no-host-configured"; return; }
|
|
212
|
+
# Graylog PATs authenticate as basic auth with the literal password "token", and
|
|
213
|
+
# the API rejects requests without X-Requested-By. Both per fetch-graylog.sh.
|
|
214
|
+
status=$(probe_basic_hdr "https://${host}/api/system" "$tok" "token" \
|
|
215
|
+
"X-Requested-By" "multi-agent-pipeline") ;;
|
|
216
|
+
jenkins)
|
|
217
|
+
host=$(pref global.hosts.jenkins)
|
|
218
|
+
[ -n "$host" ] || { echo "no-host-configured"; return; }
|
|
219
|
+
status=$(probe_http "https://${host}/api/json" "Authorization" "Bearer $tok") ;;
|
|
220
|
+
firebase)
|
|
221
|
+
# A service-account JSON, not a bearer token: an OAuth exchange would be needed
|
|
222
|
+
# to reach Crashlytics, and the fetcher does that per issue. Verifying the shape
|
|
223
|
+
# is honest about what is known - well-formed, not proven reachable.
|
|
224
|
+
if printf '%s' "$tok" | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if d.get("project_id") and d.get("private_key") else 1)' 2>/dev/null; then
|
|
225
|
+
echo "well-formed"; return
|
|
226
|
+
fi
|
|
227
|
+
echo "malformed"; return ;;
|
|
228
|
+
appstore_connect_private_key)
|
|
229
|
+
printf '%s' "$tok" | grep -q "BEGIN PRIVATE KEY" && echo "well-formed" || echo "malformed"
|
|
230
|
+
return ;;
|
|
231
|
+
*)
|
|
232
|
+
echo "not-probeable"; return ;;
|
|
233
|
+
esac
|
|
234
|
+
classify_status "$status"
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# What each logical key unlocks, in the pipeline's own terms. This is the column that
|
|
238
|
+
# turns an inventory into an actionable question: "I hold `firebase`, so give me the
|
|
239
|
+
# issue URL and I will pull the stack trace" is only sayable if the capability is
|
|
240
|
+
# written down somewhere.
|
|
241
|
+
capability_of() {
|
|
242
|
+
case "$1" in
|
|
243
|
+
jira) echo "read the ticket, its comments and its linked issues; post the Phase 7 comment" ;;
|
|
244
|
+
bitbucket_token) echo "read the repo, open and update pull requests" ;;
|
|
245
|
+
bitbucket_user) echo "identify the PR author (paired with bitbucket_token)" ;;
|
|
246
|
+
github) echo "read issues, open pull requests, read Actions runs" ;;
|
|
247
|
+
confluence) echo "read linked pages and publish the analysis document" ;;
|
|
248
|
+
firebase) echo "pull a Crashlytics issue: stack frames, affected versions, device spread (needs the issue URL)" ;;
|
|
249
|
+
fortify) echo "pull a static-analysis finding and its remediation guidance" ;;
|
|
250
|
+
graylog) echo "pull request logs by transaction or conversation id (advisory)" ;;
|
|
251
|
+
figma|figma_mcp) echo "fetch design context, screenshots and Code Connect mappings" ;;
|
|
252
|
+
jenkins) echo "read build results" ;;
|
|
253
|
+
npm) echo "publish to the npm registry" ;;
|
|
254
|
+
appstore_connect_key_id|appstore_connect_issuer_id|appstore_connect_private_key)
|
|
255
|
+
echo "validate an archive against App Store rules before submission" ;;
|
|
256
|
+
claude_oauth_token|claude_oauth_token_fallback)
|
|
257
|
+
echo "run headless review and analysis passes" ;;
|
|
258
|
+
*) echo "(no capability recorded for this key)" ;;
|
|
259
|
+
esac
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
mapping_keys() {
|
|
263
|
+
[ -f "$PREFS" ] || return 0
|
|
264
|
+
python3 - "$PREFS" <<'PY' 2>/dev/null
|
|
265
|
+
import json, sys
|
|
266
|
+
try:
|
|
267
|
+
p = json.load(open(sys.argv[1]))
|
|
268
|
+
except Exception:
|
|
269
|
+
sys.exit(0)
|
|
270
|
+
m = (p.get("global") or {}).get("keychainMapping") or {}
|
|
271
|
+
for k, v in m.items():
|
|
272
|
+
print(f"{k}\t{'1' if v else '0'}")
|
|
273
|
+
PY
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Presence probe. The value lands in /dev/null; only the exit status is observed.
|
|
277
|
+
probe() {
|
|
278
|
+
[ -n "$STORE" ] || return 2
|
|
279
|
+
bash "$STORE" get "$1" >/dev/null 2>&1
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if [ -n "$QUERY" ]; then
|
|
283
|
+
probe "$QUERY"
|
|
284
|
+
rc=$?
|
|
285
|
+
case "$rc" in
|
|
286
|
+
0)
|
|
287
|
+
if [ "$PROBE" = "1" ]; then
|
|
288
|
+
echo "$QUERY: present, $(probe_one "$QUERY") - can $(capability_of "$QUERY")"
|
|
289
|
+
else
|
|
290
|
+
echo "$QUERY: present - can $(capability_of "$QUERY")"
|
|
291
|
+
fi
|
|
292
|
+
exit 0 ;;
|
|
293
|
+
2) echo "$QUERY: no credential helper on this host" >&2; exit 1 ;;
|
|
294
|
+
*) echo "$QUERY: NOT AVAILABLE - onboard it via /multi-agent:setup before relying on it" >&2; exit 1 ;;
|
|
295
|
+
esac
|
|
296
|
+
fi
|
|
297
|
+
|
|
298
|
+
ROWS=""
|
|
299
|
+
while IFS=$'\t' read -r key mapped; do
|
|
300
|
+
[ -z "$key" ] && continue
|
|
301
|
+
if [ "$mapped" != "1" ]; then
|
|
302
|
+
state="unmapped"
|
|
303
|
+
reach="not-probed"
|
|
304
|
+
elif probe "$key"; then
|
|
305
|
+
state="present"
|
|
306
|
+
# Only configured credentials are probed. A key the user never onboarded is not a
|
|
307
|
+
# problem to report - it is a capability they chose not to enable.
|
|
308
|
+
reach=$([ "$PROBE" = "1" ] && probe_one "$key" || echo "not-probed")
|
|
309
|
+
else
|
|
310
|
+
state="mapped-but-missing"
|
|
311
|
+
reach="not-probed"
|
|
312
|
+
fi
|
|
313
|
+
ROWS="${ROWS}${key}\t${state}\t${reach}\t$(capability_of "$key")\n"
|
|
314
|
+
done < <(mapping_keys)
|
|
315
|
+
|
|
316
|
+
if [ -z "$ROWS" ]; then
|
|
317
|
+
if [ "$MODE" = "json" ]; then
|
|
318
|
+
echo '{"status":"empty","reason":"no keychainMapping in preferences","credentials":[]}'
|
|
319
|
+
else
|
|
320
|
+
echo "no keychainMapping found in $PREFS - run /multi-agent:setup" >&2
|
|
321
|
+
fi
|
|
322
|
+
exit 0
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
if [ "$MODE" = "json" ]; then
|
|
326
|
+
# The rows travel through a temp file, not a pipe. `python3 - <<EOF` takes its SCRIPT
|
|
327
|
+
# from stdin, so a piped payload never reaches sys.stdin: --json came back with empty
|
|
328
|
+
# arrays while the table mode looked correct. Caught by smoke-credential-awareness.
|
|
329
|
+
ROWS_FILE="$(mktemp)"
|
|
330
|
+
trap 'rm -f "$ROWS_FILE"' EXIT
|
|
331
|
+
printf '%b' "$ROWS" > "$ROWS_FILE"
|
|
332
|
+
python3 - "$ROWS_FILE" <<'PYJSON'
|
|
333
|
+
import json, sys
|
|
334
|
+
rows = []
|
|
335
|
+
with open(sys.argv[1]) as fh:
|
|
336
|
+
for line in fh:
|
|
337
|
+
line = line.rstrip("\n")
|
|
338
|
+
if not line:
|
|
339
|
+
continue
|
|
340
|
+
parts = line.split("\t")
|
|
341
|
+
if len(parts) < 4:
|
|
342
|
+
continue
|
|
343
|
+
rows.append({
|
|
344
|
+
"logical": parts[0],
|
|
345
|
+
"state": parts[1],
|
|
346
|
+
"reachability": parts[2],
|
|
347
|
+
"capability": parts[3],
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
# `usable` is presence only, so a caller that never probed still gets a meaningful
|
|
351
|
+
# answer. `reachable` is the stronger claim and exists only after --probe.
|
|
352
|
+
OK_REACH = {"reachable", "well-formed"}
|
|
353
|
+
BLOCKED = {"auth-rejected", "malformed"}
|
|
354
|
+
probed = [r for r in rows if r["reachability"] != "not-probed"]
|
|
355
|
+
print(json.dumps({
|
|
356
|
+
"status": "ok",
|
|
357
|
+
"probed": bool(probed),
|
|
358
|
+
"usable": [r["logical"] for r in rows if r["state"] == "present"],
|
|
359
|
+
"needsAttention": [r["logical"] for r in rows if r["state"] != "present"],
|
|
360
|
+
# Separated on purpose: a dead token is the user's to refresh, an unreachable host
|
|
361
|
+
# is usually a closed VPN, and a missing host is a setup gap. Collapsing them into
|
|
362
|
+
# one "failed" bucket is what made the pipeline ask blind questions.
|
|
363
|
+
"reachable": [r["logical"] for r in probed if r["reachability"] in OK_REACH],
|
|
364
|
+
"authRejected": [r["logical"] for r in probed if r["reachability"] in BLOCKED],
|
|
365
|
+
"unreachable": [r["logical"] for r in probed if r["reachability"] == "unreachable"],
|
|
366
|
+
"noHostConfigured": [r["logical"] for r in probed if r["reachability"] == "no-host-configured"],
|
|
367
|
+
"credentials": rows,
|
|
368
|
+
}, indent=2))
|
|
369
|
+
PYJSON
|
|
370
|
+
else
|
|
371
|
+
printf '%b' "$ROWS" | awk -F'\t' '
|
|
372
|
+
BEGIN { printf "%-28s %-20s %-20s %s\n", "LOGICAL KEY", "STATE", "REACHABILITY", "UNLOCKS" }
|
|
373
|
+
{ printf "%-28s %-20s %-20s %s\n", $1, $2, $3, $4 }'
|
|
374
|
+
fi
|
|
@@ -13,15 +13,31 @@
|
|
|
13
13
|
# error instead of `bash: $CRED_STORE: command not found`.
|
|
14
14
|
#
|
|
15
15
|
# Usage from a skill bash block:
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# for r in "$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
17
|
+
# "$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
18
|
+
# "$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
19
|
+
# "$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
20
|
+
# [ -f "$r" ] || continue
|
|
21
|
+
# . "$r" 2>/dev/null || true
|
|
22
|
+
# if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
23
|
+
# done
|
|
24
|
+
# [ -n "${CRED_STORE:-}" ] || { echo "credential helper not found - run the installer" >&2; exit 1; }
|
|
19
25
|
# TOKEN=$("$CRED_STORE" get "<KEY>")
|
|
26
|
+
#
|
|
27
|
+
# Two rules, both learned from a silent failure:
|
|
28
|
+
# 1. Check the FILE EXISTS before sourcing it. `. <missing>` aborts the shell under
|
|
29
|
+
# `set -e`, `||` included, so a `.`-chain never reaches its later candidates.
|
|
30
|
+
# 2. Decide on `$CRED_STORE`, never on the source's exit status. Sourcing this file
|
|
31
|
+
# always succeeds by design, so the caller stays alive to report a useful error.
|
|
20
32
|
|
|
21
33
|
resolve_credential_store() {
|
|
34
|
+
# All three supported hosts. Codex was missing here, so a Codex-only install could
|
|
35
|
+
# not resolve the credential helper at all - every fetcher on that host reported
|
|
36
|
+
# "credential helper not found" while the file sat in ~/.codex/lib.
|
|
22
37
|
local cands=(
|
|
23
38
|
"$HOME/.claude/lib/credential-store.sh"
|
|
24
39
|
"$HOME/.copilot/lib/credential-store.sh"
|
|
40
|
+
"$HOME/.codex/lib/credential-store.sh"
|
|
25
41
|
)
|
|
26
42
|
for c in "${cands[@]}"; do
|
|
27
43
|
if [ -x "$c" ]; then
|
|
@@ -36,11 +52,13 @@ credential helper not found.
|
|
|
36
52
|
The multi-agent pipeline expects one of these to exist:
|
|
37
53
|
~/.claude/lib/credential-store.sh (Claude Code installs)
|
|
38
54
|
~/.copilot/lib/credential-store.sh (Copilot CLI installs)
|
|
55
|
+
~/.codex/lib/credential-store.sh (Codex CLI installs)
|
|
39
56
|
|
|
40
57
|
Install with:
|
|
41
58
|
npx @mmerterden/multi-agent-pipeline install --claude # Claude Code
|
|
42
59
|
npx @mmerterden/multi-agent-pipeline install --copilot # Copilot CLI
|
|
43
|
-
npx @mmerterden/multi-agent-pipeline install --
|
|
60
|
+
npx @mmerterden/multi-agent-pipeline install --codex # Codex CLI
|
|
61
|
+
npx @mmerterden/multi-agent-pipeline install --all # all three
|
|
44
62
|
|
|
45
63
|
Or override `CRED_STORE` to point at a custom path before sourcing this resolver.
|
|
46
64
|
MSG
|
|
@@ -53,5 +71,16 @@ MSG
|
|
|
53
71
|
if [ "${BASH_SOURCE[0]:-$0}" = "${0}" ]; then
|
|
54
72
|
resolve_credential_store && echo "$CRED_STORE"
|
|
55
73
|
else
|
|
56
|
-
|
|
74
|
+
# `|| :` matters, and it is not cosmetic.
|
|
75
|
+
#
|
|
76
|
+
# A sourced file runs in the caller's shell, so a bare failing command at this top
|
|
77
|
+
# level trips the caller's `set -e` DURING the source - before the caller's own
|
|
78
|
+
# `. resolver || fallback` can catch anything. Eleven runtime scripts loaded this
|
|
79
|
+
# resolver that way, all of them with `set -e`, so on any host where the first
|
|
80
|
+
# credential-store candidate was absent (a Copilot-only or Codex-only install) they
|
|
81
|
+
# died with a bare exit 1, no message, and their error branches unreachable.
|
|
82
|
+
#
|
|
83
|
+
# Sourcing therefore always succeeds. Callers MUST decide on `$CRED_STORE` being
|
|
84
|
+
# non-empty, never on the source's exit status - see the usage note above.
|
|
85
|
+
resolve_credential_store || :
|
|
57
86
|
fi
|
|
@@ -126,11 +126,33 @@ except Exception:
|
|
|
126
126
|
fi
|
|
127
127
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Confluence_Access_Token"
|
|
128
128
|
|
|
129
|
-
# Locate the
|
|
130
|
-
#
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
130
|
+
#
|
|
131
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
132
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
133
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
134
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
135
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
136
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
137
|
+
for _cred_resolver in \
|
|
138
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
139
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
140
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
141
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
142
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
143
|
+
[ -f "$_cred_resolver" ] || continue
|
|
144
|
+
# shellcheck source=/dev/null
|
|
145
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
146
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
147
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
148
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
149
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
150
|
+
done
|
|
151
|
+
unset _cred_resolver
|
|
152
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
153
|
+
printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"confluence","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
154
|
+
exit 2
|
|
155
|
+
fi
|
|
134
156
|
|
|
135
157
|
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
136
158
|
if [ -z "$TOKEN" ]; then
|
|
@@ -124,11 +124,33 @@ except Exception:
|
|
|
124
124
|
fi
|
|
125
125
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Firebase_Access_Json"
|
|
126
126
|
|
|
127
|
-
# Locate the
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
128
|
+
#
|
|
129
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
130
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
131
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
132
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
133
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
134
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
135
|
+
for _cred_resolver in \
|
|
136
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
137
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
138
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
139
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
140
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
141
|
+
[ -f "$_cred_resolver" ] || continue
|
|
142
|
+
# shellcheck source=/dev/null
|
|
143
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
144
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
145
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
146
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
147
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
148
|
+
done
|
|
149
|
+
unset _cred_resolver
|
|
150
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
151
|
+
printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"firebase","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
152
|
+
exit 2
|
|
153
|
+
fi
|
|
132
154
|
|
|
133
155
|
SA_JSON_B64=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
134
156
|
if [ -z "$SA_JSON_B64" ]; then
|
|
@@ -38,10 +38,24 @@
|
|
|
38
38
|
set -uo pipefail
|
|
39
39
|
|
|
40
40
|
PREFS="$HOME/.claude/multi-agent-preferences.json"
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
# Locate the resolver with an existence check, not a `.`-chain: sourcing a missing file
|
|
42
|
+
# aborts the shell under `set -e`, `||` included, so a chain skips both its later
|
|
43
|
+
# candidates and its trailing `|| true`. A missing store is tolerated here - CRED_STORE
|
|
44
|
+
# simply stays empty and the caller falls back to an env-supplied token.
|
|
45
|
+
for _cred_resolver in \
|
|
46
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
47
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
48
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
49
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
50
|
+
[ -f "$_cred_resolver" ] || continue
|
|
51
|
+
# shellcheck source=/dev/null
|
|
52
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
53
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
54
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
55
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
56
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
57
|
+
done
|
|
58
|
+
unset _cred_resolver
|
|
45
59
|
CRED_STORE="${CRED_STORE:-}"
|
|
46
60
|
FIGMA_API="https://api.figma.com/v1"
|
|
47
61
|
HTTP_TIMEOUT=60
|
|
@@ -128,11 +128,33 @@ except Exception:
|
|
|
128
128
|
fi
|
|
129
129
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Fortify_Access_Token"
|
|
130
130
|
|
|
131
|
-
# Locate the
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
132
|
+
#
|
|
133
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
134
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
135
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
136
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
137
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
138
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
139
|
+
for _cred_resolver in \
|
|
140
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
141
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
142
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
143
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
144
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
145
|
+
[ -f "$_cred_resolver" ] || continue
|
|
146
|
+
# shellcheck source=/dev/null
|
|
147
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
148
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
149
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
150
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
151
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
152
|
+
done
|
|
153
|
+
unset _cred_resolver
|
|
154
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
155
|
+
printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"fortify","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
156
|
+
exit 2
|
|
157
|
+
fi
|
|
136
158
|
|
|
137
159
|
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
138
160
|
if [ -z "$TOKEN" ]; then
|
|
@@ -118,11 +118,33 @@ except Exception:
|
|
|
118
118
|
fi
|
|
119
119
|
[ -z "$TOKEN_KEY" ] && TOKEN_KEY="${USER}_Graylog_Access_Token"
|
|
120
120
|
|
|
121
|
-
# Locate the
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
# Locate the resolver with an existence check, not a `.`-chain.
|
|
122
|
+
#
|
|
123
|
+
# Sourcing a file that does not exist aborts the shell under `set -e` - `||` included -
|
|
124
|
+
# so `. <candidate> || . <candidate> || { error }` reaches neither its later candidates
|
|
125
|
+
# nor its error branch. Every fetcher used that shape starting from `$HOME/.claude/...`,
|
|
126
|
+
# so on a Copilot-only or Codex-only install they all died with a bare exit 1 and no
|
|
127
|
+
# message. Reordering does not help: whichever candidate is absent aborts at that point.
|
|
128
|
+
# Checking for the file before sourcing it is the only safe form.
|
|
129
|
+
for _cred_resolver in \
|
|
130
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/credential-store-resolver.sh" \
|
|
131
|
+
"$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../lib" 2>/dev/null && pwd)/credential-store-resolver.sh" \
|
|
132
|
+
"$HOME/.claude/lib/credential-store-resolver.sh" \
|
|
133
|
+
"$HOME/.copilot/lib/credential-store-resolver.sh" \
|
|
134
|
+
"$HOME/.codex/lib/credential-store-resolver.sh"; do
|
|
135
|
+
[ -f "$_cred_resolver" ] || continue
|
|
136
|
+
# shellcheck source=/dev/null
|
|
137
|
+
. "$_cred_resolver" 2>/dev/null || true
|
|
138
|
+
# `if`, not `[ ... ] && break`: the latter is the loop body's last command and returns
|
|
139
|
+
# 1 when CRED_STORE is still empty, which under `set -e` kills the loop on the first
|
|
140
|
+
# candidate that does not resolve - the very case the loop exists to survive.
|
|
141
|
+
if [ -n "${CRED_STORE:-}" ]; then break; fi
|
|
142
|
+
done
|
|
143
|
+
unset _cred_resolver
|
|
144
|
+
if [ -z "${CRED_STORE:-}" ]; then
|
|
145
|
+
printf '%s\n' '{"status":"blocked","reason":"missing-credential-helper","service":"graylog","expected_key":"'"$TOKEN_KEY"'"}' >&2
|
|
146
|
+
exit 2
|
|
147
|
+
fi
|
|
126
148
|
|
|
127
149
|
TOKEN=$("$CRED_STORE" get "$TOKEN_KEY" 2>/dev/null || true)
|
|
128
150
|
if [ -z "$TOKEN" ]; then
|