# Changelog -- 2026-07-24 -- caveman plugin: disable on live host + governance + guardrails

Session scope: acting on a 4-seat council review of the third-party `caveman`
Claude Code plugin (installed + off-by-default-guarded 2026-07-23, commit
338c919). Operator accepted the review recommendations (exact utterance: "I'll
accept your recommendations"). Harness-config + repo-governance only -- NO cloud
mutation, NO runbook change. CURRENT-STATE deliberately untouched (adding a SEC
row is not an L10 trigger; the G14 open-SEC count reconciles at Stage 4 close --
see item 2). Council finding that drove the disable: `enabled:true` runs
third-party `node` on every session start + every prompt REGARDLESS of
`CAVEMAN_DEFAULT_MODE=off` (the env guard gates arming/terse-output, not hook
execution), and the plugin's own honest docs put net token savings at
break-even-to-negative on this host's gated-approval workload -- so a standing
per-turn execution surface on a credential-bearing jumphost was not worth
carrying.

## Item 1 -- caveman DISABLED on the live host (enabled:false, both scopes)

- `~/.claude/settings.json` (user scope, authoritative for vcloud):
  `enabledPlugins."caveman@caveman"` flipped `true` -> `false`. Stops the
  SessionStart + UserPromptSubmit hooks from wiring at the next session start.
- `/home/jessea123/openstack-caracal-dc-dc/.claude/settings.json` (committed
  repo scope): added `enabledPlugins {"caveman@caveman": false}`. Project scope
  outranks user scope (Local > Project > User), so this is a fail-closed,
  travels-with-the-clone off-switch across the operator's workstations --
  mirrors the dual placement used for the env guard.
- `CAVEMAN_DEFAULT_MODE=off` retained in both files as a residual layer; the
  `extraKnownMarketplaces.caveman` entry RETAINED (plugin stays installed) so a
  deliberate re-enable for hardened-workflow deployment testing remains cheap.
- Verified: both settings.json parse (`jq`); user + repo
  `enabledPlugins."caveman@caveman"` = `false`; no `~/.claude/.caveman-active`
  flag (not armed). repo-lint 0 fail / 1 legacy warn (unchanged).
- **Revert:** set both `enabledPlugins."caveman@caveman"` back to `true`
  (repo copy via `git revert`/edit; user copy by hand). Note: re-enabling
  re-incurs the SEC-017 re-verify duty.

## Item 2 -- SEC-017 opened (supply-chain re-verify obligation)

- Added row **SEC-017** to `docs/security-ledger.md` (next-free by grep;
  highest prior = SEC-016). Records the standing third-party per-turn
  code-execution surface, its point-in-time benign verdict at SHA `0d95a81`,
  the not-declaratively-pinnable caveat, the disable disposition, and the
  STANDING DUTY to re-verify the hooks on any re-enable or plugin update.
- Register call = OPS (no D-number): fails GA-R3's Roosevelt-delta / A1 grep
  test (no bare-metal analog of a jumphost output-style plugin); precedent
  D-129 (OPS plugin-install stays OPS). The SEC row is the durable home the
  security-ledger mandate requires ("a row at discovery, never only a note") --
  the prior changelog (338c919) is GA-R2 non-citable/archive-bound, so the
  posture had no durable home before this.
- Count: open SEC rows 12 -> 13. This is NOT an L10 status-couple trigger
  (L10 fires on docs/audit/*, design-decisions Status lines, and session-ledger
  gate rows -- not security-ledger.md), so CURRENT-STATE is untouched; the G14
  count reconciles at Stage 4 close per its existing deferral pattern.
- **Self-measured drift baseline (sha256, 2026-07-24; executing cache copy ==
  git clone at `0d95a81`, only a `.in_use` runtime marker differing):**
  - `src/hooks/caveman-activate.js`        `bc0af3ba327657630e5f6c60be619e6b5394bf03edc29b1384a15f09a1b348bf`
  - `src/hooks/caveman-mode-tracker.js`    `3e34f004d4a9e65609f8c95b47dfe5f0e3a6f2d16b88c7f8af730187ee2fb246`
  - `src/hooks/caveman-config.js`          `f47fe2e6440578eeb20408bc3131b63fb8e1ae4f6ea26dbdcca33e7901e5ee8c`
  - `src/hooks/caveman-stats.js`           `433a345a279c2d54be51864ef77c0a7b19d0f8604b9a441ce0a1eded31f113e8`
  - `src/hooks/cavecrew-model-overrides.js` `9f2601e8551653609f0b9d700c9bd75bffa43747d2db18ffe83fb73de5dc3607`
  - `.claude-plugin/plugin.json`           `b76999bd244fa59c2b8805b1c2b573458e2d065b58e5904fc8e9e545e3dac7a6`
- **Revert:** delete the SEC-017 row from `docs/security-ledger.md`
  (`git revert <sha>` for the commit).

## Item 3 -- guardrails: repo-lint L11 + fail-closed compress prohibition

Council-reviewed guardrail set. (a) a repo-lint hash-baseline of the plugin
files was REJECTED (layering violation -- the plugin lives outside the repo
under `~/.claude`, non-portable across workstations, and duplicates the
plugin's own `checksums.sha256`; drift-on-update belongs in the SEC-017
re-verify duty, not repo-lint). Built (b) and (c):

- **(b) repo-lint L11 -- caveman-compress residue detector.** `scripts/repo_lint.py`
  gains L11: FAIL on any `*.original.md` in the tree (the backup signature of a
  lossy in-place `/caveman-compress` rewrite). Excludes `.git`, `.claude`
  worktrees, `.terraform`, `__pycache__` (reuses the `all_text()` skip set);
  opt-out marker `repo-lint: allow-original-md` per the L9 convention. Docstring
  catalog + `repo-lint.sh` header updated L1-L10 -> L1-L11. Harness
  `tests/repo-lint/run-tests.sh` gains T42 (residue FAILS), T43 (opt-out
  suppresses), T44 (worktree excluded): **45/45 PASS** (was 42). It is a
  POST-HOC detector (the original is already overwritten when the backup
  exists) -- it complements, not replaces, (c).
- **(c) fail-closed compress prohibition.** Mechanism measured read-only:
  `/caveman-compress <file>` is a SKILL that mutates by running
  `python3 -m scripts <file>` from the skill dir (in-place overwrite +
  `<name>.original.md` backup). Two layers added: (1) `.claude/settings.json`
  `deny` rules `Bash(*python3 -m scripts*)` + `Bash(*caveman-compress*)` --
  verified no legit repo usage collides (the strings appear only inside
  `repo_lint.py`'s L11 text, never in an executed argv); (2) a CLAUDE.md
  "Tooling guardrails" section prohibiting the compressor against ANY
  repo-tracked prose file, scoped to the whole record corpus (not a 4-file
  hand-list, which would rot). Note the pre-existing `Skill(*)` ask rule already
  gates the skill invocation itself; the deny is the fail-closed backstop that
  does not depend on that routing.
- Net enforcement stack: plugin DISABLED (item 1) is primary; `Skill(*)` ask +
  the compress `deny` gate invocation; L11 catches residue; CLAUDE.md is the
  human-readable rationale.
- Verification: `jq` valid; all touched files ASCII/LF; repo-lint 0 fail / 1
  legacy warn; `bash scripts/run-tests-all.sh` -> **GAUNTLET: ALL GREEN
  (77 harnesses)**.
- **Revert:** `git revert <sha>` -- removes L11 (+ its 3 harness cases), the two
  `deny` rules, and the CLAUDE.md section together.

---

(Session continued into the VR1 DC-mirror work -- distinct topic, same session, so
per GA-R2 it lands in this one changelog.)

## Item 4 -- DC1 debmirror PAUSED; bandwidth measured NOT the bottleneck

- Operator asked to pause one sync so a single mirror completes before tomorrow,
  on the premise the two syncs share the vcloud uplink. PAUSED dc1 (live):
  `systemctl stop dc1-mirror-sync.service dc1-mirror-sync.timer` on the dc1 rack;
  debmirror killed (0 procs), held at **329.9 GiB** (resumable; status left stale
  "RUNNING"). Timer stopped so it cannot auto-restart and re-contend.
- MEASURED result: pausing dc1 did NOT speed dc0 up -- solo dc0 averaged ~37 GiB/h
  vs ~35 GiB/h while contended. So the vcloud uplink is NOT the binding
  constraint (it is per-connection/per-file, dominated by universe's small-package
  tail). The pause neither helped nor hurt dc0; dc0 is on track (~70% at
  2026-07-24T15:35Z, ETA ~this evening UTC). This finding fed the DC1 decision
  (item 5): rather than idle dc1, switch it to a caching proxy.
- **Revert:** resume dc1's full sync -- on the dc1 rack:
  `systemctl daemon-reload && systemctl start dc1-mirror-sync.service dc1-mirror-sync.timer`
  (debmirror resumes from 329.9 GiB, skipping downloaded files).

## Item 5 -- DC1 interim apt caching proxy: D-135 amendment + dc-cache-proxy.sh

- **D-135 AMENDED 2026-07-24 (GA-R5, operator utterance quoted in
  design-decisions.md):** DC1 gets an INTERIM apt caching proxy to unblock its
  build now; DC0 stays the full-mirror deployment under test; DC1 proxy REPLACED
  by a full mirror afterward. Both artifact-delivery models exercised this
  rehearsal (Roosevelt input, D-132/D-133). The paused dc1 debmirror (~330 GiB)
  is KEPT dormant as the fallback, not deleted.
- **Shipped `scripts/dc-cache-proxy.sh`** (site-keyed check/install; apt-cacher-ng
  in proxy mode at the D-134 utility `.4:3142`, consumed via
  `juju model-config apt-http-proxy=http://<LISTEN>:3142`; caches archive +
  security + UCA with no per-repo remap). REUSES the `<site>-mirror-net.service`
  net layer (does not re-create it; fails loud if absent, pointing at dc-mirror.sh).
  Feasibility recon confirmed read-only first: apt-cacher-ng installable
  (`3.7.4-1ubuntu5.24.04.1`), all three upstreams 200 via the edge, port 3142 free.
- **Harness `tests/dc-cache-proxy/run-tests.sh`** (15 static cases: syntax, both
  sites' utility .4 + ruled edge gw, stable-identity keying, port pin, proxy-mode
  consumption, archive+UCA smoke coverage, net-layer reuse, MEASURED-tag
  discipline, arg/site validation) -- **15/15 PASS**.
- CURRENT-STATE Stage-4 narrative updated (DC0/DC1 artifact-path split). repo-lint
  0 fail / 1 legacy warn; `bash scripts/run-tests-all.sh` -> **GAUNTLET: ALL GREEN
  (78 harnesses)** (was 77; +tests/dc-cache-proxy).
- Live install on the DC1 rack (`apt-get install apt-cacher-ng` + `dc-cache-proxy.sh
  install dc1`) DONE + verified 2026-07-24 (gated): apt-cacher-ng
  `3.7.4-1ubuntu5.24.04.1` active on `.4:3142`, drop-in matches, net layer present,
  **check PASS 8/8 -- proxy serves archive (200) AND UCA caracal (200)** (UCA cached
  with no remap, as expected in proxy mode). NOTE: the DC1 rack reports a pending
  kernel reboot (running 6.8.0-134 vs installed -136) -- pre-existing, deferred to a
  maintenance window (a reboot would disrupt the paused mirror).
- **Revert:** `git revert <sha>` (removes the script, harness, D-135 amendment
  paragraph, and CURRENT-STATE split); if installed live, on the dc1 rack:
  `systemctl disable --now apt-cacher-ng; rm -f /etc/apt-cacher-ng/acng.conf.d/dc1-cache-proxy.conf`.

## Item 6 -- phase-4 runbook: per-DC MAAS service-credential step (Step 2.0)

- Operator-directed: the MAAS API key Juju needs is created by NO prior stage --
  a standing DEPLOYMENT-TIME task, so it belongs in the runbook. Added **Step 2.0**
  to `runbooks/dc-dc-phase4-juju-bundle-per-dc.md` (before the bootstrap MUTATION):
  create a DEDICATED per-DC MAAS admin service user (`sudo maas createadmin`) +
  issue its own API key (`sudo maas apikey --generate`) -- per-DC credential
  isolation (SEC-012/-016; Roosevelt: per-DC cloud creds), never a cross-DC reuse.
- **Creds location corrected (operator-directed):** the key stages in the JUMPHOST
  per-site creds folder (`~/vr1-<dc>-creds/maas-api-key.txt` on vcloud, SEC-009
  convention) -- NOT a new location on the region (voffice1) as the first
  walkthrough had it. Generated FROM the jumphost so it transits ssh stdout with
  no persistent copy on the region; never printed (SEC-006); consumed by Juju via
  path/stdin, never argv. Each issued key = a per-DC SEC rotation row.
- Revert: `git revert <sha>` (removes Step 2.0). No live/cloud state touched.

## Item 7 -- Per-DC MAAS service credentials created (SEC-018/-019) [CLOUD MUTATION, driven]

- Executed phase-4 Step 2.0 for both DCs (operator: "You drive it"), logged
  window `~/as-executed/2026-07-24-dc-maas-creds.log`. Passwordless sudo on
  voffice1 confirmed; `maas createadmin --password` supported (read-only precheck).
- Created dedicated MAAS superusers **`juju-vr1-dc0`** and **`juju-vr1-dc1`**
  (random passwords generated ON the region, UNSTORED -- Juju auths via the API
  key), and staged each user's API key into the vcloud per-site folder:
  `~/vr1-dc0-creds/maas-api-key.txt` and `~/vr1-dc1-creds/maas-api-key.txt` (0600).
  Keys generated FROM the jumphost (no persistent region copy), verified by
  FORMAT ONLY (both parts=3, 70 chars), never printed. Result fail=0.
- **SEC-018** (dc0 key) + **SEC-019** (dc1 key) opened -- per-DC rotation
  obligations (open SEC 13 -> 15; reconciled at Stage 4 close per the G14 pattern).
  as-executed index entry added (log itself stays uncommitted, secret-adjacent).
- Revert: on voffice1 `sudo maas apikey --username=juju-vr1-dc0 --delete=<key>`
  (+ dc1) and `sudo maas deleteuser juju-vr1-dc0` (+ dc1); `shred -u` the two
  vcloud key files; `git revert <sha>` for the SEC/index/changelog rows.

## Item 8 -- dc1 VIP overlay drafted (overlays/vr1-dc1-vips.yaml)

- Operator OK'd the overlay approach (vs editing bundle.yaml in place). Drafted
  `overlays/vr1-dc1-vips.yaml`: re-homes the 11 API-service VIPs from bundle.yaml's
  vr1-dc0 bands into vr1-dc1's MEASURED bands (lib-net vr1-dc1 PLANE_NAME):
  provider-public 10.12.4 -> 10.12.64, metal-admin 10.12.8 -> 10.12.68,
  metal-internal 10.12.12 -> 10.12.72; host octets .50-.60 unchanged. Base
  bundle.yaml stays the vr1-dc0 source of truth; overlay applied only for the dc1
  deploy (`juju deploy ./bundle.yaml --overlay ./overlays/vr1-dc1-vips.yaml`).
  YAML valid (11 apps), repo-lint 0 fail.
- Revert: `git revert <sha>` (removes the overlay). No live/cloud state.

## Item 9 -- Juju client setup on voffice1 (cloud + credentials) [client config, not a cloud mutation]

- **Juju client location = voffice1** (operator-ruled after reconsideration): the
  Office1 headend has a Roosevelt analog and D-128 runs MAAS-facing work there so
  runbooks read "from the headend"; vcloud (substrate host) has no Roosevelt
  analog, so driving the cloud from it would not transfer. Juju 3.6.25 (3/stable,
  matches VR0 + the bundle's Juju 3.6 target) installed on voffice1; removed from
  vcloud.
- `juju add-cloud --client vr1-maas` -- the ONE Office1 MAAS region
  (type=maas, endpoint `http://10.10.0.20:5240/MAAS`). `juju add-credential`
  both DCs: `vr1-dc0-cred` + `vr1-dc1-cred` from the staged keys, piped
  vcloud->voffice1 into a home-dir yaml (snap can't read /tmp), never printed,
  `shred -u`'d. Juju credential store on voffice1 now holds runtime copies (noted
  on SEC-018/-019; canonical files stay on vcloud). Credentials validate at
  bootstrap (juju does not pre-validate).
- Two snap gotchas measured + worked around: the juju snap has a private /tmp
  (stage bundle/overlay/cred files under $HOME); voffice1 ssh sessions have
  per-session /tmp. Both relevant to the deploy steps.
- Revert: `ssh voffice1 'juju remove-credential vr1-maas vr1-dc0-cred; juju
  remove-credential vr1-maas vr1-dc1-cred; juju remove-cloud --client vr1-maas'`;
  `git revert <sha>` for the SEC/changelog rows.
