Skip to content

fix(files): read a file nobody vouched for, as an untracked one already is - #6986

Merged
icecrasher321 merged 6 commits into
stagingfrom
provenance/workspace-file-parity-v2
Aug 22, 2026
Merged

fix(files): read a file nobody vouched for, as an untracked one already is#6986
icecrasher321 merged 6 commits into
stagingfrom
provenance/workspace-file-parity-v2

Conversation

@icecrasher321

@icecrasher321 icecrasher321 commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • A workspace file whose provenance was never recorded was refused by every model and runtime boundary at once: attachments, sandbox mounts, and the tool routes that parse, transcribe or describe a file. A file that was never tracked returned exact-empty two branches earlier and worked fine.
  • Both say the same thing about their contents — nothing. So the second was a permanent penalty for having tried to record provenance and failed, with no way back: nothing rewrites a file's provenance but another content write. 107 live files across 10 workspaces are sitting in it, 85 of them images, which is exactly what the vision/OCR/transcription routes consume.
  • This is the pathology the enforcement module already names for the other surfaces — "an aware writer that momentarily could not vouch strictly worse than an unaware one… a workspace could not recover without a data repair." Files were held out of that list pending their own decision; this is that decision.
  • workspace-file is now a real surface in DURABLE_SECRET_PROVENANCE_SURFACES, where the env documentation has always offered it — setting it previously logged "unrecognized" and silently did nothing. The four governed boundaries read an unrecorded file and record the same workspace-visible secret_provenance.unrecorded audit entry the other surfaces record.
  • Only absence is relaxed. The classifier collapsed a missing row, a content-version mismatch, a stale binding, and a malformed sidecar into the same unknown that a recorded absence produces. It now separates them, so the policy cannot reach the four that are not absences. Consumers that have not opted into the surface's policy (ffmpeg, knowledge ingest, the file-manage accumulator) keep refusing exactly as before.
  • Legacy is untouched: secret_provenance_version === null still returns exact-empty on the branch above, unchanged.
  • 0007 clears the files already in the state, which would otherwise report on every read forever.

Review fixes

Two real defects, both caught in review, both now fixed and pinned by tests that fail against the previous code.

An unrecorded contribution was laundered into an exact one. mergeWorkspaceFileSecretProvenance checked only for unknown, so unrecorded fell through to the exact branch — combining bytes nobody vouched for with bytes that were vouched for produced a positive claim that neither input made, and a derived file could hand a later boundary an exact-empty it had no basis for. Absence now survives the merge: unknown > unrecorded > exact.

0007 reversed the live writer's lock order. The content-update path updates workspace_files and then replaces the sidecar, one transaction, parent-first. The repair deleted the sidecar and then updated the parent — opposite order, so an overlapping upload deadlocks and Postgres aborts either the deployment or a tenant's file write. It now takes the parent lock first in id order, matching the writer and 0005 beside it, which is also what makes the status = 'unknown' re-check decisive rather than racy.

An initialization could send the database a value it rejects. The union carries three states; the sidecar's CHECK constraint accepts ('exact', 'unknown'). Only the replace path discriminated — initializeWorkspaceFileSecretProvenanceInTx forwarded the status verbatim, so an 'unrecorded' would have hit a constraint violation and aborted the enclosing transaction. Unreachable today (both callers pass exact-empty), but the signature permitted a value the column does not, which is a landmine rather than a bug. Found while auditing every consumer of the widened union.

One reported concern was checked and rejected: archive extraction flattens 'unrecorded' to 'unknown' before persisting, which sounded like it would strand extracted files in the unrelaxable state. It does not — replace writes 'unknown' for any non-exact input and that reads back as 'unrecorded', so both values persist identically and both round-trip to the relaxable state.

The doc comment claiming there was nothing here to deadlock against was false — written without checking, one PR after the same bug was caught on the table-row repair — and is gone. The ordering is now pinned by a test over both repairs, since reasoning about it has failed twice.

The premise this PR started from was wrong

