@matt82198/aesop 0.1.0-beta.5 → 0.1.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 +158 -5
- package/LICENSE +66 -21
- package/README.md +97 -33
- package/aesop.config.example.json +6 -0
- package/bin/CLAUDE.md +12 -3
- package/bin/cli.js +211 -40
- package/daemons/CLAUDE.md +1 -1
- package/daemons/backup-fleet.sh +209 -51
- package/daemons/run-watchdog.sh +208 -62
- package/dash/dash-extra.mjs +73 -4
- package/dash/watchdog-gui.sh +41 -35
- package/docs/ARCHITECTURE.md +296 -0
- package/docs/CONCEPTS.md +254 -0
- package/docs/CONFIGURE.md +256 -0
- package/docs/FIRST-WAVE.md +276 -0
- package/docs/INSTALL.md +224 -0
- package/docs/README.md +176 -27
- package/docs/autonomous-swe.md +149 -0
- package/docs/reproduce.md +121 -0
- package/hooks/CLAUDE.md +28 -0
- package/hooks/install-waveguard.sh +68 -0
- package/hooks/pre-commit-waveguard.sh +26 -0
- package/hooks/pre-push-policy.sh +253 -15
- package/monitor/CLAUDE.md +3 -3
- package/monitor/collect-signals.mjs +133 -20
- package/package.json +15 -7
- package/scan/CLAUDE.md +30 -0
- package/skills/CLAUDE.md +1 -0
- package/state_store/ingest.py +18 -1
- package/state_store/projections.py +78 -4
- package/state_store/store.py +102 -7
- package/tools/CLAUDE.md +25 -18
- package/tools/alert_bridge.py +14 -10
- package/tools/bench_runner.py +438 -0
- package/tools/buildlog.py +4 -7
- package/tools/ci_merge_wait.py +215 -34
- package/tools/common.py +62 -0
- package/tools/cost_ceiling.py +191 -0
- package/tools/dash.js +102 -0
- package/tools/doctor.js +220 -0
- package/tools/fleet_ledger.py +189 -17
- package/tools/halt.py +172 -0
- package/tools/healthcheck.py +24 -21
- package/tools/heartbeat.py +4 -7
- package/tools/metrics_gate.py +8 -2
- package/tools/orchestrator_status.py +4 -9
- package/tools/reconcile.py +277 -0
- package/tools/rotate_logs.py +152 -62
- package/tools/scanner_selftest.py +28 -3
- package/tools/secret_scan.py +359 -90
- package/tools/self_stats.py +292 -4
- package/tools/status.js +187 -0
- package/tools/verify_agent_inspector.py +289 -0
- package/tools/verify_prboard.py +344 -0
- package/tools/watch.js +49 -0
- package/ui/CLAUDE.md +2 -0
- package/ui/agents.py +332 -33
- package/ui/api/tracker.py +15 -7
- package/ui/collectors.py +125 -32
- package/ui/config.py +22 -2
- package/ui/cost.py +98 -3
- package/ui/csrf.py +2 -1
- package/ui/handler.py +209 -21
- package/ui/sse.py +102 -16
- package/ui/wave_prs.py +230 -0
- package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
- package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/monitor/.signal-state.json +0 -3
- package/monitor/ACTIONS.log +0 -1
- package/monitor/BRIEF.md +0 -24
- package/monitor/SIGNALS.json +0 -54
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/web/.gitattributes +0 -13
- package/ui/web/dist/assets/index-2LZDQirC.js +0 -9
- package/ui/web/dist/assets/index-D4M1qyOv.css +0 -1
- package/ui/web/index.html +0 -13
- package/ui/web/package-lock.json +0 -2225
- package/ui/web/package.json +0 -26
- package/ui/web/src/App.test.tsx +0 -74
- package/ui/web/src/App.tsx +0 -142
- package/ui/web/src/CONTRIBUTING-UI.md +0 -49
- package/ui/web/src/components/AgentRow.css +0 -187
- package/ui/web/src/components/AgentRow.test.tsx +0 -209
- package/ui/web/src/components/AgentRow.tsx +0 -207
- package/ui/web/src/components/AgentsPanel.css +0 -108
- package/ui/web/src/components/AgentsPanel.test.tsx +0 -41
- package/ui/web/src/components/AgentsPanel.tsx +0 -58
- package/ui/web/src/components/AlertsPanel.css +0 -88
- package/ui/web/src/components/AlertsPanel.test.tsx +0 -51
- package/ui/web/src/components/AlertsPanel.tsx +0 -67
- package/ui/web/src/components/BacklogPanel.test.tsx +0 -126
- package/ui/web/src/components/BacklogPanel.tsx +0 -122
- package/ui/web/src/components/CostChart.css +0 -110
- package/ui/web/src/components/CostChart.test.tsx +0 -144
- package/ui/web/src/components/CostChart.tsx +0 -152
- package/ui/web/src/components/CostTable.css +0 -93
- package/ui/web/src/components/CostTable.test.tsx +0 -165
- package/ui/web/src/components/CostTable.tsx +0 -94
- package/ui/web/src/components/EventsFeed.css +0 -68
- package/ui/web/src/components/EventsFeed.test.tsx +0 -36
- package/ui/web/src/components/EventsFeed.tsx +0 -31
- package/ui/web/src/components/HealthHeader.css +0 -137
- package/ui/web/src/components/HealthHeader.test.tsx +0 -278
- package/ui/web/src/components/HealthHeader.tsx +0 -281
- package/ui/web/src/components/InboxForm.css +0 -135
- package/ui/web/src/components/InboxForm.test.tsx +0 -208
- package/ui/web/src/components/InboxForm.tsx +0 -116
- package/ui/web/src/components/MessagesTail.module.css +0 -144
- package/ui/web/src/components/MessagesTail.test.tsx +0 -176
- package/ui/web/src/components/MessagesTail.tsx +0 -94
- package/ui/web/src/components/ReposPanel.css +0 -90
- package/ui/web/src/components/ReposPanel.test.tsx +0 -45
- package/ui/web/src/components/ReposPanel.tsx +0 -67
- package/ui/web/src/components/Scorecard.css +0 -106
- package/ui/web/src/components/Scorecard.test.tsx +0 -117
- package/ui/web/src/components/Scorecard.tsx +0 -85
- package/ui/web/src/components/Timeline.module.css +0 -151
- package/ui/web/src/components/Timeline.test.tsx +0 -215
- package/ui/web/src/components/Timeline.tsx +0 -99
- package/ui/web/src/components/TrackerBoard.test.tsx +0 -121
- package/ui/web/src/components/TrackerBoard.tsx +0 -107
- package/ui/web/src/components/TrackerCard.test.tsx +0 -180
- package/ui/web/src/components/TrackerCard.tsx +0 -160
- package/ui/web/src/components/TrackerForm.test.tsx +0 -189
- package/ui/web/src/components/TrackerForm.tsx +0 -144
- package/ui/web/src/lib/api.ts +0 -218
- package/ui/web/src/lib/format.test.ts +0 -89
- package/ui/web/src/lib/format.ts +0 -103
- package/ui/web/src/lib/sanitizeUrl.test.ts +0 -84
- package/ui/web/src/lib/sanitizeUrl.ts +0 -38
- package/ui/web/src/lib/types.ts +0 -230
- package/ui/web/src/lib/useHashRoute.test.ts +0 -60
- package/ui/web/src/lib/useHashRoute.ts +0 -23
- package/ui/web/src/lib/useSSE.ts +0 -175
- package/ui/web/src/main.tsx +0 -10
- package/ui/web/src/styles/global.css +0 -179
- package/ui/web/src/styles/theme.css +0 -184
- package/ui/web/src/styles/work.css +0 -572
- package/ui/web/src/test/fixtures.ts +0 -385
- package/ui/web/src/test/setup.ts +0 -49
- package/ui/web/src/views/Activity.module.css +0 -43
- package/ui/web/src/views/Activity.test.tsx +0 -89
- package/ui/web/src/views/Activity.tsx +0 -31
- package/ui/web/src/views/Cost.css +0 -87
- package/ui/web/src/views/Cost.test.tsx +0 -142
- package/ui/web/src/views/Cost.tsx +0 -54
- package/ui/web/src/views/Overview.css +0 -51
- package/ui/web/src/views/Overview.test.tsx +0 -76
- package/ui/web/src/views/Overview.tsx +0 -46
- package/ui/web/src/views/Work.test.tsx +0 -82
- package/ui/web/src/views/Work.tsx +0 -79
- package/ui/web/src/vite-env.d.ts +0 -10
- package/ui/web/tsconfig.json +0 -22
- package/ui/web/vite.config.ts +0 -25
- package/ui/web/vitest.config.ts +0 -12
package/hooks/pre-push-policy.sh
CHANGED
|
@@ -3,14 +3,45 @@ set -uo pipefail
|
|
|
3
3
|
|
|
4
4
|
json_escape() {
|
|
5
5
|
# Escape backslashes first, then quotes, then control chars for valid JSON
|
|
6
|
-
# Finding 5: Handle control characters (\
|
|
6
|
+
# Finding 5: Handle ALL C0 control characters (\x00-\x08, \x0b-\x0c, \x0e-\x1f)
|
|
7
7
|
local s="$1"
|
|
8
8
|
s="${s//\\/\\\\}"
|
|
9
9
|
s="${s//\"/\\\"}"
|
|
10
10
|
s="${s//$'\n'/\\n}"
|
|
11
11
|
s="${s//$'\r'/\\r}"
|
|
12
12
|
s="${s//$'\t'/\\t}"
|
|
13
|
-
|
|
13
|
+
# Escape remaining C0 control characters as \u00XX
|
|
14
|
+
# Using sed with explicit byte mappings for each C0 char not yet escaped
|
|
15
|
+
printf '%s' "$s" | sed \
|
|
16
|
+
-e 's/[\x00]/\\u0000/g' \
|
|
17
|
+
-e 's/[\x01]/\\u0001/g' \
|
|
18
|
+
-e 's/[\x02]/\\u0002/g' \
|
|
19
|
+
-e 's/[\x03]/\\u0003/g' \
|
|
20
|
+
-e 's/[\x04]/\\u0004/g' \
|
|
21
|
+
-e 's/[\x05]/\\u0005/g' \
|
|
22
|
+
-e 's/[\x06]/\\u0006/g' \
|
|
23
|
+
-e 's/[\x07]/\\u0007/g' \
|
|
24
|
+
-e 's/[\x08]/\\u0008/g' \
|
|
25
|
+
-e 's/[\x0b]/\\u000b/g' \
|
|
26
|
+
-e 's/[\x0c]/\\u000c/g' \
|
|
27
|
+
-e 's/[\x0e]/\\u000e/g' \
|
|
28
|
+
-e 's/[\x0f]/\\u000f/g' \
|
|
29
|
+
-e 's/[\x10]/\\u0010/g' \
|
|
30
|
+
-e 's/[\x11]/\\u0011/g' \
|
|
31
|
+
-e 's/[\x12]/\\u0012/g' \
|
|
32
|
+
-e 's/[\x13]/\\u0013/g' \
|
|
33
|
+
-e 's/[\x14]/\\u0014/g' \
|
|
34
|
+
-e 's/[\x15]/\\u0015/g' \
|
|
35
|
+
-e 's/[\x16]/\\u0016/g' \
|
|
36
|
+
-e 's/[\x17]/\\u0017/g' \
|
|
37
|
+
-e 's/[\x18]/\\u0018/g' \
|
|
38
|
+
-e 's/[\x19]/\\u0019/g' \
|
|
39
|
+
-e 's/[\x1a]/\\u001a/g' \
|
|
40
|
+
-e 's/[\x1b]/\\u001b/g' \
|
|
41
|
+
-e 's/[\x1c]/\\u001c/g' \
|
|
42
|
+
-e 's/[\x1d]/\\u001d/g' \
|
|
43
|
+
-e 's/[\x1e]/\\u001e/g' \
|
|
44
|
+
-e 's/[\x1f]/\\u001f/g'
|
|
14
45
|
}
|
|
15
46
|
compute_sha256() {
|
|
16
47
|
# P1-Bug2 fix: Single helper for sha256sum with fallback to shasum
|
|
@@ -250,6 +281,55 @@ check_branch_policy() {
|
|
|
250
281
|
return 0
|
|
251
282
|
}
|
|
252
283
|
|
|
284
|
+
get_commit_range() {
|
|
285
|
+
# Parse pre-push stdin to build commit range(s) for scanning.
|
|
286
|
+
# Format: <local-ref> <local-sha> <remote-ref> <remote-sha>
|
|
287
|
+
# A single push (git push --all / multiple branches / multiple tags in one
|
|
288
|
+
# invocation) can feed MULTIPLE ref tuples on stdin, one per line. Emits
|
|
289
|
+
# ONE "remote-sha..local-sha" range PER valid tuple found, one per output
|
|
290
|
+
# line (like check_branch_policy, which already iterates every tuple to
|
|
291
|
+
# check branch policy) -- P3 wave-25 fix: this used to `return 0` after the
|
|
292
|
+
# FIRST tuple, so a multi-ref push only ever scanned the first branch's
|
|
293
|
+
# range and every other ref in the same push silently bypassed the secret
|
|
294
|
+
# scan. Returns 0 if at least one range was emitted, 1 if none could be
|
|
295
|
+
# parsed (tty, empty stdin, or no valid tuple) -- single-ref callers see
|
|
296
|
+
# exactly the same one-line output as before.
|
|
297
|
+
local local_ref local_sha remote_ref remote_sha
|
|
298
|
+
local found=0
|
|
299
|
+
|
|
300
|
+
if [ -t 0 ]; then
|
|
301
|
+
# Running interactively on a tty; no stdin to parse
|
|
302
|
+
return 1
|
|
303
|
+
fi
|
|
304
|
+
|
|
305
|
+
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha || [ -n "$local_ref" ]; do
|
|
306
|
+
# Skip empty lines
|
|
307
|
+
if [ -z "$remote_ref" ]; then
|
|
308
|
+
continue
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# Found a valid ref tuple; build its range
|
|
312
|
+
# If remote_sha is all zeros (new branch), use merge-base with default branch
|
|
313
|
+
if [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
314
|
+
# New branch: find merge-base with main/master
|
|
315
|
+
local default_branch="main"
|
|
316
|
+
if ! git rev-parse "$default_branch" >/dev/null 2>&1; then
|
|
317
|
+
default_branch="master"
|
|
318
|
+
fi
|
|
319
|
+
printf '%s..%s\n' "$default_branch" "$local_sha"
|
|
320
|
+
else
|
|
321
|
+
# Existing branch: use remote sha as base
|
|
322
|
+
printf '%s..%s\n' "$remote_sha" "$local_sha"
|
|
323
|
+
fi
|
|
324
|
+
found=1
|
|
325
|
+
done
|
|
326
|
+
|
|
327
|
+
if [ "$found" -eq 1 ]; then
|
|
328
|
+
return 0
|
|
329
|
+
fi
|
|
330
|
+
return 1
|
|
331
|
+
}
|
|
332
|
+
|
|
253
333
|
check_secret_scan() {
|
|
254
334
|
local scan_bin
|
|
255
335
|
if command -v python >/dev/null 2>&1; then
|
|
@@ -270,17 +350,44 @@ check_secret_scan() {
|
|
|
270
350
|
return 0
|
|
271
351
|
fi
|
|
272
352
|
|
|
273
|
-
#
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
353
|
+
# Parse pre-push stdin to get commit range(s), then scan files in each range.
|
|
354
|
+
# A multi-ref push (git push --all, or multiple branches/tags in one
|
|
355
|
+
# invocation) yields one range per line from get_commit_range(); scan EVERY
|
|
356
|
+
# one and fail if ANY range is dirty (P3 wave-25 fix: previously only the
|
|
357
|
+
# first ref tuple's range was ever scanned).
|
|
358
|
+
local commit_ranges
|
|
359
|
+
commit_ranges=$(get_commit_range)
|
|
360
|
+
local parse_exit_code=$?
|
|
361
|
+
|
|
362
|
+
if [ $parse_exit_code -ne 0 ] || [ -z "$commit_ranges" ]; then
|
|
363
|
+
# Malformed stdin or unable to parse: fail-CLOSED (security P1 fix)
|
|
364
|
+
# Only fail-open for missing scanner tool, not for malformed input
|
|
365
|
+
log_block "secret_scan_stdin_parse_failed"
|
|
366
|
+
printf 'Error: Could not parse pre-push stdin for commit range (malformed or empty)\n' >&2
|
|
367
|
+
return 1
|
|
281
368
|
fi
|
|
282
369
|
|
|
283
|
-
|
|
370
|
+
local overall_exit_code=0
|
|
371
|
+
local range
|
|
372
|
+
while IFS= read -r range || [ -n "$range" ]; do
|
|
373
|
+
[ -z "$range" ] && continue
|
|
374
|
+
|
|
375
|
+
# Run scanner on this range and capture output
|
|
376
|
+
local scan_output
|
|
377
|
+
scan_output=$("$scan_bin" "$scan_script" --range "$range" 2>&1)
|
|
378
|
+
local scan_exit_code=$?
|
|
379
|
+
|
|
380
|
+
# Surface all output including ALLOWED-DOC findings to stderr
|
|
381
|
+
if [ -n "$scan_output" ]; then
|
|
382
|
+
printf '%s\n' "$scan_output" >&2
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
if [ $scan_exit_code -ne 0 ]; then
|
|
386
|
+
overall_exit_code=$scan_exit_code
|
|
387
|
+
fi
|
|
388
|
+
done <<< "$commit_ranges"
|
|
389
|
+
|
|
390
|
+
return $overall_exit_code
|
|
284
391
|
}
|
|
285
392
|
|
|
286
393
|
log_event() {
|
|
@@ -707,12 +814,130 @@ run_test_mode() {
|
|
|
707
814
|
test_failed=$((test_failed + 1))
|
|
708
815
|
fi
|
|
709
816
|
|
|
817
|
+
printf '\n=== Test 12: get_commit_range emits ALL ref tuples (multi-ref push) ===\n'
|
|
818
|
+
(
|
|
819
|
+
cd "$tmpdir" || exit 1
|
|
820
|
+
git checkout -q feature/test 2>/dev/null || git checkout -q -b feature/multiref 2>/dev/null
|
|
821
|
+
local_sha=$(git rev-parse HEAD 2>/dev/null || echo "0000000")
|
|
822
|
+
|
|
823
|
+
# Simulate a multi-ref push (git push --all / multiple branches in one
|
|
824
|
+
# invocation): two ref tuples on stdin. Wave-25 P3 regression: the old
|
|
825
|
+
# get_commit_range() `return 0`'d after the FIRST tuple, so a multi-ref
|
|
826
|
+
# push only ever produced ONE range.
|
|
827
|
+
stdin_input="refs/heads/branch-a $local_sha refs/heads/branch-a 0000000000000000000000000000000000000000
|
|
828
|
+
refs/heads/branch-b $local_sha refs/heads/branch-b 0000000000000000000000000000000000000000"
|
|
829
|
+
|
|
830
|
+
ranges=$(printf '%s\n' "$stdin_input" | get_commit_range)
|
|
831
|
+
range_count=$(printf '%s\n' "$ranges" | grep -c '\.\.')
|
|
832
|
+
|
|
833
|
+
if [ "$range_count" -eq 2 ]; then
|
|
834
|
+
printf 'PASS: get_commit_range emitted %d ranges for a 2-ref push\n' "$range_count"
|
|
835
|
+
else
|
|
836
|
+
printf 'FAIL: Expected 2 ranges for a 2-ref push, got %d. Output: %s\n' "$range_count" "$ranges"
|
|
837
|
+
exit 1
|
|
838
|
+
fi
|
|
839
|
+
)
|
|
840
|
+
if [ $? -eq 0 ]; then
|
|
841
|
+
test_passed=$((test_passed + 1))
|
|
842
|
+
else
|
|
843
|
+
test_failed=$((test_failed + 1))
|
|
844
|
+
fi
|
|
845
|
+
|
|
846
|
+
printf '\n=== Test 13: check_secret_scan scans EVERY ref range, not just the first ===\n'
|
|
847
|
+
(
|
|
848
|
+
export AESOP_ROOT="$tmpdir/aesop_multiref"
|
|
849
|
+
mkdir -p "$AESOP_ROOT/state" "$AESOP_ROOT/tools"
|
|
850
|
+
|
|
851
|
+
# Mock scanner: fails only when the --range argument's local-sha side
|
|
852
|
+
# matches the "dirty" ref's local sha. This proves BOTH ranges from a
|
|
853
|
+
# multi-ref push are actually scanned -- not just the first -- since the
|
|
854
|
+
# dirty ref is deliberately placed SECOND in stdin.
|
|
855
|
+
cat > "$AESOP_ROOT/tools/secret_scan.py" <<'SCANNER'
|
|
856
|
+
#!/usr/bin/env python3
|
|
857
|
+
import sys
|
|
858
|
+
args = sys.argv[1:]
|
|
859
|
+
range_arg = args[args.index("--range") + 1] if "--range" in args else ""
|
|
860
|
+
if "2222222222222222222222222222222222222222" in range_arg:
|
|
861
|
+
sys.exit(1)
|
|
862
|
+
sys.exit(0)
|
|
863
|
+
SCANNER
|
|
864
|
+
chmod +x "$AESOP_ROOT/tools/secret_scan.py"
|
|
865
|
+
|
|
866
|
+
# First ref tuple is clean, SECOND ref tuple is dirty. Pre-fix,
|
|
867
|
+
# get_commit_range only ever emitted the first tuple's range, so this
|
|
868
|
+
# second dirty ref would have silently bypassed the scan entirely.
|
|
869
|
+
stdin_input="refs/heads/clean-branch 1111111111111111111111111111111111111111 refs/heads/clean-branch 0000000000000000000000000000000000000000
|
|
870
|
+
refs/heads/dirty-branch 2222222222222222222222222222222222222222 refs/heads/dirty-branch 0000000000000000000000000000000000000000"
|
|
871
|
+
|
|
872
|
+
if printf '%s\n' "$stdin_input" | check_secret_scan >/dev/null 2>&1; then
|
|
873
|
+
printf 'FAIL: check_secret_scan should have blocked on the second (dirty) ref range\n'
|
|
874
|
+
exit 1
|
|
875
|
+
fi
|
|
876
|
+
printf 'PASS: check_secret_scan blocked on a dirty range from a NON-first ref tuple\n'
|
|
877
|
+
)
|
|
878
|
+
if [ $? -eq 0 ]; then
|
|
879
|
+
test_passed=$((test_passed + 1))
|
|
880
|
+
else
|
|
881
|
+
test_failed=$((test_failed + 1))
|
|
882
|
+
fi
|
|
883
|
+
|
|
884
|
+
printf '\n=== Test 14: main() stdin capture-once does not starve the second consumer ===\n'
|
|
885
|
+
(
|
|
886
|
+
export AESOP_ROOT="$tmpdir/aesop_stdin_double_read"
|
|
887
|
+
mkdir -p "$AESOP_ROOT/state" "$AESOP_ROOT/tools"
|
|
888
|
+
cat > "$AESOP_ROOT/tools/secret_scan.py" <<'SCANNER'
|
|
889
|
+
#!/usr/bin/env python3
|
|
890
|
+
import sys
|
|
891
|
+
sys.exit(0)
|
|
892
|
+
SCANNER
|
|
893
|
+
chmod +x "$AESOP_ROOT/tools/secret_scan.py"
|
|
894
|
+
|
|
895
|
+
cd "$tmpdir" || exit 1
|
|
896
|
+
git checkout -q feature/test 2>/dev/null || git checkout -q -b feature/stdin_double 2>/dev/null
|
|
897
|
+
local_sha=$(git rev-parse HEAD 2>/dev/null || echo "0000000")
|
|
898
|
+
|
|
899
|
+
stdin_input="refs/heads/feature/test $local_sha refs/heads/feature/test 0000000000000000000000000000000000000000"
|
|
900
|
+
|
|
901
|
+
# Replicate exactly what main() now does: capture stdin ONCE, then feed
|
|
902
|
+
# each consumer its own here-string copy. Before this fix, main() called
|
|
903
|
+
# check_branch_policy (reading fd0 directly) then check_secret_scan
|
|
904
|
+
# (also reading fd0 directly) against the SAME real pipe -- the first
|
|
905
|
+
# call drained it, so get_commit_range inside the second call always
|
|
906
|
+
# saw EOF and fail-closed, blocking EVERY push regardless of content.
|
|
907
|
+
captured=$(printf '%s\n' "$stdin_input" | cat)
|
|
908
|
+
|
|
909
|
+
if ! check_branch_policy <<< "$captured" >/dev/null 2>&1; then
|
|
910
|
+
printf 'FAIL: check_branch_policy unexpectedly blocked the feature branch\n'
|
|
911
|
+
exit 1
|
|
912
|
+
fi
|
|
913
|
+
|
|
914
|
+
stderr_output=$( { check_secret_scan <<< "$captured"; } 2>&1 1>/dev/null )
|
|
915
|
+
scan_exit=$?
|
|
916
|
+
|
|
917
|
+
if [ $scan_exit -ne 0 ]; then
|
|
918
|
+
printf 'FAIL: check_secret_scan failed after check_branch_policy already read the SAME captured stdin (stdin-starvation regression). stderr: %s\n' "$stderr_output"
|
|
919
|
+
exit 1
|
|
920
|
+
fi
|
|
921
|
+
|
|
922
|
+
if printf '%s' "$stderr_output" | grep -q 'parse_failed\|malformed'; then
|
|
923
|
+
printf 'FAIL: check_secret_scan reported malformed/empty stdin -- it never saw the ref tuple\n'
|
|
924
|
+
exit 1
|
|
925
|
+
fi
|
|
926
|
+
|
|
927
|
+
printf 'PASS: check_secret_scan still sees the ref tuple after check_branch_policy read the same captured stdin\n'
|
|
928
|
+
)
|
|
929
|
+
if [ $? -eq 0 ]; then
|
|
930
|
+
test_passed=$((test_passed + 1))
|
|
931
|
+
else
|
|
932
|
+
test_failed=$((test_failed + 1))
|
|
933
|
+
fi
|
|
934
|
+
|
|
710
935
|
printf '\n=== Test Results ===\n'
|
|
711
936
|
printf 'PASSED: %d\n' "$test_passed"
|
|
712
937
|
printf 'FAILED: %d\n' "$test_failed"
|
|
713
938
|
|
|
714
939
|
if [ "$test_failed" -eq 0 ]; then
|
|
715
|
-
printf '\nAll
|
|
940
|
+
printf '\nAll 14 tests passed.\n'
|
|
716
941
|
return 0
|
|
717
942
|
else
|
|
718
943
|
printf '\nSome tests failed.\n'
|
|
@@ -732,14 +957,27 @@ main() {
|
|
|
732
957
|
exit $?
|
|
733
958
|
fi
|
|
734
959
|
|
|
735
|
-
# git pre-push provides ref info on stdin,
|
|
736
|
-
|
|
960
|
+
# git pre-push provides ref info on stdin, and BOTH check_branch_policy and
|
|
961
|
+
# check_secret_scan (via get_commit_range) need to read every ref tuple.
|
|
962
|
+
# Capture the real pipe ONCE here and hand each consumer its own here-string
|
|
963
|
+
# copy -- reading a pipe twice on the same fd starves the second reader
|
|
964
|
+
# (once check_branch_policy drains it, get_commit_range would see nothing
|
|
965
|
+
# but EOF and fail-closed on every push). A here-string preserves each
|
|
966
|
+
# function's existing tty-vs-pipe read semantics unchanged (an interactive
|
|
967
|
+
# tty yields no captured content, which both functions already treat the
|
|
968
|
+
# same way as "no ref tuples" via their existing fallback/fail-closed paths).
|
|
969
|
+
local prepush_stdin=""
|
|
970
|
+
if [ ! -t 0 ]; then
|
|
971
|
+
prepush_stdin=$(cat)
|
|
972
|
+
fi
|
|
973
|
+
|
|
974
|
+
if ! check_branch_policy <<< "$prepush_stdin"; then
|
|
737
975
|
printf 'Error: Push to main/master is blocked by policy\n' >&2
|
|
738
976
|
log_block "push_to_protected_branch"
|
|
739
977
|
exit 1
|
|
740
978
|
fi
|
|
741
979
|
|
|
742
|
-
if ! check_secret_scan; then
|
|
980
|
+
if ! check_secret_scan <<< "$prepush_stdin"; then
|
|
743
981
|
printf 'Error: Secret scan failed. Push blocked.\n' >&2
|
|
744
982
|
log_block "secret_scan_failure"
|
|
745
983
|
exit 1
|
package/monitor/CLAUDE.md
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
7
|
-
- **CHARTER.md** — Governance document; defines
|
|
7
|
+
- **CHARTER.md** — Governance document; defines 11 signal checks (4 of them extended/opt-in), action tiers (AUTO/PROPOSE), outputs, single-instance guard, single-writer discipline. Read-only; behavior changes only via PROPOSALS/behavioral-PR flow.
|
|
8
8
|
- **collect-signals.mjs** — Deterministic signal collector (Node.js built-ins only); emits BRIEF.md + SIGNALS.json each cycle; reads config from env or aesop.config.json.
|
|
9
9
|
- **BRIEF.md** — Human-readable cycle snapshot (heartbeats, git state, memory freshness, log rotation, junk sprawl, stray scripts, security alerts, respawn watch, cost cadence, unreviewed prompts); overwritten each cycle; runtime.
|
|
10
10
|
- **SIGNALS.json** — Machine-readable metrics (same signal keys as BRIEF); JSON; overwritten each cycle; runtime.
|
|
11
|
-
- **PROPOSALS.md** — Structured inbox for user approval (idempotent per signal key, append-only); never edited by monitor after emission; tracks respawn-watch-breach, stray-repo-scripts, security-alerts-high-med, stale-memory-files; gitignored.
|
|
11
|
+
- **PROPOSALS.md** — Structured inbox for user approval (idempotent per signal key, append-only); never edited by monitor after emission; tracks isolation-violation-detected, respawn-watch-breach, stray-repo-scripts, security-alerts-high-med, stale-memory-files; gitignored.
|
|
12
12
|
- **ACTIONS.log** — Append-only log of AUTO tier actions taken (heartbeat updates, log rotation invokes, junk quarantine); runtime; gitignored.
|
|
13
13
|
- **.monitor-heartbeat** — Epoch timestamp (line 1) for single-instance liveness check (<300s = skip cycle); runtime; gitignored.
|
|
14
14
|
- **.signal-state.json** — Sidecar state (cycleCount, etc.); runtime; gitignored.
|
|
15
15
|
|
|
16
16
|
## Contracts
|
|
17
17
|
|
|
18
|
-
**Signal keys collected**: heartbeats, git, memory, logs, junk, strayRepo, alerts, respawnWatch, costTick, unreviewedPrompts. Outputs: BRIEF.md (human), SIGNALS.json (machine); both runtime/gitignored. PROPOSALS.md (tracked, append-only); ACTIONS.log (runtime/gitignored).
|
|
18
|
+
**Signal keys collected**: heartbeats, git, memory, logs, junk, strayRepo, alerts, respawnWatch, costTick, unreviewedPrompts, isolationViolations. Outputs: BRIEF.md (human), SIGNALS.json (machine); both runtime/gitignored. PROPOSALS.md (tracked, append-only); ACTIONS.log (runtime/gitignored).
|
|
19
19
|
|
|
20
20
|
**Extended signals (opt-in, default OFF)**: checks 5 (junk), 6 (strayRepo), 8 (respawnWatch), and 10 (unreviewedPrompts) are extended — disabled by default.
|
|
21
21
|
- Config key: `monitor.extended_signals` (boolean, default `false`) in aesop.config.json.
|
|
@@ -33,8 +33,8 @@ function expandPath(pathStr) {
|
|
|
33
33
|
if (pathStr.startsWith('~')) {
|
|
34
34
|
return path.join(os.homedir(), pathStr.slice(1));
|
|
35
35
|
}
|
|
36
|
-
// Expand environment variables like $VAR or
|
|
37
|
-
return pathStr.replace(/\$\{?([A-
|
|
36
|
+
// Expand environment variables like $VAR, $VAR_1, or ${myVar}
|
|
37
|
+
return pathStr.replace(/\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g, (match, varName) => {
|
|
38
38
|
return process.env[varName] || match;
|
|
39
39
|
});
|
|
40
40
|
}
|
|
@@ -67,6 +67,12 @@ const STATE_DIR = expandPath(
|
|
|
67
67
|
path.join(AESOP_ROOT, 'state')
|
|
68
68
|
);
|
|
69
69
|
|
|
70
|
+
const FLEET_LEDGER = expandPath(
|
|
71
|
+
process.env.AESOP_FLEET_LEDGER ||
|
|
72
|
+
config.fleet_ledger ||
|
|
73
|
+
path.join(BRAIN_ROOT, 'FLEET-LEDGER.md')
|
|
74
|
+
);
|
|
75
|
+
|
|
70
76
|
const MON = path.join(AESOP_ROOT, 'monitor');
|
|
71
77
|
|
|
72
78
|
// Config-driven thresholds and feature flags
|
|
@@ -259,7 +265,8 @@ function checkMemoryFreshness() {
|
|
|
259
265
|
}
|
|
260
266
|
|
|
261
267
|
// 4) Log file status check
|
|
262
|
-
|
|
268
|
+
// Optional: accepts pre-read content for SECURITY-ALERTS.log to avoid redundant read
|
|
269
|
+
function checkLogFiles(securityAlertsContent = null) {
|
|
263
270
|
const logFiles = [
|
|
264
271
|
path.join(STATE_DIR, 'FLEET-BACKUP.log'),
|
|
265
272
|
path.join(STATE_DIR, 'SECURITY-ALERTS.log'),
|
|
@@ -273,7 +280,11 @@ function checkLogFiles() {
|
|
|
273
280
|
if (st) {
|
|
274
281
|
sizeKb = (st.size / 1024).toFixed(1);
|
|
275
282
|
try {
|
|
276
|
-
|
|
283
|
+
// Use pre-read content for SECURITY-ALERTS.log if provided
|
|
284
|
+
const isSecurityAlerts = logPath === path.join(STATE_DIR, 'SECURITY-ALERTS.log');
|
|
285
|
+
const content = isSecurityAlerts && securityAlertsContent !== null
|
|
286
|
+
? securityAlertsContent
|
|
287
|
+
: fs.readFileSync(logPath, 'utf8');
|
|
277
288
|
lineCount = content.split('\n').filter(l => l.trim()).length;
|
|
278
289
|
} catch {
|
|
279
290
|
// skip
|
|
@@ -345,8 +356,10 @@ function detectStrayRepoScripts() {
|
|
|
345
356
|
.map(s => s.trim())
|
|
346
357
|
.filter(Boolean);
|
|
347
358
|
for (const f of new Set(recent)) {
|
|
348
|
-
|
|
349
|
-
|
|
359
|
+
// Normalize git-output paths to forward slashes immediately after read
|
|
360
|
+
const normalized = f.replace(/\\/g, '/');
|
|
361
|
+
if (/^[^/]+\.(py|mjs|js|sql)$/.test(normalized)) {
|
|
362
|
+
strayRepo.push(`${path.basename(repoPath)}: ${normalized}`);
|
|
350
363
|
}
|
|
351
364
|
}
|
|
352
365
|
}
|
|
@@ -354,12 +367,14 @@ function detectStrayRepoScripts() {
|
|
|
354
367
|
}
|
|
355
368
|
|
|
356
369
|
// 7) Security alert review
|
|
357
|
-
|
|
370
|
+
// Optional: accepts pre-read content to avoid redundant file read
|
|
371
|
+
function checkSecurityAlerts(securityAlertsContent = null) {
|
|
358
372
|
const alertLog = path.join(STATE_DIR, 'SECURITY-ALERTS.log');
|
|
359
373
|
const st = stat(alertLog);
|
|
360
374
|
if (!st) return { count: 0, highMedCount: 0 };
|
|
361
375
|
try {
|
|
362
|
-
|
|
376
|
+
// Use pre-read content if provided, otherwise read file
|
|
377
|
+
const content = securityAlertsContent !== null ? securityAlertsContent : fs.readFileSync(alertLog, 'utf8');
|
|
363
378
|
const lines = content.split('\n');
|
|
364
379
|
const highMedCount = lines.filter(l => /HIGH|MED/.test(l) && !l.includes('SUPPRESSED-FP')).length;
|
|
365
380
|
return { count: lines.filter(l => l.trim()).length, highMedCount };
|
|
@@ -368,20 +383,89 @@ function checkSecurityAlerts() {
|
|
|
368
383
|
}
|
|
369
384
|
}
|
|
370
385
|
|
|
371
|
-
//
|
|
386
|
+
// === Cursor tracking utilities (for ledger incremental read) ===
|
|
387
|
+
// Helper: Load cursor state (byte offset + line hash of last processed line)
|
|
388
|
+
function loadCursor() {
|
|
389
|
+
const cursorPath = path.join(MON, '.ledger-cursor.json');
|
|
390
|
+
try {
|
|
391
|
+
if (fs.existsSync(cursorPath)) {
|
|
392
|
+
return JSON.parse(fs.readFileSync(cursorPath, 'utf8'));
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
// Parse error; treat as missing cursor
|
|
396
|
+
}
|
|
397
|
+
return { byteOffset: 0, lineHash: '' };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Helper: Save cursor state (byte offset + line hash)
|
|
401
|
+
function saveCursor(byteOffset, lineHash) {
|
|
402
|
+
const cursorPath = path.join(MON, '.ledger-cursor.json');
|
|
403
|
+
try {
|
|
404
|
+
fs.mkdirSync(MON, { recursive: true });
|
|
405
|
+
fs.writeFileSync(cursorPath, JSON.stringify({ byteOffset, lineHash }, null, 2), 'utf8');
|
|
406
|
+
} catch (e) {
|
|
407
|
+
// Fail-open: log warning but don't crash
|
|
408
|
+
console.error(`Warning: Failed to save ledger cursor: ${e.message}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Helper: Compute a simple hash of a string (for integrity check)
|
|
413
|
+
function simpleHash(str) {
|
|
414
|
+
let h = 0;
|
|
415
|
+
for (let i = 0; i < str.length; i++) {
|
|
416
|
+
h = ((h << 5) - h) + str.charCodeAt(i);
|
|
417
|
+
h |= 0; // Convert to 32-bit integer
|
|
418
|
+
}
|
|
419
|
+
return Math.abs(h).toString(16);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 8) Respawn watch (Rule 6 retry cap) — incremental read with cursor
|
|
372
423
|
function detectRespawnWatch() {
|
|
373
424
|
const respawnWatch = [];
|
|
374
|
-
|
|
375
|
-
|
|
425
|
+
if (!fs.existsSync(FLEET_LEDGER)) return respawnWatch;
|
|
426
|
+
|
|
376
427
|
try {
|
|
377
|
-
const
|
|
378
|
-
const
|
|
428
|
+
const buffer = fs.readFileSync(FLEET_LEDGER);
|
|
429
|
+
const content = buffer.toString('utf8');
|
|
430
|
+
|
|
431
|
+
// Load cursor to find starting point
|
|
432
|
+
const cursor = loadCursor();
|
|
433
|
+
let startOffset = cursor.byteOffset;
|
|
434
|
+
|
|
435
|
+
// If cursor points beyond file, reset to start (file was truncated or rotated)
|
|
436
|
+
if (startOffset > buffer.length) {
|
|
437
|
+
startOffset = 0;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Extract only new content since last cursor position
|
|
441
|
+
const newContent = content.substring(startOffset);
|
|
442
|
+
|
|
443
|
+
// Split new content into lines, filtering empty and header lines
|
|
444
|
+
// Keep data rows (start with | and contain data), skip header rows (| --- | or column names)
|
|
445
|
+
const newLines = newContent.split('\n')
|
|
446
|
+
.filter(l => {
|
|
447
|
+
const trimmed = l.trim();
|
|
448
|
+
if (!trimmed) return false; // Skip empty lines
|
|
449
|
+
// Skip header separator rows (contain --- between pipes)
|
|
450
|
+
if (/\|\s*---/.test(trimmed)) return false;
|
|
451
|
+
// Skip column name row (has 'timestamp' or 'agent' or 'dispatch' or 'description')
|
|
452
|
+
if (trimmed.includes('timestamp') && trimmed.includes('agent')) return false;
|
|
453
|
+
// Keep all other rows starting with |
|
|
454
|
+
return trimmed.startsWith('|');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// If no new lines, return early (nothing to process)
|
|
458
|
+
if (newLines.length === 0) {
|
|
459
|
+
return respawnWatch;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Process new lines to detect respawn violations
|
|
379
463
|
const windowSize = 50;
|
|
380
|
-
const
|
|
464
|
+
const recentLines = newLines.slice(Math.max(0, newLines.length - windowSize));
|
|
381
465
|
const signatures = {};
|
|
382
466
|
const normalize = (desc) => (desc || '').substring(0, 40).toLowerCase().trim();
|
|
383
|
-
|
|
384
|
-
|
|
467
|
+
|
|
468
|
+
for (const line of recentLines) {
|
|
385
469
|
const parts = line.split('|').map(s => s.trim());
|
|
386
470
|
if (parts.length >= 4) {
|
|
387
471
|
const description = parts[3];
|
|
@@ -389,6 +473,8 @@ function detectRespawnWatch() {
|
|
|
389
473
|
if (sig) signatures[sig] = (signatures[sig] || 0) + 1;
|
|
390
474
|
}
|
|
391
475
|
}
|
|
476
|
+
|
|
477
|
+
// Check for violations (count > 3)
|
|
392
478
|
for (const [sig, count] of Object.entries(signatures)) {
|
|
393
479
|
if (count > 3) {
|
|
394
480
|
respawnWatch.push({
|
|
@@ -398,9 +484,17 @@ function detectRespawnWatch() {
|
|
|
398
484
|
});
|
|
399
485
|
}
|
|
400
486
|
}
|
|
401
|
-
|
|
402
|
-
//
|
|
487
|
+
|
|
488
|
+
// Update cursor to end of file
|
|
489
|
+
const lastLine = newLines[newLines.length - 1] || '';
|
|
490
|
+
const newByteOffset = buffer.length;
|
|
491
|
+
const newLineHash = simpleHash(lastLine);
|
|
492
|
+
saveCursor(newByteOffset, newLineHash);
|
|
493
|
+
} catch (e) {
|
|
494
|
+
// Fail-open on error
|
|
495
|
+
console.error(`Warning: Failed to read FLEET-LEDGER: ${e.message}`);
|
|
403
496
|
}
|
|
497
|
+
|
|
404
498
|
return respawnWatch;
|
|
405
499
|
}
|
|
406
500
|
|
|
@@ -673,6 +767,14 @@ function emitProposal(signalKey, problem, suggestedChange) {
|
|
|
673
767
|
|
|
674
768
|
// If lock acquisition failed (fail-closed), skip emission for this cycle
|
|
675
769
|
if (!lockDir) {
|
|
770
|
+
// On lock timeout, append a one-line MISSED-PROPOSAL record to ACTIONS.log (append-only)
|
|
771
|
+
// so the condition surfaces next cycle
|
|
772
|
+
const actionsLogPath = path.join(MON, 'ACTIONS.log');
|
|
773
|
+
try {
|
|
774
|
+
fs.appendFileSync(actionsLogPath, `[${timestamp}] MISSED-PROPOSAL: ${signalKey} (lock timeout)\n`, 'utf8');
|
|
775
|
+
} catch (e) {
|
|
776
|
+
// Fail-open: ignore write errors to ACTIONS.log
|
|
777
|
+
}
|
|
676
778
|
return;
|
|
677
779
|
}
|
|
678
780
|
|
|
@@ -721,17 +823,28 @@ ${suggestedChange}
|
|
|
721
823
|
}
|
|
722
824
|
|
|
723
825
|
// === Main ===
|
|
826
|
+
// Read SECURITY-ALERTS.log once and share the content with both consumers to avoid redundant file read
|
|
827
|
+
let securityAlertsContent = null;
|
|
828
|
+
const securityAlertsPath = path.join(STATE_DIR, 'SECURITY-ALERTS.log');
|
|
829
|
+
try {
|
|
830
|
+
if (fs.existsSync(securityAlertsPath)) {
|
|
831
|
+
securityAlertsContent = fs.readFileSync(securityAlertsPath, 'utf8');
|
|
832
|
+
}
|
|
833
|
+
} catch {
|
|
834
|
+
// File exists but is unreadable; consumers will handle gracefully
|
|
835
|
+
}
|
|
836
|
+
|
|
724
837
|
const staleLoops = checkHeartbeats();
|
|
725
838
|
const gitState = checkGitState();
|
|
726
839
|
const memory = checkMemoryFreshness();
|
|
727
|
-
const logFiles = checkLogFiles();
|
|
840
|
+
const logFiles = checkLogFiles(securityAlertsContent);
|
|
728
841
|
const isolationViolations = detectIsolationViolations();
|
|
729
842
|
|
|
730
843
|
// Extended signal checks (5, 6, 8, 10) — skipped if extended_signals is OFF
|
|
731
844
|
const junk = extendedSignals ? detectJunkScripts() : { skipped: true };
|
|
732
845
|
const strayRepo = extendedSignals ? detectStrayRepoScripts() : { skipped: true };
|
|
733
846
|
|
|
734
|
-
const alerts = checkSecurityAlerts();
|
|
847
|
+
const alerts = checkSecurityAlerts(securityAlertsContent);
|
|
735
848
|
|
|
736
849
|
const respawnWatch = extendedSignals ? detectRespawnWatch() : { skipped: true };
|
|
737
850
|
const { cycleCount, costTick } = trackCostCadence();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matt82198/aesop",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Multi-agent orchestration for AI coding agents, designed to survive failure \u00e2\u20ac\u201d a plain-file brain, git as the only durable layer, cheap subagent fleets, and guardrails enforced in code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aesop": "bin/cli.js"
|
|
@@ -12,12 +12,20 @@
|
|
|
12
12
|
"dash/",
|
|
13
13
|
"monitor/",
|
|
14
14
|
"mcp/",
|
|
15
|
-
"tools
|
|
16
|
-
"
|
|
15
|
+
"tools/*.py",
|
|
16
|
+
"tools/*.sh",
|
|
17
|
+
"tools/*.mjs",
|
|
18
|
+
"tools/*.js",
|
|
19
|
+
"tools/*.md",
|
|
20
|
+
"ui/*.py",
|
|
21
|
+
"ui/api/*.py",
|
|
22
|
+
"ui/*.md",
|
|
23
|
+
"ui/web/dist/",
|
|
17
24
|
"docs/",
|
|
18
25
|
"hooks/",
|
|
19
26
|
"skills/",
|
|
20
|
-
"state_store
|
|
27
|
+
"state_store/*.py",
|
|
28
|
+
"state_store/*.md",
|
|
21
29
|
"scan/",
|
|
22
30
|
"aesop.config.example.json",
|
|
23
31
|
"CLAUDE-TEMPLATE.md",
|
|
@@ -38,7 +46,7 @@
|
|
|
38
46
|
"resilience",
|
|
39
47
|
"template"
|
|
40
48
|
],
|
|
41
|
-
"license": "
|
|
49
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
42
50
|
"author": "Matt Culliton",
|
|
43
51
|
"repository": {
|
|
44
52
|
"type": "git",
|
|
@@ -52,8 +60,8 @@
|
|
|
52
60
|
"access": "public"
|
|
53
61
|
},
|
|
54
62
|
"scripts": {
|
|
55
|
-
"test": "node --test --test-force-exit --test-timeout=60000 tests
|
|
56
|
-
"test:node": "node --test --test-force-exit --test-timeout=60000 tests
|
|
63
|
+
"test": "node --test --test-force-exit --test-timeout=60000 tests/*.test.mjs",
|
|
64
|
+
"test:node": "node --test --test-force-exit --test-timeout=60000 tests/*.test.mjs",
|
|
57
65
|
"test:sh": "bash tests/test_pre_push_policy.sh && bash tests/test-run-watchdog.sh && bash tests/backup-fleet.test.sh && bash tests/test_reconstitute.sh && bash tests/test_reconstitute_fixes.sh && bash tests/test_agent_forensics.sh && bash hooks/pre-push-policy.sh --test && bash tools/reconstitute.sh --test",
|
|
58
66
|
"test:py": "python -m unittest discover -s tests",
|
|
59
67
|
"test:all": "npm run test:node && npm run test:sh && npm run test:py"
|
package/scan/CLAUDE.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# scan/ — Example IOC/secret scanner template
|
|
2
|
+
|
|
3
|
+
The `fleet-scan.example.mjs` is a config-driven template for scanning committed code and fleet transcripts for security and alignment red-flags during each watchdog cycle.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Copy `fleet-scan.example.mjs` → `fleet-scan.mjs` (in the same directory)
|
|
8
|
+
2. Edit the configuration section to match your fleet setup (REPOS array, PROJECT_ROOTS paths)
|
|
9
|
+
3. Configure paths via `aesop.config.json` or environment variables:
|
|
10
|
+
- `AESOP_FLEET_ROOT`: root directory containing your project repositories
|
|
11
|
+
- `AESOP_TRANSCRIPTS_ROOT`: root directory containing `~/.claude/projects` transcripts
|
|
12
|
+
4. Ensure `aesop.config.json` contains repo definitions (see `aesop.config.example.json`)
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
When enabled (by copying to `fleet-scan.mjs`), the scanner:
|
|
17
|
+
- Runs every watchdog cycle (default: every 150s)
|
|
18
|
+
- Scans git repositories and transcript directories for patterns matching your configured IOC/secret rules
|
|
19
|
+
- Logs findings to `SECURITY-ALERTS.log` with severity (HIGH/MED/LOW)
|
|
20
|
+
- Never blocks the fleet (non-fatal, append-only)
|
|
21
|
+
- Marks reviewed findings with `NOTE:` or `RESOLVED-FP` prefix to skip future alerts
|
|
22
|
+
|
|
23
|
+
## Customization
|
|
24
|
+
|
|
25
|
+
Edit the `fleet-scan.mjs` rules section to add domain-specific checks:
|
|
26
|
+
- Pattern-based secret/credential detection
|
|
27
|
+
- Alignment violations (e.g., non-approved libraries, unclosed TODO markers)
|
|
28
|
+
- Transcript red-flags (e.g., cost anomalies, stalled agents)
|
|
29
|
+
|
|
30
|
+
Output findings via the `add(severity, kind, where, detail)` function; the scanner handles deduplication and logging.
|