Catch a credential when it syncs, not only when you share it (#162)

* refactor(secrets): lift the share-time credential rules into internal/secrets

The rules only ever ran on the rarest path a file takes. Moving them out of
internal/webapp is what lets internal/syncer run the same six rules on the
path every file takes, without inverting the dependency.

Pure move plus one addition: Label(), the six human strings that until now
lived only in the frontend's SECRET_LABELS — so 'bdrive share' stops printing
a bare rule id where the web dialog says 'an AWS access key'. Rule ids and the
rule/line JSON tags are unchanged: Browser.tsx keys off them, so they are a
wire contract.

* feat(sync): warn when a synced file looks like it holds a credential

The six share-time rules now run on the path every file takes. A file with an
AWS key in it used to ride a normal sync to the hub, to every teammate's disk
and into every future agent's context with no badge and no warning — while the
Share dialog one click later blocked that exact file.

Warn, never block: the op is journaled and pushed exactly as before. A hold arm
would mean a false positive silently parks someone's changes, and it would
break the cycle's degrade-to-offline posture.

- scan() reads the blob PutBlobFile just wrote (the bytes that were actually
  journaled), only on the branches that wrote one — an unchanged file is still
  never re-read.
- Findings persist per path in secrets-<mount>.json, merged rather than
  replaced: nearly every cycle scans zero files, and a whole-set rewrite would
  erase the warning seconds after it appeared. Fixing the file clears it.
- bdrive status grows a secrets block; the agent hook appends one advisory
  sentence. Rule ids and line numbers only, never the matched bytes.
- SaveSecrets failing logs and continues: advisory telemetry never gets a veto
  over convergence.

* docs: the credential check now runs on sync, not only on share

README, the CLI reference and project-files get the new bdrive status block
and the warn-never-block posture, with the three limits stated (checked when
it changes, first 1 MiB, writing device only). Diagrams: internal/secrets is a
package of its own in the overview, secretLog joins the sync engine, and the
share-gate class notes that it no longer owns the rules.

* test(sync): assert an unchanged file is never re-read for credentials

The check must ride the branch that already reads the file. Clearing the record
by hand and cycling proves it: a scan that re-read unchanged files would put the
finding back, and the daemon's 3-second tick would pay for it on every file.
This commit is contained in:
Snow Lee (Sungwon)
2026-08-18 15:08:44 -07:00
committed by GitHub
parent 398f30d64b
commit a3dfa73fef
24 changed files with 913 additions and 174 deletions
+28 -4
View File
@@ -24,7 +24,7 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server.
| `bdrive sync [folder]` | Run one sync cycle now. Refuses folders this device never `init`ed and folders paused by `bdrive stop`. `--note <text>` stamps session context onto changes; `--note-ttl` (default 30m) bounds it. `--prune` also removes from the hub what `.bdriveignore` now excludes (files stay on disk everywhere). `--hook <label>` is agent-hook plumbing: it also reports the files teammates changed since the agent's last turn |
| `bdrive hooks [install\|uninstall]` | Register turn-boundary sync hooks in each detected agent platform's user config — once per machine, covering every folder. Run automatically by `bdrive init`; idempotent; `--agent` overrides detection. `uninstall` removes only BearDrive's own hook entries |
| `bdrive read-log [folder]` | Hook plumbing: queue agent file reads for the hub's read heatmap. Registered by `bdrive hooks install` |
| `bdrive status [folder]` | Projects, daemon state, pending changes |
| `bdrive status [folder]` | Projects, daemon state, pending changes, and any synced files that looked like they held credentials when they last changed |
| `bdrive log [folder] [-p path] [-n N]` | Change history: account, device, time, file — newest first by the time shown, which is when the file was written (ops recorded before this was tracked, and deletes, show their sync time instead) |
| `bdrive restore <file> [version]` | Put an earlier version of a file back, as a new change. No version restores the previous one; `--list` shows the versions with their short hashes |
| `bdrive export [folder]` | Export the whole project — all devices' history and content — to a portable `.tar.gz` (`-o` names the file) |
@@ -199,9 +199,10 @@ version wins and the path comes back. Run `--prune` again once they have synced.
### `bdrive status` — and the two degraded access states
Alongside `pending`, `status` prints an `access:` line whenever the hub is
refusing this device. Neither is the same as being offline, and neither ever
touches your files:
Alongside `pending`, `status` prints a `secrets:` block naming any synced file
that looked like it held a credential when it last changed, and an `access:`
line whenever the hub is refusing this device. Neither access state is the same
as being offline, and neither ever touches your files:
```
pending: 3 local change(s) not yet pushed
@@ -224,6 +225,29 @@ identity was never bound to your account, which is fixed by updating `bdrive`
and running `bdrive login` on that machine, not in Project settings. Checking
your permissions there will show `write` and tell you nothing.
### The credential warning
The six rules `bdrive share` checks at mint time also run on **every file as it
syncs** — the path every file takes, rather than the rare one:
```
secrets: 1 file(s) looked like they contain credentials when they last changed
deploy.md:12 an AWS access key
```
It **only ever warns**. The change is journaled and pushed exactly as it would
be otherwise; the finding appears here and in an agent's context at the start
of its next turn. Nothing is held, blocked, or un-pushed — a false positive
costs one line of text, never a stalled file.
Fixing the file is the whole remedy: the next cycle reads it again and the line
goes away, with no command and no flag. Three limits worth knowing: a file is
checked **when it changes** (so this never says a file is clean), only its
first 1 MiB is read, and the check runs on the device that made the change — a
file synced from elsewhere is not flagged here until it next changes here.
Rule names and line numbers only; the matched text is never printed, logged, or
stored.
`bdrive sync` shows the same two as `remote: read-only (pull only)` /
`remote: no access — sync paused` with the reason on the line below, and the
daemon logs each once on transition rather than on every tick — including the
@@ -123,7 +123,9 @@ Nothing is keyed by folder path, which is why moves and renames are free.
├─ blobs/ content-addressed file content (sha256)
├─ journal/ one append-only op log per device
├─ state.json what's materialized
─ sync.json lamport clock + push cursor
─ sync.json lamport clock + push cursor
└─ secrets-<mount-id>.json files that looked like they held credentials when
they last changed (what `bdrive status` reports)
```
Also here for a running project: `daemon.pid` and `daemon.log`.