Relaxing a stored unknown assumed it always meant "nobody recorded this". It did not. The sidecar collapsed two opposite claims into one stored value:

  • Absence — no registry ran, so nothing was written down. Says exactly what an untracked file says.
  • Taint — a writer knew secrets were present and refused: a child of an archive whose parent held secret entries (archive.ts:327), a generated asset whose decision.safe came back false, a transcode whose scanner had hasSecrets: true and could not locate them.

Relaxing the second would have walked known-secret-bearing content through every model boundary at once. Both reviewers caught this independently.

The decision type already separated them — a registry that never ran returns safe: true, every refusal returns safe: false — and only storage lost it. Migration 0302 widens the CHECK to accept a third status; writers persist unrecorded and unknown separately; readers relax the first and refuse the second. The attachment test asserts both directions on one call.

Only workspace files store this distinction, and that is deliberate. On every other durable surface the non-exact sidecar comes from a single condition — an incomplete incoming bundle or registry — which is always an absence, so two statuses say everything there is to say. Files are the only surface that derives one stored object from another, so they are the only one that can refuse on purpose. The shared policy is unchanged and uniform across all five: read an absence and audit it, refuse a taint. That invariant is now written down on DURABLE_SECRET_PROVENANCE_SURFACES so the asymmetry is not mistaken for drift in either direction.

Type of Change

  • Bug fix

Testing

Tested manually. bun run lint, all 24 CI audits, check:migrations, and type-check pass. 5,836 tests pass across the uploads, provenance, executor, knowledge, copilot and file-tool suites; packages/db 60.

Three pre-existing tests encoded the old fail-closed decision and were updated deliberately — the unrecorded attachment is now kept, an unrecorded mount now proceeds without importing, and the mixed egress test keeps refusing tainted, stale and cross-workspace keys with only the unrecorded row removed. Two others were failing wrongly mid-change and caught a real bug: the first cut of this applied the policy to a content-version mismatch and a missing row, which it must not.

Each of the two review fixes has a test verified to fail against the reverted fix before being kept — the merge one on precedence, the migration one on statement order across both repairs.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 22, 2026 9:41pm

Request Review

@cursor

cursor Bot commented Aug 22, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes secret-provenance classification and model/runtime file-read gates, including a CHECK-constraint migration. A mis-split between absence and taint could leak secret-bearing files or keep files unreadable.

Overview
Workspace files whose secret provenance was never recorded were refused at every model and runtime boundary, even though untracked files already read as exact-empty. Both say the same thing about their bytes — nothing — so the old fail-closed path permanently punished a writer that tried and failed.

Absence vs taint is now stored separately. Sidecar status 'unrecorded' is a recorded absence (no registry ran). Stored 'unknown' remains a deliberate refusal (archive child, unsafe generate/transcode). Migration 0302 widens the CHECK to allow 'unrecorded'. Readers relax only absence (and audit workspace-file); taint, stale bindings, and malformed sidecars stay refused. DURABLE_SECRET_PROVENANCE_ENFORCED_SURFACES now actually recognizes workspace-file.

Writes without a registry persist unrecorded. Copies keep absence as absence. Merge order is unknown > unrecorded > exact so unrecorded bytes cannot be laundered into exact. ffmpeg, knowledge ingest, and the file-manage accumulator still treat non-exact as unknown. Legacy secret_provenance_version === null is unchanged.

Reviewed by Cursor Bugbot for commit 01468e0. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR distinguishes unrecorded workspace-file provenance from deliberate taint and makes only the unrecorded state eligible for policy-controlled reads.

  • Adds workspace-file to durable provenance enforcement and auditing.
  • Extends the database status constraint and updates provenance storage, merging, copying, and boundary handling.
  • Repairs eligible legacy provenance rows while preserving stale, malformed, and mismatched rows.
  • Aligns repair lock ordering with live file writers and adds regression coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/uploads/contexts/workspace/workspace-file-secret-provenance.ts Adds distinct exact, unrecorded, and unknown semantics across merge, persistence, copying, classification, and governed read boundaries.
