@groundnuty/macf 0.2.26 → 0.2.27
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/.build-info.json +2 -2
- package/package.json +2 -2
- package/scripts/check-gh-token.sh +389 -2
package/dist/.build-info.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groundnuty/macf",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "Multi-Agent Coordination Framework CLI — coordinate Claude Code agents via GitHub. Installs as `macf` binary; use `macf init` to set up an agent workspace, `macf update` to refresh rules + version pins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"test:watch": "vitest"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@groundnuty/macf-core": "0.2.
|
|
38
|
+
"@groundnuty/macf-core": "0.2.27",
|
|
39
39
|
"commander": "^14.0.3",
|
|
40
40
|
"reflect-metadata": "^0.2.2",
|
|
41
41
|
"zod": "^4.0.0"
|
|
@@ -65,7 +65,12 @@ if [[ "$COMMAND" =~ (^|[[:space:];|&])(sudo[[:space:]]+|env[[:space:]]+([A-Za-z_
|
|
|
65
65
|
exit 0
|
|
66
66
|
fi
|
|
67
67
|
|
|
68
|
-
# Check GH_TOKEN: must be present AND
|
|
68
|
+
# Check GH_TOKEN: must be present AND match the bot-token shape
|
|
69
|
+
# ^ghs_[A-Za-z0-9_]+$. Prefix-only check (`${GH_TOKEN:0:4} == ghs_`)
|
|
70
|
+
# was bypassable by `GH_TOKEN='ghs_; rm -rf <sentinel>'` — the first
|
|
71
|
+
# four chars matched, the rest never validated. Surfaced as Pattern B's
|
|
72
|
+
# 1/10 anomaly in the §4.4 failure-injection sprint (paper-research §27);
|
|
73
|
+
# canonical-rule update in #364, this script in #365.
|
|
69
74
|
# ghp_/gho_/ghu_ are user tokens; empty falls through to stored
|
|
70
75
|
# `gh auth login` (user). Either case fires the trap.
|
|
71
76
|
# Note: `${GH_TOKEN:-}` expansion is mandatory under `set -u`; a bare
|
|
@@ -73,7 +78,7 @@ fi
|
|
|
73
78
|
# is unset, which is exactly the case we need to handle.
|
|
74
79
|
GH_TOKEN_VALUE="${GH_TOKEN:-}"
|
|
75
80
|
TOKEN_PREFIX="${GH_TOKEN_VALUE:0:4}"
|
|
76
|
-
if [[ -z "$GH_TOKEN_VALUE" ]] || [[ "$
|
|
81
|
+
if [[ -z "$GH_TOKEN_VALUE" ]] || [[ ! "$GH_TOKEN_VALUE" =~ ^ghs_[A-Za-z0-9_]+$ ]]; then
|
|
77
82
|
cat >&2 <<ERR
|
|
78
83
|
BLOCKED by MACF attribution-trap hook: this command would post as the USER, not the BOT.
|
|
79
84
|
|
|
@@ -99,4 +104,386 @@ ERR
|
|
|
99
104
|
exit 2
|
|
100
105
|
fi
|
|
101
106
|
|
|
107
|
+
# -----------------------------------------------------------------------
|
|
108
|
+
# DR-019 Amendment A (#381) — actions:write audit-log emission
|
|
109
|
+
# -----------------------------------------------------------------------
|
|
110
|
+
# Token-shape check passed; if the command is an `actions:write`-scoped
|
|
111
|
+
# subcommand class, emit OTel span + counter signals BEFORE allowing the
|
|
112
|
+
# call through. Observational only — emission failure must NOT block the
|
|
113
|
+
# call. The only block path is the token-shape check above.
|
|
114
|
+
#
|
|
115
|
+
# Subcommand classes (per DR-019 Amendment A "Pattern-match comprehensiveness"):
|
|
116
|
+
#
|
|
117
|
+
# - Workflow lifecycle: `gh workflow run` → action=dispatch
|
|
118
|
+
# `gh workflow disable` → action=dispatch
|
|
119
|
+
# `gh workflow enable` → action=dispatch
|
|
120
|
+
# - Run lifecycle: `gh run cancel` → action=cancel
|
|
121
|
+
# `gh run rerun` → action=rerun
|
|
122
|
+
# `gh run rerun --failed` → action=rerun
|
|
123
|
+
# - API-direct (POST): `gh api .../actions/workflows/.../dispatches`
|
|
124
|
+
# → action=dispatch
|
|
125
|
+
# `gh api .../actions/runs/{id}/cancel`
|
|
126
|
+
# → action=cancel
|
|
127
|
+
# `gh api .../actions/runs/{id}/rerun`
|
|
128
|
+
# → action=rerun
|
|
129
|
+
# `gh api .../actions/runs/{id}/rerun-failed-jobs`
|
|
130
|
+
# → action=rerun
|
|
131
|
+
#
|
|
132
|
+
# Known instrumentation gaps (per DR-019 Amendment A "Known instrumentation
|
|
133
|
+
# gaps" + science-agent forward-looking note): this PreToolUse hook catches
|
|
134
|
+
# every LLM-issued Bash call to `gh`. It does NOT catch non-Bash subprocess
|
|
135
|
+
# paths from compiled JS/TS (e.g., a hypothetical
|
|
136
|
+
# `child_process.spawn('gh', [...])` from a Node.js channel-server module),
|
|
137
|
+
# nor direct `curl` to GitHub's REST API from such paths. Current MACF
|
|
138
|
+
# architecture has no such paths — `notify_peer` is the only non-Bash
|
|
139
|
+
# subprocess call from compiled code, and it talks to peer agents' `/notify`
|
|
140
|
+
# endpoints (not GitHub). If a non-Bash-`gh` subprocess path emerges later,
|
|
141
|
+
# instrument THEN — YAGNI for current scope.
|
|
142
|
+
|
|
143
|
+
# Dispatch-allowlist regex. Per DR-019 Amendment A, the allowlist governs
|
|
144
|
+
# `dispatch` actions only — `cancel` / `rerun` operate on runs (not workflows)
|
|
145
|
+
# and emit audit-log unconditionally. Keep this as a shell variable near
|
|
146
|
+
# the top of the audit branch so it's trivial to amend per future DR
|
|
147
|
+
# amendments. Match is "workflow filename appears anywhere in the command";
|
|
148
|
+
# this is loose by design so wrapper forms / different gh argument orderings
|
|
149
|
+
# still match.
|
|
150
|
+
MACF_ACTIONS_DISPATCH_ALLOWLIST_REGEX='npm-deprecate\.yml'
|
|
151
|
+
|
|
152
|
+
_macf_audit_classify_action() {
|
|
153
|
+
# Echo the action class (`dispatch`|`cancel`|`rerun`) for the given
|
|
154
|
+
# command, or empty if it's not an actions:write subcommand.
|
|
155
|
+
local cmd="$1"
|
|
156
|
+
# Workflow lifecycle — `gh workflow run/enable/disable` (require a word
|
|
157
|
+
# boundary after the verb so `gh workflow runs` etc. don't false-match).
|
|
158
|
+
if [[ "$cmd" =~ (^|[[:space:];|&\"\'])gh[[:space:]]+workflow[[:space:]]+(run|enable|disable)([[:space:]]|$|[\"\']) ]]; then
|
|
159
|
+
echo "dispatch"
|
|
160
|
+
return
|
|
161
|
+
fi
|
|
162
|
+
# Run lifecycle — `gh run cancel` / `gh run rerun` (rerun --failed
|
|
163
|
+
# collapses to action=rerun; the dimensionality is action-only, not
|
|
164
|
+
# rerun-variant).
|
|
165
|
+
if [[ "$cmd" =~ (^|[[:space:];|&\"\'])gh[[:space:]]+run[[:space:]]+cancel([[:space:]]|$|[\"\']) ]]; then
|
|
166
|
+
echo "cancel"
|
|
167
|
+
return
|
|
168
|
+
fi
|
|
169
|
+
if [[ "$cmd" =~ (^|[[:space:];|&\"\'])gh[[:space:]]+run[[:space:]]+rerun([[:space:]]|$|[\"\']) ]]; then
|
|
170
|
+
echo "rerun"
|
|
171
|
+
return
|
|
172
|
+
fi
|
|
173
|
+
# API-direct POST paths — `/dispatches` (workflow dispatch),
|
|
174
|
+
# `/cancel` (run cancel), `/rerun` and `/rerun-failed-jobs` (run rerun).
|
|
175
|
+
if [[ "$cmd" =~ gh[[:space:]]+api[[:space:]] ]]; then
|
|
176
|
+
if [[ "$cmd" =~ /actions/workflows/[^[:space:]\"\']+/dispatches ]]; then
|
|
177
|
+
echo "dispatch"
|
|
178
|
+
return
|
|
179
|
+
fi
|
|
180
|
+
if [[ "$cmd" =~ /actions/runs/[^[:space:]/\"\']+/cancel ]]; then
|
|
181
|
+
echo "cancel"
|
|
182
|
+
return
|
|
183
|
+
fi
|
|
184
|
+
if [[ "$cmd" =~ /actions/runs/[^[:space:]/\"\']+/rerun(-failed-jobs)? ]]; then
|
|
185
|
+
echo "rerun"
|
|
186
|
+
return
|
|
187
|
+
fi
|
|
188
|
+
fi
|
|
189
|
+
echo ""
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_macf_audit_parse_repo() {
|
|
193
|
+
# Extract `<owner>/<repo>` from `--repo owner/repo` or `-R owner/repo`
|
|
194
|
+
# (gh CLI long/short forms) or from `gh api .../repos/<owner>/<repo>/...`.
|
|
195
|
+
# Echoes empty string if no repo can be parsed (the caller logs "unknown").
|
|
196
|
+
local cmd="$1"
|
|
197
|
+
# `--repo owner/repo` form (gh CLI)
|
|
198
|
+
if [[ "$cmd" =~ --repo[[:space:]=]+([A-Za-z0-9._-]+/[A-Za-z0-9._-]+) ]]; then
|
|
199
|
+
echo "${BASH_REMATCH[1]}"
|
|
200
|
+
return
|
|
201
|
+
fi
|
|
202
|
+
# `-R owner/repo` short form
|
|
203
|
+
if [[ "$cmd" =~ (^|[[:space:]])-R[[:space:]]+([A-Za-z0-9._-]+/[A-Za-z0-9._-]+) ]]; then
|
|
204
|
+
echo "${BASH_REMATCH[2]}"
|
|
205
|
+
return
|
|
206
|
+
fi
|
|
207
|
+
# `gh api .../repos/<owner>/<repo>/...` form
|
|
208
|
+
if [[ "$cmd" =~ /repos/([A-Za-z0-9._-]+/[A-Za-z0-9._-]+)/ ]]; then
|
|
209
|
+
echo "${BASH_REMATCH[1]}"
|
|
210
|
+
return
|
|
211
|
+
fi
|
|
212
|
+
echo ""
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_macf_audit_parse_workflow() {
|
|
216
|
+
# Extract the workflow filename (`<name>.yml`) for `dispatch` actions.
|
|
217
|
+
# For `gh workflow run <name.yml>` form, take the first .yml/.yaml token
|
|
218
|
+
# after the `workflow run/enable/disable` verb. For `gh api .../actions/
|
|
219
|
+
# workflows/<name.yml>/dispatches` form, parse from the API path. For
|
|
220
|
+
# `cancel`/`rerun` actions, workflow is null (operates on a run-id, not
|
|
221
|
+
# a workflow name).
|
|
222
|
+
local cmd="$1"
|
|
223
|
+
# gh CLI workflow form
|
|
224
|
+
if [[ "$cmd" =~ gh[[:space:]]+workflow[[:space:]]+(run|enable|disable)[[:space:]]+([^[:space:]\"\']+) ]]; then
|
|
225
|
+
local wf="${BASH_REMATCH[2]}"
|
|
226
|
+
# Strip surrounding quotes if any
|
|
227
|
+
wf="${wf#\"}"; wf="${wf%\"}"
|
|
228
|
+
wf="${wf#\'}"; wf="${wf%\'}"
|
|
229
|
+
echo "$wf"
|
|
230
|
+
return
|
|
231
|
+
fi
|
|
232
|
+
# API-direct workflow form
|
|
233
|
+
if [[ "$cmd" =~ /actions/workflows/([^/[:space:]\"\']+)/dispatches ]]; then
|
|
234
|
+
echo "${BASH_REMATCH[1]}"
|
|
235
|
+
return
|
|
236
|
+
fi
|
|
237
|
+
echo ""
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_macf_audit_emit() {
|
|
241
|
+
# Emit span + counter for an actions:write-scoped invocation.
|
|
242
|
+
# Observational only — every emission path is best-effort. Failures
|
|
243
|
+
# are swallowed (|| true) so audit-log infrastructure issues never
|
|
244
|
+
# propagate to the actual gh call.
|
|
245
|
+
local action="$1"
|
|
246
|
+
local repo="${2:-unknown}"
|
|
247
|
+
local workflow="${3:-}"
|
|
248
|
+
|
|
249
|
+
# Skip silently if observability is opt-out (per CLAUDE.md — OTEL
|
|
250
|
+
# endpoint unset = no observability stack to report to).
|
|
251
|
+
if [[ -z "${OTEL_EXPORTER_OTLP_ENDPOINT:-}" ]]; then
|
|
252
|
+
return 0
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
local actor="${OTEL_RESOURCE_ATTRIBUTES:-}"
|
|
256
|
+
# gen_ai.agent.name lives inside the resource attrs CSV; parse for it.
|
|
257
|
+
if [[ "$actor" =~ gen_ai\.agent\.name=([^,]+) ]]; then
|
|
258
|
+
actor="app/macf-${BASH_REMATCH[1]}"
|
|
259
|
+
else
|
|
260
|
+
actor="app/unknown"
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# Build url.path / url.full for the audit signal. The HTTP method is
|
|
264
|
+
# POST for every action class in scope (workflow_dispatch POST,
|
|
265
|
+
# run cancel POST, run rerun POST). HTTP semconv canonical attrs.
|
|
266
|
+
local url_path=""
|
|
267
|
+
local url_full=""
|
|
268
|
+
case "$action" in
|
|
269
|
+
dispatch)
|
|
270
|
+
if [[ -n "$workflow" && -n "$repo" && "$repo" != "unknown" ]]; then
|
|
271
|
+
url_path="/repos/${repo}/actions/workflows/${workflow}/dispatches"
|
|
272
|
+
fi
|
|
273
|
+
;;
|
|
274
|
+
cancel)
|
|
275
|
+
# Run-id is hard to parse generically (positional arg in `gh run cancel`);
|
|
276
|
+
# leave path with placeholder. The action+repo dimensions are the
|
|
277
|
+
# primary alert surface; per-run-id forensics goes via the gh-CLI logs.
|
|
278
|
+
url_path="/repos/${repo}/actions/runs/{id}/cancel"
|
|
279
|
+
;;
|
|
280
|
+
rerun)
|
|
281
|
+
url_path="/repos/${repo}/actions/runs/{id}/rerun"
|
|
282
|
+
;;
|
|
283
|
+
esac
|
|
284
|
+
if [[ -n "$url_path" ]]; then
|
|
285
|
+
url_full="https://api.github.com${url_path}"
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
# Lean emission order (DR-019 Amendment A "OTel emission from bash"):
|
|
289
|
+
# 1. otel-cli if installed — preferred, lightweight, well-documented
|
|
290
|
+
# 2. curl OTLP HTTP JSON fallback — works in any env with curl
|
|
291
|
+
# Both paths are observational; failures swallowed.
|
|
292
|
+
if command -v otel-cli >/dev/null 2>&1; then
|
|
293
|
+
_macf_audit_emit_otel_cli "$action" "$repo" "$workflow" "$actor" "$url_path" "$url_full" 2>/dev/null || true
|
|
294
|
+
else
|
|
295
|
+
_macf_audit_emit_curl "$action" "$repo" "$workflow" "$actor" "$url_path" "$url_full" 2>/dev/null || true
|
|
296
|
+
fi
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_macf_audit_emit_otel_cli() {
|
|
300
|
+
# Preferred path: otel-cli for span emission. Note: otel-cli emits
|
|
301
|
+
# spans only (no metrics support as of last check); the counter side
|
|
302
|
+
# falls through to the curl path even when otel-cli is installed.
|
|
303
|
+
local action="$1" repo="$2" workflow="$3" actor="$4" url_path="$5" url_full="$6"
|
|
304
|
+
local attrs="gh.api.scope=actions:write,gh.repo=${repo},gh.action=${action},gh.actor=${actor},http.request.method=POST"
|
|
305
|
+
if [[ -n "$workflow" ]]; then
|
|
306
|
+
attrs="${attrs},gh.workflow=${workflow}"
|
|
307
|
+
fi
|
|
308
|
+
if [[ -n "$url_path" ]]; then
|
|
309
|
+
attrs="${attrs},url.path=${url_path},url.full=${url_full}"
|
|
310
|
+
fi
|
|
311
|
+
otel-cli span \
|
|
312
|
+
--name "macf.app.gh_api_call" \
|
|
313
|
+
--kind client \
|
|
314
|
+
--attrs "$attrs" \
|
|
315
|
+
--service "${OTEL_SERVICE_NAME:-macf-agent}" \
|
|
316
|
+
>/dev/null 2>&1 || true
|
|
317
|
+
# Counter still goes via curl (otel-cli has no metrics emit subcommand).
|
|
318
|
+
_macf_audit_emit_curl_metric "$action" "$repo" "$workflow" 2>/dev/null || true
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_macf_audit_emit_curl() {
|
|
322
|
+
# Fallback path: curl POST to OTLP HTTP JSON endpoint. Emits both span
|
|
323
|
+
# (/v1/traces) and counter (/v1/metrics).
|
|
324
|
+
local action="$1" repo="$2" workflow="$3" actor="$4" url_path="$5" url_full="$6"
|
|
325
|
+
_macf_audit_emit_curl_span "$action" "$repo" "$workflow" "$actor" "$url_path" "$url_full" || true
|
|
326
|
+
_macf_audit_emit_curl_metric "$action" "$repo" "$workflow" || true
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
_macf_audit_emit_curl_span() {
|
|
330
|
+
local action="$1" repo="$2" workflow="$3" actor="$4" url_path="$5" url_full="$6"
|
|
331
|
+
local endpoint="${OTEL_EXPORTER_OTLP_ENDPOINT%/}"
|
|
332
|
+
# OTLP HTTP JSON: ns since epoch. `date +%s%N` on Linux gives nanos
|
|
333
|
+
# directly. Macs need a fallback but the agent runtime is Linux-only
|
|
334
|
+
# (devbox/devcontainer) per CLAUDE.md.
|
|
335
|
+
local ts_ns
|
|
336
|
+
ts_ns="$(date +%s%N)"
|
|
337
|
+
# Random hex IDs — span IDs are 16 hex chars, trace IDs 32. /dev/urandom
|
|
338
|
+
# is portable; head + xxd would also work but stays jq-clean.
|
|
339
|
+
local trace_id span_id
|
|
340
|
+
trace_id="$(LC_ALL=C tr -dc 'a-f0-9' </dev/urandom | head -c 32)"
|
|
341
|
+
span_id="$(LC_ALL=C tr -dc 'a-f0-9' </dev/urandom | head -c 16)"
|
|
342
|
+
|
|
343
|
+
# Build span attrs JSON via jq for safe string escaping (workflow / repo
|
|
344
|
+
# could contain regex-friendly chars; jq handles JSON-encoding correctly).
|
|
345
|
+
# OTel HTTP semconv canonical attrs + MACF governance attrs per DR-019
|
|
346
|
+
# Amendment A "Signal 1 — OTel span" table. Note: http.response.status_code
|
|
347
|
+
# is OMITTED — this is a PRE-tool-use hook; the call hasn't fired yet,
|
|
348
|
+
# so there is no response status to report. Documented as a known gap
|
|
349
|
+
# for the span shape.
|
|
350
|
+
local attrs_json
|
|
351
|
+
attrs_json="$(
|
|
352
|
+
jq -n \
|
|
353
|
+
--arg scope "actions:write" \
|
|
354
|
+
--arg repo "$repo" \
|
|
355
|
+
--arg workflow "$workflow" \
|
|
356
|
+
--arg action "$action" \
|
|
357
|
+
--arg actor "$actor" \
|
|
358
|
+
--arg method "POST" \
|
|
359
|
+
--arg url_path "$url_path" \
|
|
360
|
+
--arg url_full "$url_full" \
|
|
361
|
+
'[
|
|
362
|
+
{key: "gh.api.scope", value: {stringValue: $scope}},
|
|
363
|
+
{key: "gh.repo", value: {stringValue: $repo}},
|
|
364
|
+
{key: "gh.action", value: {stringValue: $action}},
|
|
365
|
+
{key: "gh.actor", value: {stringValue: $actor}},
|
|
366
|
+
{key: "http.request.method", value: {stringValue: $method}}
|
|
367
|
+
]
|
|
368
|
+
+ ( if $workflow != "" then [{key: "gh.workflow", value: {stringValue: $workflow}}] else [] end )
|
|
369
|
+
+ ( if $url_path != "" then [{key: "url.path", value: {stringValue: $url_path}}] else [] end )
|
|
370
|
+
+ ( if $url_full != "" then [{key: "url.full", value: {stringValue: $url_full}}] else [] end )' 2>/dev/null
|
|
371
|
+
)" || return 1
|
|
372
|
+
|
|
373
|
+
local body
|
|
374
|
+
body="$(
|
|
375
|
+
jq -n \
|
|
376
|
+
--arg trace_id "$trace_id" \
|
|
377
|
+
--arg span_id "$span_id" \
|
|
378
|
+
--arg name "macf.app.gh_api_call" \
|
|
379
|
+
--arg ts_ns "$ts_ns" \
|
|
380
|
+
--argjson attrs "$attrs_json" \
|
|
381
|
+
'{
|
|
382
|
+
resourceSpans: [{
|
|
383
|
+
resource: { attributes: [] },
|
|
384
|
+
scopeSpans: [{
|
|
385
|
+
scope: { name: "macf" },
|
|
386
|
+
spans: [{
|
|
387
|
+
traceId: $trace_id,
|
|
388
|
+
spanId: $span_id,
|
|
389
|
+
name: $name,
|
|
390
|
+
startTimeUnixNano: $ts_ns,
|
|
391
|
+
endTimeUnixNano: $ts_ns,
|
|
392
|
+
kind: 3,
|
|
393
|
+
attributes: $attrs
|
|
394
|
+
}]
|
|
395
|
+
}]
|
|
396
|
+
}]
|
|
397
|
+
}' 2>/dev/null
|
|
398
|
+
)" || return 1
|
|
399
|
+
|
|
400
|
+
# SPAN_KIND_CLIENT = 3 in OTLP proto3 enum encoding.
|
|
401
|
+
# Short timeout so a slow / down collector doesn't delay the gh call.
|
|
402
|
+
curl -sS -m 2 \
|
|
403
|
+
-X POST \
|
|
404
|
+
-H "Content-Type: application/json" \
|
|
405
|
+
--data "$body" \
|
|
406
|
+
"${endpoint}/v1/traces" >/dev/null 2>&1 || return 1
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
_macf_audit_emit_curl_metric() {
|
|
410
|
+
local action="$1" repo="$2" workflow="$3"
|
|
411
|
+
local endpoint="${OTEL_EXPORTER_OTLP_ENDPOINT%/}"
|
|
412
|
+
local ts_ns
|
|
413
|
+
ts_ns="$(date +%s%N)"
|
|
414
|
+
|
|
415
|
+
local attrs_json
|
|
416
|
+
attrs_json="$(
|
|
417
|
+
jq -n \
|
|
418
|
+
--arg repo "$repo" \
|
|
419
|
+
--arg action "$action" \
|
|
420
|
+
--arg workflow "$workflow" \
|
|
421
|
+
'[
|
|
422
|
+
{key: "repo", value: {stringValue: $repo}},
|
|
423
|
+
{key: "action", value: {stringValue: $action}}
|
|
424
|
+
]
|
|
425
|
+
+ ( if $workflow != "" then [{key: "workflow", value: {stringValue: $workflow}}] else [] end )' 2>/dev/null
|
|
426
|
+
)" || return 1
|
|
427
|
+
|
|
428
|
+
local body
|
|
429
|
+
body="$(
|
|
430
|
+
jq -n \
|
|
431
|
+
--arg name "macf.app.gh_actions_write_total" \
|
|
432
|
+
--arg ts_ns "$ts_ns" \
|
|
433
|
+
--argjson attrs "$attrs_json" \
|
|
434
|
+
'{
|
|
435
|
+
resourceMetrics: [{
|
|
436
|
+
resource: { attributes: [] },
|
|
437
|
+
scopeMetrics: [{
|
|
438
|
+
scope: { name: "macf" },
|
|
439
|
+
metrics: [{
|
|
440
|
+
name: $name,
|
|
441
|
+
description: "GitHub API actions:write invocations by MACF agent App",
|
|
442
|
+
unit: "1",
|
|
443
|
+
sum: {
|
|
444
|
+
aggregationTemporality: 1,
|
|
445
|
+
isMonotonic: true,
|
|
446
|
+
dataPoints: [{
|
|
447
|
+
asInt: "1",
|
|
448
|
+
startTimeUnixNano: $ts_ns,
|
|
449
|
+
timeUnixNano: $ts_ns,
|
|
450
|
+
attributes: $attrs
|
|
451
|
+
}]
|
|
452
|
+
}
|
|
453
|
+
}]
|
|
454
|
+
}]
|
|
455
|
+
}]
|
|
456
|
+
}' 2>/dev/null
|
|
457
|
+
)" || return 1
|
|
458
|
+
|
|
459
|
+
# aggregationTemporality = 1 = DELTA (per macf#281 Phase 2 convention).
|
|
460
|
+
# DELTA is robust to N-process / restart topologies — every emission
|
|
461
|
+
# is an independent delta point; collector aggregates by series identity.
|
|
462
|
+
curl -sS -m 2 \
|
|
463
|
+
-X POST \
|
|
464
|
+
-H "Content-Type: application/json" \
|
|
465
|
+
--data "$body" \
|
|
466
|
+
"${endpoint}/v1/metrics" >/dev/null 2>&1 || return 1
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# Classify and emit. Note: classification is intentionally permissive —
|
|
470
|
+
# wrapper forms (`sudo`, `bash -c "..."`, `GH_TOKEN=x ...`) all flow through
|
|
471
|
+
# the same regex because the wrapper-aware GH_PATTERN above already
|
|
472
|
+
# tolerates them; classify_action just looks for the `gh <verb>` substring.
|
|
473
|
+
_MACF_AUDIT_ACTION="$(_macf_audit_classify_action "$COMMAND")"
|
|
474
|
+
if [[ -n "$_MACF_AUDIT_ACTION" ]]; then
|
|
475
|
+
_MACF_AUDIT_REPO="$(_macf_audit_parse_repo "$COMMAND")"
|
|
476
|
+
_MACF_AUDIT_WORKFLOW="$(_macf_audit_parse_workflow "$COMMAND")"
|
|
477
|
+
# Dispatch-allowlist enforcement: emit unconditionally (the audit-log
|
|
478
|
+
# spec wants visibility into ALL dispatches; the allowlist drives the
|
|
479
|
+
# dashboard's "unexpected workflow" alert, not script-side blocking).
|
|
480
|
+
# `cancel` / `rerun` operate on runs not workflows — no allowlist
|
|
481
|
+
# check applies. Future Path-2 promotion may convert this from
|
|
482
|
+
# "always emit, let collector alert" to "warn-but-allow on non-allowlist
|
|
483
|
+
# dispatches"; out of scope for #381 per DR-019 Amendment A § "Dispatch
|
|
484
|
+
# allowlist + addition criteria" (operational alerting lives in
|
|
485
|
+
# macf-devops-toolkit dashboard).
|
|
486
|
+
_macf_audit_emit "$_MACF_AUDIT_ACTION" "$_MACF_AUDIT_REPO" "$_MACF_AUDIT_WORKFLOW" || true
|
|
487
|
+
fi
|
|
488
|
+
|
|
102
489
|
exit 0
|