vault-sync: 2026-10-11 12:01 (jimmy)
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# Hermes headless approvals & command_allowlist
|
||||
|
||||
> Reusable failure mode. Diagnosed 2026-10-11 while running the killcorpus loop (RUDR9 profiles) headless.
|
||||
|
||||
## Symptom
|
||||
|
||||
A headless sibling profile — `hermes --profile <p> chat -q "<task>"` — has its `terminal` / `execute_code` calls refused with `[BLOCKED: Command flagged as dangerous …]`, while trivial probes pass. The agent half-runs and still exits 0. An agent whose fallback is `execute_code` gets the same block, so it loops: `BLOCKED: terminal → BLOCKED: execute_code`.
|
||||
|
||||
## What is actually doing the blocking
|
||||
|
||||
Not "manual mode". The relevant settings (defaults from `hermes_cli/config_defaults.py`) are:
|
||||
|
||||
```yaml
|
||||
approvals:
|
||||
mode: smart # default; global behaviour when a human is present
|
||||
timeout: 300
|
||||
cron_mode: deny # binary deny|approve, per context
|
||||
single_query_mode: deny # ← the one that blocks a `-q` / oneshot turn
|
||||
unattended_mode: deny
|
||||
```
|
||||
|
||||
A single-query (`-q`) session has nobody to answer a prompt, so it takes the deterministic `single_query_mode` path: **deny**. That is the gate — not `mode`.
|
||||
|
||||
## The allowlist (`command_allowlist`)
|
||||
|
||||
Top-level key in a profile's `config.yaml`; a list of strings. It is a **pre-gate** — a match runs even in a context whose mode is `deny`.
|
||||
|
||||
Matching (`tools/approval_floors.py::_command_matches_permanent_allowlist`):
|
||||
- Matches the **whole command string**: exact text **or** a shell glob (`fnmatchcase`), e.g. `podman *`.
|
||||
- A command containing a **shell operator** (`&&`, `;`, `|`, `>`, …) **never** matches.
|
||||
- It is **not** path-scoped and **not** argument-semantic. You cannot say "any write under `C:\...\project\`" or "sqlite touching only `results/*.db`".
|
||||
|
||||
```yaml
|
||||
command_allowlist:
|
||||
- "python *paper_trader_instrumented.py *"
|
||||
- "sqlite3 results/*.db *"
|
||||
- "w32tm /query /status"
|
||||
```
|
||||
|
||||
## Implications
|
||||
|
||||
- **Native file tools** (`read_file` / `write_file` / `patch`) are not shell commands and are **not gated** by this system at all — in-repo file work already runs unattended. Only *shell* file ops (`rm`/`mv`/`cp` via `terminal`) gate, and those cannot be path-scoped.
|
||||
- So an agent must invoke an allowlisted command with an **absolute path and no shell operators**, or it won't match and will still gate.
|
||||
- Changing a profile's `config.yaml` is picked up by a fresh `-q` process immediately; platform/gateway-delivered turns may need a gateway restart to re-read it.
|
||||
- Keep the genuinely dangerous verbs **out** of the allowlist: `git push` / remote writes, package installs, `w32tm /config` & `/resync`, anything that moves money.
|
||||
|
||||
## Lessons
|
||||
|
||||
- When a headless agent "does nothing and exits 0", read its transcript for `BLOCKED` lines before trusting any self-report.
|
||||
- Diagnose the gate by name (`single_query_mode`), then grant the *minimum* — a narrow allowlist — rather than flipping `single_query_mode: approve` for the whole profile.
|
||||
- Config edits are safe to make by hand but Hermes snapshots `config.yaml` on `config set`; keep your own copy before editing directly.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Windows clock sync (w32time) for latency-gated work
|
||||
|
||||
> Reusable failure mode. Diagnosed 2026-10-11 on Tony's desktop (physical ASUS box) while building a latency-gated paper harness whose spec voids any candidate whose NTP skew exceeds 250 ms.
|
||||
|
||||
## Symptom
|
||||
|
||||
Anything that gates on the local clock fails: an NTP gate rejects every candidate, timestamps look wrong, and the clock **drifts** instead of holding. On this box the offset went from ~−1.8 s, to ~+10 ms right after a sync, then slid to **−284 ms and kept going** (~4–5 ms/min).
|
||||
|
||||
## Diagnose
|
||||
|
||||
```bash
|
||||
sc.exe query w32time # STATE: 1 STOPPED == no time sync at all
|
||||
w32tm /query /status # Leap Indicator, Stratum, Last Successful Sync, Poll Interval
|
||||
```
|
||||
|
||||
Then measure the offset **independently** of w32time (a stdlib NTP query against `pool.ntp.org`, `time.google.com`, `time.cloudflare.com`). Multiple servers agreeing confirms it is a real local-clock error, not one bad source. HTTP `Date` headers are useless for sub-second work — they are **whole-second** granularity.
|
||||
|
||||
## Root causes seen (in order encountered)
|
||||
|
||||
1. **The service was stopped.** `w32time` not running ⇒ no sync at all.
|
||||
2. **Registry writes went to the wrong key path.** `MinPollInterval`, `MaxPollInterval`, `MaxAllowedPhaseOffset`, `LocalClockDispersion` live under
|
||||
`HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config` — **not** the service root (`…\Services\W32Time`). `reg add` reported success but nothing applied. `SpecialPollInterval`/`NtpServer` live under `…\W32Time\TimeProviders\NtpClient`.
|
||||
3. **Default poll is 1024 s**, so a bad RTC drifts far between syncs.
|
||||
4. **`MaxAllowedPhaseOffset = 1` (s) makes w32time *slew*, not *step*** — it never nulls a sub-second residual. The clock sat stable but stuck at ~−180 ms.
|
||||
|
||||
## Fix (all elevated)
|
||||
|
||||
```bat
|
||||
set CFG=HKLM\SYSTEM\CurrentControlSet\Services\W32Time\Config
|
||||
set NTP=HKLM\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient
|
||||
sc config w32time start= auto
|
||||
w32tm /config /manualpeerlist:"time.google.com,0x1 time.cloudflare.com,0x1 pool.ntp.org,0x1" /syncfromflags:manual /reliable:no /update
|
||||
reg add "%CFG%" /v MinPollInterval /t REG_DWORD /d 6 /f REM 64 s
|
||||
reg add "%CFG%" /v MaxPollInterval /t REG_DWORD /d 10 /f
|
||||
reg add "%CFG%" /v LocalClockDispersion /t REG_DWORD /d 3 /f
|
||||
reg add "%CFG%" /v MaxAllowedPhaseOffset /t REG_DWORD /d 0 /f REM force a STEP to null the residual
|
||||
reg add "%NTP%" /v SpecialPollInterval /t REG_DWORD /d 64 /f REM honoured only with the 0x1 flag
|
||||
net stop w32time & net start w32time
|
||||
w32tm /resync /force
|
||||
```
|
||||
|
||||
Result: **−284 ms drifting → ±2 ms stable** (held over 150 s across four NTP servers).
|
||||
|
||||
## Lessons
|
||||
|
||||
- A `reg add` that "completed successfully" can be a **no-op** if the key path is wrong. Verify with `w32tm /query /configuration`, never with the command's exit code.
|
||||
- `MaxAllowedPhaseOffset: 0` forces a step on every sync — right for nulling a residual; if constant stepping is unwanted, raise it to a small tolerance afterwards.
|
||||
- Post-step, `w32tm /query /status` can read `Leap Indicator: 3 (not synchronized)` / `Stratum 0` while the time is actually correct — trust an independent NTP measurement, not the status flag.
|
||||
- `Date` headers (1 s granularity) can never satisfy a sub-second freshness/skew gate; measure freshness from **local timestamps** instead.
|
||||
- Requires elevation (UAC). A physical box with a bad RTC has no host time-integration to fall back on, unlike a VM.
|
||||
Reference in New Issue
Block a user