packages/db/script-migrations/0007_repair_unknown_workspace_file_provenance.ts Repairs eligible legacy unknown rows using current-binding guards and parent-first deterministic locking.
packages/db/migrations/0302_fat_sentry.sql Widens the workspace-file provenance status constraint to persist unrecorded separately from unknown.
apps/sim/lib/execution/durable-secret-provenance-enforcement.ts Registers workspace files as a configurable durable provenance surface with shared audit behavior.
apps/sim/lib/copilot/tools/server/media/ffmpeg.ts Keeps all non-exact input provenance fail-closed for media derivation and opaque error projection.
apps/sim/lib/knowledge/documents/service.ts Continues mapping every non-exact workspace-file provenance state to unknown at knowledge-ingestion boundaries.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  W[Workspace file write] --> C{Provenance decision}
  C -->|Exact| E[Store exact entries]
  C -->|No recorder| U[Store unrecorded]
  C -->|Deliberate refusal| T[Store unknown taint]
  E --> R[Read boundary]
  U --> P{Workspace-file policy enforced?}
  T --> D[Refuse read]
  P -->|No| A[Audit and allow]
  P -->|Yes| D
  R --> E2{Entries empty?}
  E2 -->|Yes| A2[Allow]
  E2 -->|No| I[Import provenance controls]
Loading

Reviews (6): Last reviewed commit: "fix(files): copy a recorded absence as a..." | Re-trigger Greptile

Comment thread packages/db/script-migrations/0007_repair_unknown_workspace_file_provenance.ts Outdated
…dy is

A workspace file whose secret provenance could not be recorded was refused by every model and
runtime boundary at once — attachments, mounts, and the tool routes that parse, transcribe or
describe a file. A file that was never tracked at all returned exact-empty and worked fine.
Both say exactly as much about their contents: nothing. So the refusal was a permanent penalty
for having tried to record provenance and failed, with no way back, since nothing rewrites a
file's provenance but another content write.

Workspace files now do what the other durable surfaces do — proceed and record an audit entry
naming the surface and the count, so the people who own the secrets are told. The state is
distinguished from the four that really are unknown (missing row, version mismatch, stale
binding, malformed), which stay refused; only a current sidecar that says so reads as
unrecorded.

Absence survives a merge rather than dropping through to exact, so a derived file cannot
launder an unrecorded input into a positive claim. Script migration 0007 clears the files
already stranded in the old state, taking the parent lock first in id order to match the live
writer, with the unknown status re-checked under it so a concurrent write is never undone.

Nothing changes for a legacy file: no sidecar row still means untracked, still reads
exact-empty. The enforcement flag stays in place.
@icecrasher321
icecrasher321 force-pushed the provenance/workspace-file-parity-v2 branch from e1a2f1f to dff6bc7 Compare August 22, 2026 20:43
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

Rebased onto staging now that #6982 has merged — the conflict was the stacked parent's commits appearing twice after its squash-merge. This is now a single commit on top of staging, MERGEABLE, and the diff no longer contains 0005/0006.

Both review findings were real and are fixed; replies on the threads. @greptileai review

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

The union carries three states; the sidecar's CHECK constraint accepts two. Only the replace
path discriminated — initialize forwarded the status verbatim, so an 'unrecorded' would have
reached the database as a value it rejects, aborting the enclosing transaction rather than
writing a bad row. Nothing passes one today, which is why it belongs here and not in a caller.

Also names the storage mismatch on the type itself: a stored 'unknown' reads back as
'unrecorded', not as the union's 'unknown'. Same word, two layers, different meanings.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

Fourth round — one more real defect, found auditing every consumer of the widened union rather than reported.

initializeWorkspaceFileSecretProvenanceInTx forwarded status: provenance.status straight into a column whose CHECK accepts only ('exact', 'unknown'). An 'unrecorded' would have been a constraint violation aborting the enclosing transaction, not a bad row. Unreachable today — both callers pass exact-empty — but the signature permitted a value the database rejects, so it's narrowed to match the replace path, with a test verified to fail against the raw forward.

Also confirmed the other direction: no consumer treats "not unknown" as safe. Every downstream branch is === 'exact' / !== 'exact', so 'unrecorded' fails closed at all five merge call sites. Two ffmpeg branches were widened from === 'unknown' to !== 'exact' in this PR — without that, an unrecorded input would have leaked a projected error message.

