Skip to content

Indirect locators, so the record carries no personal names - #689

Merged
xmap merged 1 commit into
mainfrom
worktree-indirect-locators-slice18
Aug 19, 2026
Merged

Indirect locators, so the record carries no personal names#689
xmap merged 1 commit into
mainfrom
worktree-indirect-locators-slice18

Conversation

@xmap

@xmap xmap commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Why

2-BM's scan-file directories embed a real person's surname (2026-08-Haridy-1015116, DMagic's {yyyy-mm}-{PIlastname}-{GUP#} convention). CaptureScanIngestor (slice 17, merged, switch still off) writes that path directly onto DatasetRegistered.uri / DistributionRegistered.uri, immutable, INSERT-only events with no erasure path.

The disposition table already drops uri from the published record and has no other person-name field anywhere in this codebase's event schema. This would introduce the record's only personal name, automatically, for every run, the moment the switch goes on.

What changed

CaptureScanIngestor now mints an indirect cora-capture-path:// locator instead of a real path:

cora-capture-path://tomdet/local1/2BM/run-<uuid>/scan_005.h5
                     ^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^ ^^^^^^^^^^^
                     host   tier       token        filename

Host and tier stay visible (real, non-personal provenance, useful to a reader). The personal segment is replaced with a run-<uuid> token resolved through run_capture_path — the same erasable vault capture_path.py already keeps observed_path in, for the identical reason.

ingest_scan's handler resolves this back to the real path once, before the reader/checksum computer ever see it. The event still records the indirect form. The manual POST route and MCP tool are unaffected — resolution is a pass-through for every other scheme.

Naming caught before it locked

Gate review's naming pass (auto-triggered per this repo's convention on new names) found the working name cora-vault collided with this codebase's other, unrelated PII vault (actor_profile). Since a scheme string here is permanent once written to an event, this was fixed before commit: renamed to cora-capture-path, matching the already-established capture_path/CapturePathStore/run_capture_path family instead of inventing a second one.

Gate review

4-agent panel (architecture / test-coverage / cross-BC consistency / security specialist). One real P0:

  • Unreadable.reason / Unreachable.error_detail are sourced from bare os.stat/h5py error text that embeds whatever path the reader actually opened, and InvalidScanFileError carries that text verbatim into an HTTP 400. For a resolved indirect locator this would echo the real, personal-data path back to a caller who submitted a reference they had no prior right to — the exact disclosure this slice exists to prevent, via a different door. Fixed: the handler tracks whether resolution substituted a different string and redacts the detail text only in that case; a direct file:// caller (who supplied the path themselves) sees unchanged behavior.

