@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
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 +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- 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/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -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/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -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/agents.py +183 -0
- package/ui/api/__init__.py +84 -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/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
package/hooks/CLAUDE.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# hooks/ — Installable org-policy git pre-push enforcement
|
|
2
|
+
|
|
3
|
+
**Purpose**: Ship executable git hooks that gate pushes with organization security policies (branch protection, secret scanning).
|
|
4
|
+
|
|
5
|
+
## Hook: pre-push-policy.sh
|
|
6
|
+
|
|
7
|
+
Runs on `git push` via `.git/hooks/pre-push` symlink or copy.
|
|
8
|
+
|
|
9
|
+
**Checks & Exit Contract**:
|
|
10
|
+
1. `check_branch_policy()` — blocks direct pushes to main/master; exit 1 on violation
|
|
11
|
+
2. `check_secret_scan()` — runs `tools/secret_scan.py --staged`; exit 1 on failure
|
|
12
|
+
3. Both trigger `log_block()` to append audit record before exit
|
|
13
|
+
|
|
14
|
+
**Audit-Ledger Contract**:
|
|
15
|
+
- Path: `${AESOP_ROOT:-$HOME/aesop}/state/SECURITY-AUDIT.log` (append-only, git-ignored)
|
|
16
|
+
- Format: JSON-lines (one record per line)
|
|
17
|
+
- Schema: `{"ts":"2025-07-12T14:32:01Z","repo":"aesop","event":"push_blocked","reason":"secret_scan_failure","user":"alice"}`
|
|
18
|
+
- All string values must be json_escaped (backslash → `\\`, quote → `\"`)
|
|
19
|
+
|
|
20
|
+
**Self-Test Convention**:
|
|
21
|
+
- `bash hooks/pre-push-policy.sh --test` runs the self-test suite, including:
|
|
22
|
+
1. Branch policy blocks main/master
|
|
23
|
+
2. Branch policy allows feature/* branches
|
|
24
|
+
3. Audit log JSON format is valid
|
|
25
|
+
4. JSON escaping handles special chars (quotes, backslashes)
|
|
26
|
+
5. stdin handling (git pre-push pipe) doesn't crash hook
|
|
27
|
+
- Exit 0 = all pass; exit 1 = any fail
|
|
28
|
+
|
|
29
|
+
**Installation**:
|
|
30
|
+
- See `docs/HOOK-INSTALL.md` for symlink (Linux/macOS/Git Bash) and copy (Windows) methods
|
|
31
|
+
- Test with `bash hooks/pre-push-policy.sh --test` before org distribution
|
|
32
|
+
|
|
33
|
+
## Hook: hooks/claude/force-model-policy.mjs
|
|
34
|
+
|
|
35
|
+
Claude Code hook enforcing subagent Haiku dispatch (cost optimization).
|
|
36
|
+
|
|
37
|
+
**Trigger**: On skill invocation or task delegation from main orchestrator thread. Examines Claude API request and enforces model constraint.
|
|
38
|
+
|
|
39
|
+
**Policy**:
|
|
40
|
+
- **Main orchestrator** (Fable/Opus on primary): no override (uses native model)
|
|
41
|
+
- **Subagent dispatch** (fleet workers): **enforce Haiku** (claude-haiku-4-5-*). Exit 1 on non-Haiku model request.
|
|
42
|
+
- **Specialists** (typed dispatches): Pin model to Haiku in the dispatch call; hook validates + blocks if violated.
|
|
43
|
+
|
|
44
|
+
**Logging**:
|
|
45
|
+
- On policy violation: log to `state/SECURITY-ALERTS.log` with timestamp, model-name, worker-id, and reason.
|
|
46
|
+
- No alerts on compliant requests.
|
|
47
|
+
|
|
48
|
+
**Self-Test**:
|
|
49
|
+
- `node hooks/claude/force-model-policy.mjs --test` validates:
|
|
50
|
+
1. Haiku model allowed on subagents
|
|
51
|
+
2. Non-Haiku (e.g., Opus) blocked on subagents
|
|
52
|
+
3. Orchestrator not subject to policy
|
|
53
|
+
4. JSON logging format is valid
|
|
54
|
+
- Exit 0 = all pass; exit 1 = any fail
|
|
55
|
+
|
|
56
|
+
## Invariants
|
|
57
|
+
|
|
58
|
+
- POSIX sh compatible, CRLF-safe (no line continuations)
|
|
59
|
+
- Tolerate git pre-push stdin (ref list) + optional args without choking
|
|
60
|
+
- Fail-open only for missing optional tooling (secret_scan.py absent → allow); fail-closed for policy checks
|
|
61
|
+
- Use `AESOP_ROOT` env var or `$HOME/aesop` fallback; no hardcoded machine paths/usernames
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// force-model-policy.mjs — Claude Code PreToolUse hook enforcing the
|
|
3
|
+
// "subagents are always Haiku" cardinal rule as versioned, executable policy.
|
|
4
|
+
//
|
|
5
|
+
// Wire it in settings.json under hooks.PreToolUse with matcher "Agent|Task"
|
|
6
|
+
// (see docs/HOOK-INSTALL.md). For every subagent dispatch:
|
|
7
|
+
//
|
|
8
|
+
// - model absent or non-compliant -> rewritten to the policy model ("haiku",
|
|
9
|
+
// or aesop.config.json cardinal_rules.subagent_model when present)
|
|
10
|
+
// - prompt contains [[ALLOW-NON-HAIKU]] -> deliberate escape hatch: input is
|
|
11
|
+
// left untouched, but the bypass is announced via permissionDecisionReason
|
|
12
|
+
// (visible in the transcript) AND appended as a JSON-line audit record to
|
|
13
|
+
// ${AESOP_ROOT:-~/aesop}/state/MODEL-POLICY-ESCAPES.log so every use is
|
|
14
|
+
// reviewable — prompts are untrusted text and can smuggle the marker.
|
|
15
|
+
// - anything else (compliant, other tools, malformed input) -> no output
|
|
16
|
+
//
|
|
17
|
+
// Output contract source: Claude Code hooks reference ("PreToolUse Decision
|
|
18
|
+
// Control", https://docs.anthropic.com/en/docs/claude-code/hooks) — a hook may
|
|
19
|
+
// emit on stdout:
|
|
20
|
+
// {"hookSpecificOutput":{"hookEventName":"PreToolUse",
|
|
21
|
+
// "permissionDecision":"allow","updatedInput":{...}}}
|
|
22
|
+
// where updatedInput replaces tool_input for the dispatched call. Emitting
|
|
23
|
+
// nothing (exit 0) means "no opinion" and the call proceeds unchanged.
|
|
24
|
+
//
|
|
25
|
+
// Reliability rule: this hook NEVER crashes the harness. Any parse or IO
|
|
26
|
+
// failure results in no output and exit 0 (fail-open), and nothing from the
|
|
27
|
+
// payload is ever logged on failure paths. stdin is raced against a 2s timer
|
|
28
|
+
// so a never-closing pipe cannot hang Agent/Task dispatch (fail-open, no
|
|
29
|
+
// rewrite). The only deliberate exception to "log nothing" is the escape-hatch
|
|
30
|
+
// audit record above.
|
|
31
|
+
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import os from 'node:os';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
|
|
36
|
+
const ESCAPE_HATCH = '[[ALLOW-NON-HAIKU]]';
|
|
37
|
+
const DEFAULT_MODEL = 'haiku';
|
|
38
|
+
const GOVERNED_TOOLS = new Set(['Agent', 'Task']);
|
|
39
|
+
const STDIN_TIMEOUT_MS = 2000;
|
|
40
|
+
const ESCAPE_LOG_NAME = 'MODEL-POLICY-ESCAPES.log';
|
|
41
|
+
|
|
42
|
+
/** State root, consistent with the repo's resolution pattern
|
|
43
|
+
* (`${AESOP_ROOT:-$HOME/aesop}/state/` — see hooks/pre-push-policy.sh). */
|
|
44
|
+
function stateDir() {
|
|
45
|
+
const root = process.env.AESOP_ROOT || path.join(os.homedir(), 'aesop');
|
|
46
|
+
return path.join(root, 'state');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Append a JSON-line audit record for an escape-hatch use. Best-effort:
|
|
50
|
+
* an unwritable log never blocks the dispatch (fail-open). */
|
|
51
|
+
function logEscapeUse(payload) {
|
|
52
|
+
try {
|
|
53
|
+
const dir = stateDir();
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
const input = payload.tool_input || {};
|
|
56
|
+
const rec = {
|
|
57
|
+
ts: new Date().toISOString(),
|
|
58
|
+
event: 'model_policy_escape',
|
|
59
|
+
tool: payload.tool_name,
|
|
60
|
+
session_id: typeof payload.session_id === 'string' ? payload.session_id : null,
|
|
61
|
+
cwd: typeof payload.cwd === 'string' ? payload.cwd : null,
|
|
62
|
+
description: typeof input.description === 'string' ? input.description : null,
|
|
63
|
+
requested_model: typeof input.model === 'string' ? input.model : null,
|
|
64
|
+
prompt_head: typeof input.prompt === 'string' ? input.prompt.slice(0, 200) : null
|
|
65
|
+
};
|
|
66
|
+
fs.appendFileSync(path.join(dir, ESCAPE_LOG_NAME), JSON.stringify(rec) + '\n');
|
|
67
|
+
} catch {
|
|
68
|
+
// audit logging is best-effort; never block or crash the harness
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolve the policy model: aesop.config.json cardinal_rules.subagent_model,
|
|
73
|
+
* looked up in $AESOP_ROOT then cwd; falls back to "haiku". */
|
|
74
|
+
function policyModel() {
|
|
75
|
+
const roots = [process.env.AESOP_ROOT, process.cwd()].filter(Boolean);
|
|
76
|
+
for (const root of roots) {
|
|
77
|
+
try {
|
|
78
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(root, 'aesop.config.json'), 'utf8'));
|
|
79
|
+
const m = cfg && cfg.cardinal_rules && cfg.cardinal_rules.subagent_model;
|
|
80
|
+
if (typeof m === 'string' && m.trim()) return m.trim();
|
|
81
|
+
} catch {
|
|
82
|
+
// missing or unreadable config in this root — try the next one
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return DEFAULT_MODEL;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Read stdin raced against a timer: a pipe that never closes must not hang
|
|
89
|
+
* Agent/Task dispatch. On timeout, resolve '' (fail-open, no rewrite). */
|
|
90
|
+
function readStdin(timeoutMs = STDIN_TIMEOUT_MS) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
let settled = false;
|
|
93
|
+
const finish = (value) => {
|
|
94
|
+
if (settled) return;
|
|
95
|
+
settled = true;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
resolve(value);
|
|
98
|
+
};
|
|
99
|
+
const timer = setTimeout(() => finish(''), timeoutMs);
|
|
100
|
+
let data = '';
|
|
101
|
+
process.stdin.setEncoding('utf8');
|
|
102
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
103
|
+
process.stdin.on('end', () => finish(data));
|
|
104
|
+
process.stdin.on('error', () => finish(''));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function main() {
|
|
109
|
+
const raw = await readStdin();
|
|
110
|
+
|
|
111
|
+
let payload;
|
|
112
|
+
try {
|
|
113
|
+
payload = JSON.parse(raw);
|
|
114
|
+
} catch {
|
|
115
|
+
return; // malformed input: fail-open, no output, log nothing
|
|
116
|
+
}
|
|
117
|
+
if (!payload || typeof payload !== 'object') return;
|
|
118
|
+
if (!GOVERNED_TOOLS.has(payload.tool_name)) return;
|
|
119
|
+
|
|
120
|
+
const input = payload.tool_input;
|
|
121
|
+
if (!input || typeof input !== 'object') return;
|
|
122
|
+
|
|
123
|
+
const model = policyModel();
|
|
124
|
+
|
|
125
|
+
// Deliberate opt-out — but never silent: prompts are untrusted text, so the
|
|
126
|
+
// marker could be smuggled in from repo/file content. Every use is announced
|
|
127
|
+
// in the transcript (permissionDecisionReason) and appended to the audit log.
|
|
128
|
+
if (typeof input.prompt === 'string' && input.prompt.includes(ESCAPE_HATCH)) {
|
|
129
|
+
logEscapeUse(payload);
|
|
130
|
+
process.stdout.write(JSON.stringify({
|
|
131
|
+
hookSpecificOutput: {
|
|
132
|
+
hookEventName: 'PreToolUse',
|
|
133
|
+
permissionDecision: 'allow',
|
|
134
|
+
permissionDecisionReason:
|
|
135
|
+
`Model policy BYPASSED via ${ESCAPE_HATCH} escape hatch: this ` +
|
|
136
|
+
`${payload.tool_name} dispatch keeps model ` +
|
|
137
|
+
`"${typeof input.model === 'string' ? input.model : '(default)'}" ` +
|
|
138
|
+
`instead of policy model "${model}". Use recorded in ` +
|
|
139
|
+
`state/${ESCAPE_LOG_NAME}.`
|
|
140
|
+
}
|
|
141
|
+
}) + '\n');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (input.model === model) return; // already compliant — no opinion
|
|
146
|
+
|
|
147
|
+
process.stdout.write(JSON.stringify({
|
|
148
|
+
hookSpecificOutput: {
|
|
149
|
+
hookEventName: 'PreToolUse',
|
|
150
|
+
permissionDecision: 'allow',
|
|
151
|
+
permissionDecisionReason:
|
|
152
|
+
`Model policy: subagent dispatches run on "${model}" (cardinal rule; ` +
|
|
153
|
+
`override with ${ESCAPE_HATCH} in the prompt).`,
|
|
154
|
+
updatedInput: { ...input, model }
|
|
155
|
+
}
|
|
156
|
+
}) + '\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().catch(() => { /* never crash the harness */ }).finally(() => process.exit(0));
|