@greptileai review

Comment thread packages/db/script-migrations/0007_repair_unknown_workspace_file_provenance.ts Outdated
Comment thread packages/db/script-migrations/0007_repair_unknown_workspace_file_provenance.ts Outdated
The repair selected on status alone, which is wider than the branch it was meant to undo. The
reader answers 'unrecorded' only for a sidecar that is version 1, bound to the file's current
bytes, and holding a well-formed entries array; anything else is a fault it refuses and no
policy relaxes.

Clearing a fault was not a smaller claim but a larger one. Deleting the sidecar sets
secret_provenance_version to NULL, and a NULL version reads as exact-empty — so a file refused
because its provenance described bytes it no longer holds would have come back positively
vouched for, silently, with no audit entry. Four such rows exist in production today.

Both the candidate query and the delete now carry every condition, so a concurrent write
cannot lose the distinction between an absence and a fault. Stale, unversioned and malformed
sidecars stay refused exactly as the surface refuses them, and recover as they always have, on
the next content write.
Relaxing a stored `unknown` assumed it always meant "nobody recorded this". It did not. The
sidecar collapsed two opposite claims into one value: bytes nobody recorded, and bytes a writer
refused on purpose — a child of an archive whose parent provably held secrets, a generated
asset whose safety decision came back false, a transcode whose scanner knew secrets were
present and could not locate them. Relaxing the second would have walked known-secret-bearing
content through every model boundary at once.

The decision type already told them apart: a registry that never ran is `safe: true`, while
every refusal is `safe: false`. Only storage lost it. Writers now persist `unrecorded` and
`unknown` separately, readers relax the first and refuse the second, and the constraint is
widened to accept it.

Only workspace files store this distinction, deliberately. Every other durable surface produces
a non-exact sidecar from one condition — an incomplete incoming bundle or registry — which is
always an absence. Files are the only surface that derives one stored object from another, so
they are the only one that can refuse on purpose. The shared policy is unchanged and uniform:
read an absence and audit it, refuse a taint.

Also aligns the metadata batch classifier, which answered `unknown` where the single-file reader
answers `unrecorded`, leaving two classifiers describing one policy differently.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

Fifth round. The high-severity finding was right and it invalidated this PR's premise, not just an edge case — details on the thread and in the body.

Short version: the sidecar collapsed "nobody recorded this" and "a writer refused this on purpose" into one stored value, so relaxing the former relaxed the latter. Storage now keeps them apart (0302 widens the CHECK), writers persist the distinction the decision type already made, and readers relax only the absence.

Also confirmed with production data that the stale-binding finding was reachable: 4 rows sit unknown with a stale binding against 105 with a current one, and clearing those 4 would have promoted refused files to exact-empty.

Cross-surface check, since this adds a status to one of five sidecars: memory, document, embedding and table_row each produce a non-exact sidecar from exactly one condition — an incomplete bundle or registry — which is always an absence. Files are the only surface deriving one stored object from another, so the only one able to refuse on purpose. The invariant is documented on the shared surface list so the asymmetry reads as intent.

@greptileai review

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

The test that pins "closing the surface refuses an unrecorded file again" used a stored
unknown, which the split had just turned into a taint. A taint is refused whatever the flag
says, so the assertion held with enforcement off too and proved nothing about the switch this
posture rests on. It now uses a recorded absence, and fails if the flag is ignored.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

The copy path required an exact source, so a stored unrecorded one fell in with the refusals
and the target was written as a taint the source never carried. A workspace fork or a chat file
copy therefore turned a readable file into a permanently refused one — the pathology this
surface exists to undo, reached by copying — and nothing rewrites a file's provenance but
another content write.

Safe past the scope checks it now precedes: those exist to stop one workspace's secret entries
landing in another's file, and a recorded absence has no entries to carry.

The legacy Function export marker keeps writing unknown. Its record had resolved secret names
in scope and an unreadable bundle, so whether that is an absence is not something this change
can establish, and guessing in the readable direction is what caused this round.
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 01468e0. Configure here.

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