Also fixed: two P1s narrowing CapturePathStore to a get-only CapturePathLookup Protocol (this codebase's port-shaped-by-consumer convention) and correcting a false "same pool" docstring claim for the in-memory deployment branch; two P1 test gaps (a "genuine independent check" test that never actually exercised drift between two reads, and an unreached branch in the locator parser); and one bounded, documented-not-code-fixed P1: a caller holding IngestScan but not read access to Run capture paths could craft a locator naming a different run's bytes. Fixing that structurally would couple this Data BC module to a specific cora.api-owned agent identity — a worse cross-layer dependency than the risk it closes — so it's recorded as a deployment-authz responsibility in the module's own docstring instead.

Scope corrections from the plan

Two originally-planned fix sites (launch_argv, the compute port) needed no change: nothing today wires a Dataset/Distribution URI into a launch spec automatically, and the existing allowlist would refuse it loudly if that ever changed. A third (record_attestation) is deliberately not fixed: SshPosixChecksumComputer already deferred building an SSH-capable ChecksumVerifier "per the rule-of-three" in slice 17, and attestation over an SSH-sourced Distribution was unverifiable before this slice regardless of locator scheme — building that verifier now would be exactly the mistake the deferral warns against.

Risk

Inert on merge, same as slice 17: capture_scan_ingestor_enabled still defaults off. 31,181 architecture tests, 13,847 unit tests, 1,261 integration tests, pyright/ruff/tach all clean.

🤖 Generated with Claude Code

The 2-BM pilot's scan-file directories embed a real person's surname
(2026-08-Haridy-1015116, DMagic's {yyyy-mm}-{PIlastname}-{GUP#}
convention). CaptureScanIngestor (slice 17, merged, switch still off)
writes that path directly onto DatasetRegistered.uri /
DistributionRegistered.uri, immutable INSERT-only events with no
erasure path. The disposition table already drops uri from the
published record and has no other person-name field anywhere, so
this would introduce the record's only personal name, and do it
automatically for every run once the switch goes on.

CaptureScanIngestor now mints an indirect cora-capture-path://
locator instead: host and tier stay visible (real, non-personal
provenance), the personal segment is replaced with a run-<uuid>
token resolved through run_capture_path (the same erasable vault
capture_path.py already keeps observed_path in). ingest_scan's
handler resolves this back to the real path once, before the reader
and checksum computer ever see it; the event still records the
indirect form. The manual POST route and MCP tool are unaffected,
since resolution is a pass-through for every other scheme.

Naming review caught a lock-time mistake before it shipped: the
working name cora-vault collided with this codebase's OTHER,
unrelated PII vault (actor_profile), which would have been permanent
once written to an event. Renamed to cora-capture-path, matching the
already-established capture_path/CapturePathStore/run_capture_path
family instead of inventing a second one.

Gate review (4-agent panel, security specialist declared for the
same reason as slice 17) found one real P0: Unreadable.reason and
Unreachable.error_detail are sourced from bare os.stat/h5py error
text that embeds whatever path the reader actually opened, and
InvalidScanFileError carries that text verbatim into an HTTP 400.
For a resolved indirect locator this would echo the real,
personal-data path back to a caller who submitted a reference they
had no prior right to -- the exact disclosure this slice exists to
prevent, via a different door. Fixed: the handler tracks whether
resolution substituted a different string and redacts the detail
text only in that case; a direct file:// caller, who supplied the
path themselves, sees unchanged behavior.

Also fixed: two P1s narrowing the CapturePathStore dependency to a
get-only CapturePathLookup Protocol per this codebase's
port-shaped-by-consumer convention, and correcting a false "same
pool" claim in a docstring for the in-memory deployment branch; two
P1 test gaps (a "genuine independent check" test that never actually
exercised drift, and an unreached branch in resolve's segment
parsing); and a bounded, documented-not-code-fixed P1 covering a
caller who holds IngestScan but not read access to Run capture
paths crafting a locator that names a different run's bytes.

Two of the plan's originally-listed fix sites (launch_argv, the
compute port) turned out to need no change: nothing today wires a
Dataset/Distribution URI into a launch spec automatically, and the
existing allowlist would refuse it loudly if that ever changed. A
third (record_attestation) is deliberately NOT fixed here, correcting
the plan: SshPosixChecksumComputer already deferred building an
SSH-capable ChecksumVerifier "per the rule-of-three" in slice 17, and
attestation over an SSH-sourced Distribution was unverifiable before
this slice regardless of locator scheme.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@xmap
xmap force-pushed the worktree-indirect-locators-slice18 branch from 1e0d739 to f605250 Compare August 19, 2026 22:11
@xmap xmap changed the title Slice 18: indirect locators, so the record carries no personal names Indirect locators, so the record carries no personal names Aug 19, 2026
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/api
  _capture_scan_ingestor.py
  apps/api/src/cora/data
  wire.py
  apps/api/src/cora/data/adapters
  capture_path_locator.py
  apps/api/src/cora/data/features/ingest_scan
  handler.py 236
Project Total  

This report was generated by python-coverage-comment-action

@xmap
xmap merged commit 4f3cea9 into main Aug 19, 2026
19 checks passed
@xmap
xmap deleted the worktree-indirect-locators-slice18 branch August 19, 2026 22:28
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