Skip to content

Offline floor + local ledger + flush: floors apply when the gateway cannot see a call, and the history uploads on connect (0.16.0, #29 phase 1) - #31

Merged
davidcrowe merged 1 commit into
mainfrom
feat/offline-floor-ledger
Sep 8, 2026
Merged

Offline floor + local ledger + flush: floors apply when the gateway cannot see a call, and the history uploads on connect (0.16.0, #29 phase 1)#31
davidcrowe merged 1 commit into
mainfrom
feat/offline-floor-ledger

Conversation

@davidcrowe

Copy link
Copy Markdown
Collaborator

Refs #29 (phase 1, additive). Gateway ingest side: davidcrowe/gatewaystack-connect#1115. Floor rules ported from gatewaystack-connect#1109.

What changes

Three states where the gateway does not see a call: no key (the on-ramp), key present but the gateway unreachable or rejecting the key (an outage), and --local policy mode. Before this PR the no-key path allowed everything with a warning and never ran a floor, and the unreachable path ran no floor either. Now, in all three:

  1. The two floors apply. Hardline denies (unchanged rules). New ask-level destructive floor in decide.mjs: force push (not --force-with-lease), destructive SQL handed to a database client in any spelling (flag, positional, here-string, heredoc, piped literal), a remote download piped into a shell, recursive delete outside cwd / temp roots. Scans what will execute: heredoc bodies written to files and quoted prose cannot fire it. In --local mode the floor tightens a policy allow to ask; a policy deny still wins.
  2. A local ledger (~/.acp/ledger.jsonl) records the call: id, ts, tool, classification, decision, reason, mode, session, cwd, bounded input preview. Size-capped (5 MB, oldest half dropped). Never blocks the hook. Nothing leaves the machine without a key.
  3. Flush on the first governed call after a gap and at SessionStart with a key: a detached govern.mjs --flush child POSTs batches of 500 to /govern/ledger/flush, truncates on ack, writes a 5-minute backoff on failure, at most one spawn per minute. SessionStart prints one line saying how many buffered calls are uploading.

decide.mjs stays pure and import-free. The local policy engine is frozen, not removed (phase 2 is gated on flush data per #29).

Not changed

Verification

  • node --test test/: 180 pass (was 172). New: destructive-floor.test.mjs (unit, same fixtures as the gateway), offline-floor.test.mjs (real subprocess against a throwaway HOME: no-key, unreachable interactive + unattended, --flush against a local server, backoff, no-key sends nothing, SessionStart announce + detached flush).
  • One existing fixture changed: the "no token warns once" test used a catastrophic command as its warn-only case; that command is now correctly denied, so the fixture is a benign command.

Release

Version 0.15.0 → 0.16.0 (govern.mjs, plugin.json, marketplace). Attestation baseline is per version, so the hash change lands cleanly (#888 lesson). acp-install embeds decide.mjs verbatim; sync after merge (#4).

With no key, the hook allowed every call with a warning and never ran a
floor; unreachable did the same. Now every call the gateway cannot see
gets the two floors (hardline deny, new ask-level destructive floor:
force push, destructive SQL to a db client, pipe-to-shell, recursive
delete outside cwd) and a row in ~/.acp/ledger.jsonl. With a key, the
ledger flushes once, detached, so the console shows history from the
first call. Local policy engine frozen, not removed (phase 2 is gated).

Refs #29
@davidcrowe
davidcrowe force-pushed the feat/offline-floor-ledger branch from 018a255 to 95af920 Compare September 8, 2026 20:01
@davidcrowe
davidcrowe merged commit f47cc2a into main Sep 8, 2026
2 checks passed
@davidcrowe
davidcrowe deleted the feat/offline-floor-ledger branch September 8, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant