Enrollment privacy review: deadline hold + hypaware-privacy skill + hyp purge + query-seam filter (LLP 0100-0108)#316
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the picker (LLP 0101/0102)
Generalize the pick-pending export hold into a first-sync review window: on an
attended enrolling login the sink driver exports nothing until a printed,
absolute deadline (the next local 11:59pm, rolled to the following day when
under a 4-hour floor). The deadline lives INSIDE the marker so an hours-long
hold survives incidental touches; corrupt or past markers read as absent
(fail-open), and a past deadline is opportunistically unlinked.
- src/core/usage-policy/first_sync_hold.js: new module (replaces
pick_pending.js). computeFirstSyncDeadline, writeFirstSyncHoldMarker (writes
usage-policy/first-sync-hold.json), readFirstSyncDeadline.
- src/core/sinks/driver.js + types.d.ts: tick() holds whole ticks while
now < deadline; held reason renamed pick_pending -> first_sync_hold.
- src/core/cli/remote_commands.js: the fresh-enroll fork writes the hold BEFORE
enrollCentralSink and never clears it (runs to its deadline, no finally
clear); hyp join and re-logins hold nothing. Picker retired per LLP 0102:
ENROLLMENT_PICKER_ENABLED, refineLocalOnly, the captured-directory wait
(waitForCapturedDirectories) and freshenCaptureEnumeration removed; every
fork prints the durable-command hint.
- src/core/commands/local_only.js: runLocalOnlyPicker + its wiring deleted;
listCapturedDirectories, CAPTURE_DATASET and DURABLE_HINT survive as the
skill's survey substrate. Picker result types dropped from types.d.ts.
- llp/0101: add the {#hold} anchor the plan's driver-check @ref targets.
- Tests: first-sync-hold.test.js (deadline incl. 4h floor + DST-agnostic
invariant, marker fail-open, driver hold/release); remote-login and
local-only tests reworked to the retirement + hold-ordering behavior.
Task-Id: T5
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(LLP 0103 cli)
Extends the machine-local usage-policy list from a flat local-only dirs
array (v1) to a class-per-entry store (v2: {dir, class}), migrating v1
files on read, so `hyp ignore`/`hyp unignore` can mark a subtree with any
of the three usage classes without touching the committable .hypignore
dotfile:
- `hyp ignore --private` records `ignore` in the machine-local store
(never recorded, never a repo breadcrumb, unlike the dotfile).
- `hyp ignore --local-only` keeps the existing local-only behavior.
- `hyp ignore --sync` records an explicit `full` entry: the implicit
default made durable, so a later classification pass can tell "asked;
syncs" apart from "never asked".
- `hyp unignore` grows the same three flags, symmetric removal.
- `hyp ignore --check` now names the governing source (`dotfile` /
`machine-local` / `none`) alongside the class, and points at `hyp purge`
when residual cached rows remain.
Marking/unmarking stays non-destructive: it only ever edits the
machine-local list file, never touches cached rows (that split is
`hyp purge`'s job).
Includes the minimal LLP 0103 store/resolver foundation this task
depends on (v2 read/write, matcher class-per-entry resolution, most-
specific-wins among nested list entries) - the capture-seam adapter
wiring (the five createUsagePolicyResolver() call sites in the Claude/
Codex plugins) is out of scope for this task and left for its own task.
Fixes a resolver tie-break bug found by this work: an explicit `full`
list entry used to lose its governedBy to the implicit default on a
same-rank tie, which would have made --sync's idempotency check and
--check's source-naming silently misreport "not governed by anything".
@ref LLP 0103 [implements]: class-aware marking verbs, dotfile meaning untouched, non-destructive marking
Task-Id: T2
Implements the retroactive-deletion verb settled by LLP 0104: `hyp purge` removes already-cached rows from this machine's local query cache, cache-only (never contacts a sink or the remote). Four targets, exactly one required (bare `hyp purge` errors): - <path> rows whose cwd equals or descends from the path (subtree) - --session <id> one session's rows - --ignored every row whose cwd currently resolves to ignore - --all every recorded row Mechanics (resolved here per the decision-first convention, LLP 0104 deferred them to "the design doc that follows"): deletion is via Iceberg position-deletes (deleteMatchingRows, the same primitive the retention enforcer uses), NOT a rewrite of survivors. This preserves surviving rows' part_id identity and every sink's _hyp_ingest_seq watermark, so a purge-then-re-record is idempotent server-side (chunk dedupe absorbs the identical part_ids) and purged rows never resurrect via a stale watermark. Marking (hyp ignore) stays non-destructive. Confirmation prompt on a TTY, --yes for scripts. Resurrection warning driven off the cwds actually deleted: any that still resolves `full` will be re-imported by the next backfill (--ignored dirs are durable, so they never warn). Structured purge.* telemetry with row/partition counts and a hashed target token. - src/core/cache/iceberg/store.js: deleteMatchingRows position-delete primitive - src/core/cache/purge.js: purgeCache orchestration over discovered partitions - src/core/commands/purge.js + core_commands.js registration: the CLI verb - src/core/cache/types.d.ts: PurgeTarget / PurgeSummary - test/core/purge-command.test.js: 17 tests (each target shape, segment-aware subtree, part_id + watermark integrity, cursor recount, idempotency, and the full CLI: validation, confirm gate, resurrection warning, --json) - hypaware-core/smoke/flows/purge_removes_cached_rows.js: hermetic smoke proving purged rows vanish from `hyp query` results via the real CLI dispatch Task-Id: T3 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the transcript leak: a query running inside a captured session in a synced directory can no longer surface local-only rows into that session's transcript through hyp's own read surfaces. One shared filter at executeQuerySql (never per command, the LLP 0049 R4 rule) now consults the same two-source usage-policy resolver the export seam uses, twice: once for the caller's cwd and once per row. A row is included iff the caller's class >= the row's class on the restrictiveness lattice (ignore > local-only > full); an unknown caller (no derivable cwd) fails closed. Every surface funnels through it: hyp query sql, hyp graph neighbors, and the MCP tools (the stdio host's spawn directory is the caller's context). The wrapper refuses every scan fast path that could bypass the filter: cwd is forced into projected scans and stripped back off, scanColumn and numRows are withheld (a column stream cannot carry cwd; COUNT(*) must not count withheld rows), pushed-down LIMIT/OFFSET is not forwarded, and WHERE is kept from suppression-active datasets so a predicate over suppressed content cannot reveal a row's presence. Graph provenance (LLP 0105 #graph-provenance) resolves on the suppression side: node/edge rows aggregate across sessions and carry no per-row cwd, so the datasets declare their content-bearing columns (localOnlyContentColumns) and restricted callers get structure with natural_key/label/props/source_keys nulled, counted and reported. Propagating per-row provenance through the projection merge remains the higher-fidelity follow-up. Withholding is never silent: results carry a localOnly report, the verbs print the withheld/suppressed counts (only ever counts) on stderr, and usage_policy.query_withhold telemetry mirrors the export seam's discipline. --include-local-only on both query verbs is the informed-consent override; its help text names the transcript-capture consequence. Kernel-internal cache-to-cache reads (graph projection and its dedup, enrich pipeline, capture-directory enumeration, ignore --check residual counts) bypass the filter explicitly, each with its justification, since their output never enters a transcript and enumeration is the privacy review's survey substrate. Tests: caller-class lattice truth table, unknown-caller exclusion, projection/scanColumn/numRows/LIMIT/WHERE bypass closure, suppression semantics, override plumbing, resolver fail-loud. New hermetic smoke local_only_query_withhold drives the real CLI from synced, local-only, and overridden caller contexts; local_only_export_withhold updated to the refined "queryable from private contexts" semantics. The 11 smoke flows failing on this branch (blob_sink_parquet_local_fs, cli_bundled_plugins_activated, client_attach_on_join, config_load_validate, context_graph_projects_rows, gateway_claude_capture, local_parquet_export, remote_oidc_login, s3_sink_export_fixture, walkthrough_picker_to_first_query, walkthrough_to_first_query) fail identically on the integration base and are unrelated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Task-Id: T4
…4' into HEAD # Conflicts: # src/core/usage-policy/index.js # src/core/usage-policy/matcher.js
…LP 0100 R1/R9) The attended enrolling login fork now prints the first-sync deadline it already writes: an absolute local time, the backfilled-history statement, and the hypaware-privacy skill invocation hint, on stderr the same way whether stdin is a TTY or not. hyp status surfaces the same pending deadline (text and --json) while the hold is live, sharing one date formatter with the login message so the two consent surfaces never drift on wording. A corrupt or expired hold marker still reads as absent per LLP 0101's fail-open polarity. Task-Id: T6
…§skill) The agent-assisted privacy review the deferred first sync directs the user to run. One shared six-step body per client (LLP 0107 sharing convention): protect the review session via the gateway session opt-out and verify it (R3), check backfill settlement before surveying (LLP 0094 failure mode), survey captured cwds with the LLP 0069 enumerate query and sample content, explain ignore/local-only/sync in plain language before the first marking (R5), propose redacted excerpts with per-item confirmation (R4), and apply only via `hyp ignore --private/--local-only/--sync` plus an offered `hyp purge` for every ignored directory and flagged session (R6, R7). States the LLP 0105 scope honesty (hyp surfaces only). The Codex variant embeds its own session-id discovery from the newest `~/.codex/sessions/**` rollout `session_meta.id`, cross-checked against the working directory, since Codex exposes no CLAUDE_CODE_SESSION_ID and has no hypaware-ignore skill today. Registered for both clients in each plugin manifest and index.js skill loop (R8). Task-Id: T9
…LLP 0106) On an enrolled machine, an interactive session opened in a directory with no explicit governing usage class is asked once to classify it (sync / local-only / ignore), writing the answer through the same T2 marking verbs (`hyp ignore --sync/--local-only/--private`). Choosing sync writes the explicit `full` entry that suppresses the next prompt; every other case (unenrolled, already classified, non-interactive/headless) passes through silently and the hook never hangs or fails a run. - src/core/usage-policy/classification.js: shared, client-agnostic core - the decision (decideClassification), the pinned consent copy (buildClassificationPrompt), the class->verb mapping (verbArgvForClass), and the disk-reading evaluator (evaluateCwdClassification) that treats a governedBy != null resolve as "asked and answered". - claude: `hyp claude-hook classify-cwd` emits SessionStart additionalContext (Claude's context-injection "block with a prompt"); installed at attach on SessionStart + CwdChanged and recorded in the marker so `hyp detach` / `hyp leave` strip it via the LLP 0044 perimeter. - codex: `hyp codex-hook classify-cwd` degrades to a firm plain-text first-prompt nag (Codex has no context-injection hook); same decision, copy, and verbs. Tests pin the copy, the decision truth table, enrolled/unenrolled/classified/ non-interactive branches, the never-throw contract, and that the answer lands via the real `hyp ignore` verbs. npm test (2226 pass), typecheck, and build:types all green. Task-Id: T8 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round-1 review BLOCKER: the five capture-seam usage-policy resolvers
(claude projector/settle/backfill, codex exchange-projector/backfill)
were constructed with no `localOnlyListPath`, so they fell back to a
`.hypignore`-dotfile-only resolver blind to the machine-local list. A
directory marked via `hyp ignore --private` (the primary `ignore` path,
since marking verbs forbid repo dotfiles) therefore kept recording, and
`hyp backfill` re-imported it after a purge - defeating the class's
"do not record this folder's sessions at all" promise and the SKILL.md
durability claim.
Thread `localOnlyListPath(ctx.paths.stateDir)` from each plugin's
activate() into all five factories, matching the already-correct export
seam (activation.js), query seam (visibility.js), and purge (purge.js).
The drop decision stays class-aware: only `ignore` yields
USAGE_POLICY_DROP; `local-only` still records (queryable locally,
withheld later at the export seam per LLP 0070/0105); `full`/sync record
normally.
Tests:
- capture-seam-machine-local-list.test.js: constructs the real claude +
codex projectors with a written machine-local list (no injected
resolver) and asserts an `ignore` dir DROPS at capture while a
`local-only` dir still records.
- context-graph-content-column-allowlist.test.js (Finding 3): pins that
every node/edge schema column is classified content (CONTENT_COLUMNS /
localOnlyContentColumns) or structural, so an unclassified new
content column fails CI (LLP 0105 #graph-provenance).
- null-cwd-full-class.test.js (Finding 4): pins that a null-cwd
ai_gateway_messages row is visible (query seam, incl. the fail-closed
unknown caller) and forwarded (export seam) - full-class by design,
not a leak (LLP 0105 #unknown, LLP 0070 #enforce).
Finding 2: the SKILL.md durability copy ("once `ignore`d, the capture
seam blocks re-import, so the purge is durable") is now TRUE and
consistent with classification.js:57; left unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
neutral review - round 1 (head 51740f5)Verdict at review time: block. One blocker + one cascading major + two minor test-hardening items. All actionable findings were fixed on this branch (fixes landed at BLOCKER (fixed) - T1/LLP 0103: capture seam never honored machine-local
|
…ir (LLP 0103) The round-1 capture-seam wiring resolved the machine-local usage-policy list at localOnlyListPath(ctx.paths.stateDir), the per-plugin dir (<stateRoot>/plugins/<name>). The list lives at the shared state root (<stateRoot>/usage-policy/local-only.json), so readListEntriesSync found nothing, the resolver stayed blind, and `hyp ignore --private <dir>` never stopped recording at capture. Derive the list path the way purge.js, the export seam (activation.js), and the query seam (visibility.js) do: localOnlyListPath(readObservabilityEnv(ctx.env).stateDir). Add activate()-driven coverage that stages a per-plugin ctx.paths.stateDir while writing the list at the shared root, so it fails against the old wiring and passes after the fix. The prior tests passed the correct path straight to the factories, bypassing activate(), which masked the bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
neutral review - round 2 (head 3c7424a)Verdict at review time: block. Round 2 re-reviewed the round-1 capture-seam fix and found it was inert: the resolvers were wired to BLOCKER (fixed at 2b6a456) - stateDir derivation
MINOR (fixed at 2b6a456) - test could not catch the derivation bug
The head has advanced to |
neutral review - round 3 (head 2b6a456)Clean. Round 3 verified the round-2 stateDir fix at the current head. No actionable findings. Delta reviewed (
CI green at this head (typecheck + test SUCCESS). Mergeable, reviewed. Terminal: held as a ready PR for a human to merge - neutral never merges. Review lineage: round 1 found the capture-seam blocker (fixed at 3c7424a); round 2 found that fix inert - wrong stateDir (fixed at 2b6a456); round 3 confirms clean. maxReviewRounds=4. |
Implements the enrollment privacy review flow specified in LLP 0100 and
settled by decisions LLP 0101–0107 (merged in #308). Plan: LLP 0108.
The flow inverts the retired in-login picker (LLP 0102/0094):
hyp remote loginstays fast, the first sync to the org server is held to a printed absolute
deadline (LLP 0101), and the user reviews the settled cache afterwards in their
own agent session via the new
hypaware-privacyskill, applying choicesthrough
hypverbs before anything ships.Tasks (LLP 0108)
ignore|local-only|full, v1 migrate-on-read, resolver merge, capture-seam plumbing so machine-localignoreactually stops recordinghyp ignore --private / --local-only / --sync, symmetric unignore,--checknames the governing sourcehyp purgedestructive verb: by path subtree /--session/--ignoredsweep /--all; cache-only, never touches sinks or remoteexecuteQuerySqlfor query/graph/MCP; caller-class lattice; closes the transcript-leak pathpick_pending; next 11:59pm, +1d under 4h; written beforeenrollCentralSink); picker path deletedhyp statusshows the live holdhyp leavehypaware-privacyskill for Claude and Codex: the six-step review job, registered in both plugin manifestsEach task landed as a verified
--no-ffmerge; tests accompany every task(
purge-command,query-local-only-visibility,first-sync-hold,usage-policy-classification,ignore-private-sync-command,claude/codexclassify-hook,status-first-sync-hold, and others).Generated by neutral; held as a draft for human review and merge — neutral
never merges. Coverage of spec LLP 0100 closes via the code
@refs carried inthese commits (per the spec's
## @ref annotations code will carrysection).Change-Set: enrollment-privacy-review