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

* fix(files): read a file nobody vouched for, as an untracked one already 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.

* fix(files): narrow an initialization to a status the column accepts

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.

* fix(provenance): repair only what the reader calls an absence

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.

* fix(provenance): store an absence apart from a refusal

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.

* test(provenance): assert the enforcement switch on a row it can act on

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.

* fix(files): copy a recorded absence as an absence

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.
This commit is contained in:
Vikhyath Mondreti
2026-08-22 15:05:39 -07:00
committed by GitHub
parent fb8f0d66d1
commit 7ea8692e7d
16 changed files with 20908 additions and 66 deletions
+5 -1
View File
@@ -302,7 +302,11 @@ async function getFileContentProvenance(
continue
}
const provenance = await getBoundWorkspaceFileSecretProvenance(workspaceId, source.identity)
if (provenance.status === 'unknown') {
/**
* `unrecorded` is a more specific `unknown`, and this accumulator has not opted into the
* workspace file surface's policy, so it latches exactly as it did before.
*/
if (provenance.status !== 'exact') {
accumulator.markIncomplete('workspace-file-provenance-unknown')
continue
}
@@ -578,7 +578,12 @@ describe('maybeWriteOutputToFile', () => {
)
})
it('preserves legacy writes without a registry and marks their provenance unknown', async () => {
/**
* No registry means no recorder ran, so nothing was written down about these bytes — an absence,
* which the surface's policy may relax, and distinct from the taint a refused safety decision
* produces below.
*/
it('preserves legacy writes without a registry and marks their provenance unrecorded', async () => {
const result = await maybeWriteOutputToFile(
RunFunction.id,
{ outputs: { files: [{ path: 'files/report.json', mode: 'overwrite' }] } },
@@ -589,7 +594,7 @@ describe('maybeWriteOutputToFile', () => {
expect(result.success).toBe(true)
expect(mockWriteWorkspaceFileByPath).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ secretProvenance: { status: 'unknown' } })
expect.objectContaining({ secretProvenance: { status: 'unrecorded' } })
)
})
@@ -114,7 +114,7 @@ export const ffmpegServerTool: BaseServerTool<FfmpegArgs, FfmpegResult> = {
context: fileRecord.storageContext ?? 'workspace',
})
inputRequiresOpaqueError ||=
fileProvenance.status === 'unknown' || fileProvenance.entries.length > 0
fileProvenance.status !== 'exact' || fileProvenance.entries.length > 0
const { content: buffer } = await executeCopilotFileUseCase(
context,
readWorkspaceFileContent,
@@ -139,7 +139,7 @@ export const ffmpegServerTool: BaseServerTool<FfmpegArgs, FfmpegResult> = {
const inputProvenance = mergeWorkspaceFileSecretProvenance(...inputProvenances)
inputRequiresOpaqueError ||=
inputProvenance.status === 'unknown' || inputProvenance.entries.length > 0
inputProvenance.status !== 'exact' || inputProvenance.entries.length > 0
assertServerToolNotAborted(context)
const result = await runFfmpegOperation(params.operation, mediaFiles, {
text: params.text,
@@ -67,15 +67,28 @@ describe('durable secret provenance enforcement', () => {
})
it('reports an unrecognized surface rather than silently enforcing nothing', () => {
configure('memory,workspace-file')
configure('memory,not-a-surface')
expect(isDurableSecretProvenanceEnforced('memory')).toBe(true)
expect(mockLogger.error).toHaveBeenCalledWith(
'Ignoring unrecognized durable secret provenance surfaces',
expect.objectContaining({ unrecognized: ['workspace-file'] })
expect.objectContaining({ unrecognized: ['not-a-surface'] })
)
})
/**
* `workspace-file` was this test's example of an unrecognized name while the env documentation
* already offered it — so anyone who set it got a silent no-op and the fail-closed posture they
* were trying to change. It is a real surface now.
*/
it('recognizes every surface its own configuration documents', () => {
configure('workspace-file')
expect(isDurableSecretProvenanceEnforced('workspace-file')).toBe(true)
expect(isDurableSecretProvenanceEnforced('table-row')).toBe(false)
expect(mockLogger.error).not.toHaveBeenCalled()
})
it('reports at error with the surface, cause, and affected count so it survives every LOG_LEVEL default', () => {
reportUnrecordedDurableProvenance({
surface: 'table-row',
@@ -9,8 +9,26 @@ const logger = createLogger('DurableSecretProvenanceEnforcement')
*
* Named by the call site rather than derived, so the surface survives refactors and stays
* greppable — the same convention the projection-refusal `site` strings use.
*
* One policy governs all of them: an **absence** — nobody recorded what these bytes carry — may be
* read, with an audit entry naming the surface, because a value nobody recorded says exactly what
* an untracked one does. A **taint** — a writer that knew secrets were present and could not map
* them to this output — is refused, and no policy relaxes it.
*
* Only `workspace-file` stores the difference, and that is deliberate rather than drift. On the
* other surfaces every non-exact sidecar comes from one condition, an incomplete incoming bundle
* or registry, which is always an absence; two stored statuses say everything there is to say.
* Files are the only surface that derives one stored object from another — an archive extracted
* into children, a generated asset, a transcoded output — so they are the only one that can refuse
* on purpose, and the only one with two claims to keep apart. Adding a third status elsewhere would
* encode a distinction that surface cannot make; removing it here would collapse one that matters.
*/
export const DURABLE_SECRET_PROVENANCE_SURFACES = ['memory', 'table-row', 'knowledge'] as const
export const DURABLE_SECRET_PROVENANCE_SURFACES = [
'memory',
'table-row',
'knowledge',
'workspace-file',
] as const
export type DurableSecretProvenanceSurface = (typeof DURABLE_SECRET_PROVENANCE_SURFACES)[number]
@@ -62,9 +80,13 @@ let enforcedSurfaces: ReadonlySet<DurableSecretProvenanceSurface> | undefined
* Set `DURABLE_SECRET_PROVENANCE_ENFORCED_SURFACES` to `all`, or to a comma-separated subset of
* {@link DURABLE_SECRET_PROVENANCE_SURFACES}, to close a surface.
*
* Workspace files are deliberately not a surface here: their unknown check sits in the callers
* rather than in the shared import, and one unknown file locks vfs reads, chat attachments, sandbox
* mounts, and the file tool routes at once. They stay fail-closed until that is its own decision.
* Workspace files were held out of this list while their fail-closed posture was its own decision.
* That decision arrived as broken state: because a file that was never tracked already reads as
* exact-empty, refusing only the file whose provenance could not be recorded made a writer that
* momentarily could not vouch permanently worse off than one that never tried — with no way back,
* since nothing rewrites a file's provenance but another content write. Live files across several
* workspaces sat unreadable behind it, by every tool route at once, carrying exactly as much
* information about their contents as the untracked files beside them: none.
*/
export function isDurableSecretProvenanceEnforced(
surface: DurableSecretProvenanceSurface
+5 -1
View File
@@ -558,7 +558,11 @@ function durableSecretProvenanceFromWorkspaceFile(
provenance: WorkspaceFileSecretProvenance,
binding: FileMetadataRecord
): DurableSecretProvenance {
if (provenance.status === 'unknown') return provenance
/**
* `unrecorded` is a more specific `unknown`, and this boundary has not opted into the workspace
* file surface's policy, so it keeps refusing exactly as it did.
*/
if (provenance.status !== 'exact') return { status: 'unknown' }
return {
status: 'exact',
entries: provenance.entries.map((entry) => ({
@@ -4,6 +4,18 @@
import { workspaceFileSecretProvenance, workspaceFiles } from '@sim/db/schema'
import { dbChainMock, dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockIsEnforced, mockReport } = vi.hoisted(() => ({
mockIsEnforced: vi.fn(() => false),
mockReport: vi.fn(),
}))
vi.mock('@/lib/execution/durable-secret-provenance-enforcement', () => ({
DURABLE_SECRET_PROVENANCE_SURFACES: ['memory', 'table-row', 'knowledge', 'workspace-file'],
isDurableSecretProvenanceEnforced: mockIsEnforced,
reportUnrecordedDurableProvenance: mockReport,
}))
import type { DbTransaction } from '@/lib/db/types'
import {
areModelSafeWorkspaceFileKeys,
@@ -26,6 +38,7 @@ describe('workspace file secret provenance', () => {
beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
mockIsEnforced.mockReturnValue(false)
dbChainMockFns.returning.mockResolvedValue([{ id: 'tracked-file' }])
})
@@ -139,6 +152,50 @@ describe('workspace file secret provenance', () => {
expect(dbChainMockFns.set).toHaveBeenCalledWith({ secretProvenanceVersion: 1 })
})
/**
* Absence and taint are different claims and must round-trip separately. Collapsing them is what
* made a file a writer deliberately refused indistinguishable from one nobody recorded, and so
* eligible for the same relaxation.
*/
it('persists an unrecorded initialization as its own status', async () => {
await initializeWorkspaceFileSecretProvenanceInTx(
dbChainMock.db as unknown as DbTransaction,
'file-1',
CONTENT_UPDATED_AT,
{ status: 'unrecorded' }
)
expect(dbChainMockFns.values).toHaveBeenCalledWith(
expect.objectContaining({ fileId: 'file-1', status: 'unrecorded', entries: [] })
)
})
it('persists a refused write as unknown, not as an absence', async () => {
await replaceWorkspaceFileSecretProvenanceInTx(
dbChainMock.db as unknown as DbTransaction,
'file-1',
CONTENT_UPDATED_AT,
{ status: 'unknown' }
)
expect(dbChainMockFns.values).toHaveBeenCalledWith(
expect.objectContaining({ fileId: 'file-1', status: 'unknown', entries: [] })
)
})
it('persists an unrecorded replacement as its own status', async () => {
await replaceWorkspaceFileSecretProvenanceInTx(
dbChainMock.db as unknown as DbTransaction,
'file-1',
CONTENT_UPDATED_AT,
{ status: 'unrecorded' }
)
expect(dbChainMockFns.values).toHaveBeenCalledWith(
expect.objectContaining({ fileId: 'file-1', status: 'unrecorded', entries: [] })
)
})
it('rejects a marker write that cannot bind the exact tracked content version', async () => {
dbChainMockFns.returning.mockResolvedValueOnce([])
@@ -274,6 +331,17 @@ describe('workspace file secret provenance', () => {
status: 'unknown',
entries: [],
},
{
id: 'unrecorded-id',
key: 'unrecorded-key',
workspaceId: 'workspace-1',
context: 'workspace',
fileContentUpdatedAt: CONTENT_UPDATED_AT,
secretProvenanceVersion: 1,
provenanceContentUpdatedAt: CONTENT_UPDATED_AT,
status: 'unrecorded',
entries: [],
},
{
id: 'other-workspace-id',
key: 'other-workspace-key',
@@ -317,6 +385,7 @@ describe('workspace file secret provenance', () => {
{ id: 'wrong-id', key: 'safe-key' },
{ id: 'safe-id', key: 'tainted-key' },
{ id: 'unknown-id', key: 'unknown-key' },
{ id: 'unrecorded-id', key: 'unrecorded-key' },
{ id: 'other-workspace-id', key: 'other-workspace-key' },
{ id: 'pre-marker-sidecar-id', key: 'pre-marker-sidecar-key' },
{ id: 'synthetic-execution-id', key: 'untracked-context-key' },
@@ -331,6 +400,12 @@ describe('workspace file secret provenance', () => {
{ key: 'safe-key' },
{ id: 'file-1700000000000', key: 'safe-key' },
{ id: 'wrong-id', key: 'safe-key' },
/**
* Unrecorded, so kept — the untracked keys below say the same thing and always were. The
* stored `unknown` above is dropped: a writer refused those bytes on purpose, which is a
* different claim from nobody having recorded them, and no policy relaxes it.
*/
{ id: 'unrecorded-id', key: 'unrecorded-key' },
{ id: 'pre-marker-sidecar-id', key: 'pre-marker-sidecar-key' },
{ id: 'synthetic-execution-id', key: 'untracked-context-key' },
{ id: 'legacy-id', key: 'legacy-key' },
@@ -673,7 +748,11 @@ describe('workspace file secret provenance', () => {
)
})
it('rejects unavailable mounted-file provenance without importing it', async () => {
/**
* Unrecorded says exactly what an untracked file says, and that one has always mounted. There is
* nothing to import either way, so the mount proceeds and the workspace is told.
*/
it('mounts an unrecorded file without importing provenance for it', async () => {
const registry = {
importProvenance: vi.fn(),
isPermanentlyIncomplete: vi.fn().mockReturnValue(false),
@@ -683,7 +762,7 @@ describe('workspace file secret provenance', () => {
fileContentUpdatedAt: CONTENT_UPDATED_AT,
secretProvenanceVersion: 1,
provenanceContentUpdatedAt: CONTENT_UPDATED_AT,
status: 'unknown',
status: 'unrecorded',
entries: [],
},
])
@@ -694,8 +773,9 @@ describe('workspace file secret provenance', () => {
identity: { fileId: 'file-1', key: 'file-key', context: 'workspace' },
registry,
})
).resolves.toBe(false)
).resolves.toBe(true)
expect(registry.importProvenance).not.toHaveBeenCalled()
expect(mockReport).toHaveBeenCalledWith(expect.objectContaining({ surface: 'workspace-file' }))
})
it('rejects tracked mounted-file provenance when no complete runtime registry can import it', async () => {
@@ -730,7 +810,7 @@ describe('workspace file secret provenance', () => {
).resolves.toBe(false)
})
it('rejects a tainted, unknown, stale, or cross-workspace model egress key', async () => {
it('rejects a tainted, stale, or cross-workspace model egress key', async () => {
const rejectedRows = [
{
id: 'tainted-id',
@@ -743,17 +823,6 @@ describe('workspace file secret provenance', () => {
status: 'exact',
entries: [{ name: 'API_KEY', encryptedValue: 'encrypted' }],
},
{
id: 'unknown-id',
key: 'unknown-key',
workspaceId: 'workspace-1',
context: 'workspace',
fileContentUpdatedAt: CONTENT_UPDATED_AT,
secretProvenanceVersion: 1,
provenanceContentUpdatedAt: CONTENT_UPDATED_AT,
status: 'unknown',
entries: [],
},
{
id: 'stale-id',
key: 'stale-key',
@@ -1021,6 +1090,54 @@ describe('workspace file secret provenance', () => {
)
})
/**
* A fork or chat copy must not turn a readable absence into a permanent refusal. Folding
* `unrecorded` in with the refusals would hand the target a taint the source never carried, and
* nothing rewrites a file's provenance but another content write.
*/
it('copies a recorded absence as an absence, not as a refusal', async () => {
const targetContentUpdatedAt = new Date('2026-08-04T00:00:01.000Z')
dbChainMockFns.limit
.mockResolvedValueOnce([
{
userId: 'user-1',
workspaceId: 'workspace-2',
contentUpdatedAt: targetContentUpdatedAt,
},
])
.mockResolvedValueOnce([
{
key: 'source-key',
userId: 'user-1',
workspaceId: 'workspace-1',
secretProvenanceVersion: 1,
fileContentUpdatedAt: CONTENT_UPDATED_AT,
provenanceContentUpdatedAt: CONTENT_UPDATED_AT,
status: 'unrecorded',
entries: [],
},
])
await copyWorkspaceFileSecretProvenanceInTx(
dbChainMock.db as unknown as DbTransaction,
{
fileId: 'source-file',
key: 'source-key',
contentUpdatedAtMs: CONTENT_UPDATED_AT.getTime(),
},
'target-file'
)
expect(dbChainMockFns.values).toHaveBeenCalledWith(
expect.objectContaining({
fileId: 'target-file',
contentUpdatedAt: targetContentUpdatedAt,
status: 'unrecorded',
entries: [],
})
)
})
it('preserves anonymous provenance across a byte-identical file copy', async () => {
const targetContentUpdatedAt = new Date('2026-08-04T00:00:01.000Z')
dbChainMockFns.limit
@@ -1287,4 +1404,108 @@ describe('workspace file secret provenance', () => {
})
)
})
/**
* The whole reason this surface was brought under the policy: a file that was never tracked
* already reads as safe two branches earlier, and an unrecorded one says exactly the same thing
* about its contents. Refusing only the second left files permanently unreadable for having
* tried to record provenance and failed.
*/
it('reads an unrecorded file, as it already reads an untracked one', async () => {
queueTableRows(workspaceFiles, [
{
id: 'unrecorded-id',
key: 'unrecorded-key',
workspaceId: 'workspace-1',
context: 'workspace',
fileContentUpdatedAt: CONTENT_UPDATED_AT,
secretProvenanceVersion: 1,
provenanceContentUpdatedAt: CONTENT_UPDATED_AT,
status: 'unrecorded',
entries: [],
},
])
await expect(isModelSafeWorkspaceFileKey('unrecorded-key')).resolves.toBe(true)
expect(mockReport).toHaveBeenCalledWith(
expect.objectContaining({ surface: 'workspace-file', cause: 'durable-provenance-unknown' })
)
})
/**
* The row has to be a recorded absence, not a refusal. A stored `unknown` is refused whatever the
* flag says, so asserting against one would pass with enforcement off and prove nothing about the
* switch this whole posture rests on.
*/
it('refuses an unrecorded file again once the surface is closed', async () => {
mockIsEnforced.mockReturnValue(true)
queueTableRows(workspaceFiles, [
{
id: 'unrecorded-id',
key: 'unrecorded-key',
workspaceId: 'workspace-1',
context: 'workspace',
fileContentUpdatedAt: CONTENT_UPDATED_AT,
secretProvenanceVersion: 1,
provenanceContentUpdatedAt: CONTENT_UPDATED_AT,
status: 'unrecorded',
entries: [],
},
])
await expect(isModelSafeWorkspaceFileKey('unrecorded-key')).resolves.toBe(false)
expect(mockReport).not.toHaveBeenCalled()
})
/**
* Narrower than "not exact" on purpose. A stale binding describes content that has since changed
* and a malformed sidecar is a fault; neither is the absence the policy covers, and neither may
* ride in on it.
*/
it('still refuses a stale binding and a malformed sidecar', async () => {
for (const row of [
{
id: 'stale-id',
key: 'stale-key',
workspaceId: 'workspace-1',
context: 'workspace',
fileContentUpdatedAt: CONTENT_UPDATED_AT,
secretProvenanceVersion: 1,
provenanceContentUpdatedAt: new Date('2026-08-03T00:00:00.000Z'),
status: 'unknown',
entries: [],
},
{
id: 'malformed-id',
key: 'malformed-key',
workspaceId: 'workspace-1',
context: 'workspace',
fileContentUpdatedAt: CONTENT_UPDATED_AT,
secretProvenanceVersion: 1,
provenanceContentUpdatedAt: CONTENT_UPDATED_AT,
status: 'unknown',
entries: 'not-an-array',
},
]) {
queueTableRows(workspaceFiles, [row])
await expect(isModelSafeWorkspaceFileKey(row.key)).resolves.toBe(false)
}
})
/**
* Bytes nobody vouched for do not become vouched-for by being combined with bytes that were.
* Falling through to the exact branch would have handed a later boundary a positive claim that
* neither input made — a stronger statement than either, produced by merging.
*/
it('keeps an unrecorded contribution unrecorded when merged with an exact one', () => {
expect(
mergeWorkspaceFileSecretProvenance({ status: 'exact', entries: [] }, { status: 'unrecorded' })
).toEqual({ status: 'unrecorded' })
})
it('still lets an unknown contribution dominate an unrecorded one', () => {
expect(
mergeWorkspaceFileSecretProvenance({ status: 'unrecorded' }, { status: 'unknown' })
).toEqual({ status: 'unknown' })
})
})
@@ -12,6 +12,10 @@ import {
importDurableSecretProvenance,
isPrivateSecretProvenanceScopeCompatible,
} from '@/lib/execution/durable-secret-provenance'
import {
isDurableSecretProvenanceEnforced,
reportUnrecordedDurableProvenance,
} from '@/lib/execution/durable-secret-provenance-enforcement'
import {
PROVENANCE_MAX_ENTRIES,
PROVENANCE_MAX_SERIALIZED_BYTES,
@@ -29,9 +33,23 @@ const LEGACY_ANONYMOUS_WORKSPACE_FILE_SECRET_STORAGE_NAME =
export const MODEL_UNSAFE_WORKSPACE_FILE_ERROR_MESSAGE =
'File cannot be sent to a model because its secret provenance is unavailable'
/**
* What can be said about the secrets a file's bytes carry.
*
* Note the deliberate mismatch with storage: the sidecar's `status` column holds only
* `'exact' | 'unknown'`, and a stored `'unknown'` maps to `'unrecorded'` here, not to the
* `'unknown'` below. Storage is recording what the writer could vouch for; this union is recording
* what a reader can conclude, and "the writer said it could not vouch" and "there is nothing usable
* to read" are different conclusions that must not share a branch — only the first is an absence a
* policy may relax. `'unrecorded'` is the name the shared vocabulary already uses for it
* (`reportUnrecordedDurableProvenance`, the `secret_provenance.unrecorded` audit action).
*/
export type WorkspaceFileSecretProvenance =
| { status: 'exact'; entries: readonly WorkspaceFileSecretProvenanceEntry[] }
/** Nothing can be said: the row is gone, the version moved, or the sidecar is stale or malformed. */
| { status: 'unknown' }
/** A current sidecar recording that nobody vouched for these bytes — an absence, not a fault. */
| { status: 'unrecorded' }
export const EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE = Object.freeze({
status: 'exact' as const,
@@ -95,13 +113,23 @@ interface ModelSafeWorkspaceFileRow {
entries: unknown
}
/** Combines byte-contributing classifications without broadening an unknown source. */
/**
* Combines byte-contributing classifications without broadening any source.
*
* Ordered by how little each says: `unknown` beats `unrecorded`, which beats `exact`. An absence
* has to survive the merge — bytes nobody vouched for do not become vouched-for by being combined
* with bytes that were, and dropping through to the exact branch would hand a later boundary a
* positive claim that neither input made.
*/
export function mergeWorkspaceFileSecretProvenance(
...provenances: readonly WorkspaceFileSecretProvenance[]
): WorkspaceFileSecretProvenance {
if (provenances.some((provenance) => provenance.status === 'unknown')) {
return { status: 'unknown' }
}
if (provenances.some((provenance) => provenance.status === 'unrecorded')) {
return { status: 'unrecorded' }
}
return {
status: 'exact',
@@ -214,7 +242,12 @@ export async function createWorkspaceFileSecretProvenanceFromRegistry(
representations: readonly WorkspaceFileSecretProvenanceRepresentation[] = [],
representationsComplete = true
): Promise<WorkspaceFileSecretProvenanceWriteDecision> {
if (!registry) return { safe: true, provenance: { status: 'unknown' } }
/**
* No registry means no recorder ran, so nothing was written down about these bytes. That is an
* absence, and it is the one thing this surface's policy may relax — distinct from the taint
* every `safe: false` below produces, which a caller persists as `unknown` and no policy relaxes.
*/
if (!registry) return { safe: true, provenance: { status: 'unrecorded' } }
const sourceProvenance = registry.exportCommittedProvenanceForValue(sourceValue)
const persistedProvenance = Object.is(sourceValue, persistedValue)
? sourceProvenance
@@ -394,31 +427,47 @@ export async function replaceWorkspaceFileSecretProvenanceInTx(
return
}
/**
* Absence and taint are different claims and must not share a stored value. Collapsing them here
* is what let a file the writer deliberately refused — a child of a secret-bearing archive, a
* generated asset whose safety decision was `false` — become indistinguishable from one nobody
* recorded, and therefore eligible for the same relaxation.
*/
const status = provenance.status === 'unrecorded' ? 'unrecorded' : 'unknown'
await tx
.insert(workspaceFileSecretProvenance)
.values({ fileId, contentUpdatedAt, status: 'unknown', entries: [], updatedAt: new Date() })
.values({ fileId, contentUpdatedAt, status, entries: [], updatedAt: new Date() })
.onConflictDoUpdate({
target: workspaceFileSecretProvenance.fileId,
set: { contentUpdatedAt, status: 'unknown', entries: [], updatedAt: new Date() },
set: { contentUpdatedAt, status, entries: [], updatedAt: new Date() },
})
await markWorkspaceFileSecretProvenanceTrackedInTx(tx, fileId, contentUpdatedAt)
}
/** Initializes provenance for an exact file version without replacing an existing classification. */
/**
* Initializes provenance for an exact file version without replacing an existing classification.
*
* Narrows to the two states the column accepts, as {@link replaceWorkspaceFileSecretProvenanceInTx}
* does. The union has three; the CHECK constraint permits `('exact', 'unknown')`, so forwarding the
* status verbatim would let an `'unrecorded'` reach the database as a value it rejects — a
* constraint violation aborting the enclosing transaction, not a bad row. Nothing passes one today,
* which is exactly why it needs saying here rather than in a caller.
*/
export async function initializeWorkspaceFileSecretProvenanceInTx(
tx: DbTransaction,
fileId: string,
contentUpdatedAt: Date,
provenance: WorkspaceFileSecretProvenance
): Promise<void> {
const entries =
provenance.status === 'exact' ? serializeExactEntriesForStorage(provenance.entries) : []
const isExact = provenance.status === 'exact'
const entries = isExact ? serializeExactEntriesForStorage(provenance.entries) : []
const status = isExact ? 'exact' : provenance.status === 'unrecorded' ? 'unrecorded' : 'unknown'
await tx
.insert(workspaceFileSecretProvenance)
.values({
fileId,
contentUpdatedAt,
status: provenance.status,
status,
entries,
updatedAt: new Date(),
})
@@ -536,8 +585,29 @@ export async function copyWorkspaceFileSecretProvenanceInTx(
}
if (
source.provenanceContentUpdatedAt?.getTime() !== source.fileContentUpdatedAt.getTime() ||
!isValidStoredEntries(source.entries)
) {
await replaceWorkspaceFileSecretProvenanceInTx(tx, targetFileId, target.contentUpdatedAt, {
status: 'unknown',
})
return
}
/**
* An absence copies as an absence. Folding it in with the refusals below would hand the target a
* taint the source never carried, and a fork or a chat copy would turn a readable file into a
* permanently refused one — the pathology this surface exists to undo, reached by copying.
*
* Safe to carry across the scope checks below, which exist to stop one workspace's secret entries
* landing in another's file. A recorded absence has no entries to carry.
*/
if (source.status === 'unrecorded') {
await replaceWorkspaceFileSecretProvenanceInTx(tx, targetFileId, target.contentUpdatedAt, {
status: 'unrecorded',
})
return
}
if (
source.status !== 'exact' ||
!isValidStoredEntries(source.entries) ||
(source.entries.length > 0 &&
(!source.workspaceId ||
!target.workspaceId ||
@@ -634,6 +704,10 @@ export async function getBoundWorkspaceFileSecretProvenance(
.limit(1)
if (!row) return { status: 'unknown' }
/**
* A version mismatch is not an absence: the caller is asking about content this file no longer
* holds, so nothing can be said about it and no policy relaxes that.
*/
if (
identity.contentUpdatedAt &&
identity.contentUpdatedAt.getTime() !== row.fileContentUpdatedAt.getTime()
@@ -642,13 +716,17 @@ export async function getBoundWorkspaceFileSecretProvenance(
}
if (row.secretProvenanceVersion === null) return EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE
if (row.secretProvenanceVersion !== 1) return { status: 'unknown' }
if (
row.provenanceContentUpdatedAt?.getTime() !== row.fileContentUpdatedAt.getTime() ||
row.status !== 'exact' ||
!isValidStoredEntries(row.entries)
) {
return { status: 'unknown' }
}
const bindingIsCurrent =
row.provenanceContentUpdatedAt?.getTime() === row.fileContentUpdatedAt.getTime()
if (!bindingIsCurrent || !isValidStoredEntries(row.entries)) return { status: 'unknown' }
/**
* The one shape the surface's policy may relax: a sidecar bound to this exact content recording
* that nobody vouched for it — the same statement the untracked file above makes. A stored
* `unknown` is the opposite claim, written by a writer that refused these bytes on purpose, and
* stays refused.
*/
if (row.status === 'unrecorded') return { status: 'unrecorded' }
if (row.status !== 'exact') return { status: 'unknown' }
return { status: 'exact', entries: deserializeExactEntriesFromStorage(row.entries) }
}
@@ -697,12 +775,25 @@ export async function getBoundWorkspaceFileSecretProvenanceByMetadata(
if (
row.secretProvenanceVersion !== 1 ||
row.provenanceContentUpdatedAt?.getTime() !== row.fileContentUpdatedAt.getTime() ||
row.status !== 'exact' ||
!isValidStoredEntries(row.entries)
) {
result.set(row.id, { status: 'unknown' })
continue
}
/**
* Same answer the single-file reader gives the same row. Collapsing a recorded absence into
* `unknown` here would leave two classifiers describing one policy differently, and the
* caller that eventually distinguishes them would get a different verdict depending on which
* one it happened to call.
*/
if (row.status === 'unrecorded') {
result.set(row.id, { status: 'unrecorded' })
continue
}
if (row.status !== 'exact') {
result.set(row.id, { status: 'unknown' })
continue
}
result.set(row.id, {
status: 'exact',
entries: deserializeExactEntriesFromStorage(row.entries),
@@ -715,6 +806,33 @@ export async function getBoundWorkspaceFileSecretProvenanceByMetadata(
return result
}
/**
* Whether a file nobody could vouch for may still be read.
*
* A file that was never tracked already returns exact-empty a branch earlier, and it makes exactly
* the same statement as this one: nobody recorded which secrets these bytes carry. Refusing only
* the second left a writer that momentarily could not vouch permanently worse off than one that
* never tried, with no way back — nothing rewrites a file's provenance but another content write,
* so the file simply stopped working, everywhere, for good.
*
* So it reads, and the workspace is told. Deliberately narrower than "not exact": a stale binding
* describes content that has since changed and a malformed sidecar is a fault, and neither is the
* absence this covers. Closing the surface again is a matter of naming it in
* `DURABLE_SECRET_PROVENANCE_ENFORCED_SURFACES`.
*/
function mayReadUnrecordedWorkspaceFile(workspaceId: string | undefined, count = 1): boolean {
if (isDurableSecretProvenanceEnforced('workspace-file')) return false
if (count > 0) {
reportUnrecordedDurableProvenance({
surface: 'workspace-file',
cause: 'durable-provenance-unknown',
...(count > 1 ? { affectedCount: count } : {}),
...(workspaceId ? { workspaceId } : {}),
})
}
return true
}
/**
* Authorizes one model-facing view of an exact workspace-file version. Complete text views import
* the entire sidecar so representation-changing consumers retain the original lineage. Derived
@@ -730,6 +848,7 @@ export async function importWorkspaceFileSecretProvenanceForModelView(args: {
}): Promise<boolean> {
const provenance = await getBoundWorkspaceFileSecretProvenance(args.workspaceId, args.identity)
if (provenance.status === 'unknown') return false
if (provenance.status === 'unrecorded') return mayReadUnrecordedWorkspaceFile(args.workspaceId)
if (provenance.entries.length === 0) return true
if (args.view === 'opaque' || !args.registry) return false
@@ -748,7 +867,9 @@ export async function isOpaqueWorkspaceFileEgressSafe(
identity: WorkspaceFileSecretProvenanceIdentity
): Promise<boolean> {
const provenance = await getBoundWorkspaceFileSecretProvenance(workspaceId, identity)
return provenance.status === 'exact' && provenance.entries.length === 0
if (provenance.status === 'unknown') return false
if (provenance.status === 'unrecorded') return mayReadUnrecordedWorkspaceFile(workspaceId)
return provenance.entries.length === 0
}
/**
@@ -763,6 +884,7 @@ export async function importWorkspaceFileSecretProvenanceForRuntime(args: {
}): Promise<boolean> {
const provenance = await getBoundWorkspaceFileSecretProvenance(args.workspaceId, args.identity)
if (provenance.status === 'unknown') return false
if (provenance.status === 'unrecorded') return mayReadUnrecordedWorkspaceFile(args.workspaceId)
if (provenance.entries.length === 0) return true
if (!args.registry) return false
@@ -801,30 +923,44 @@ export async function filterModelSafeWorkspaceFileAttachments<
const rows = await loadModelSafeWorkspaceFileRows(keys)
const rowByKey = new Map(rows.map((row) => [row.key, row]))
return attachments.filter((attachment) => {
let unrecorded = 0
const kept = attachments.filter((attachment) => {
if (typeof attachment.key !== 'string' || attachment.key.length === 0) return true
const row = rowByKey.get(attachment.key)
if (!row) return true
if (row.context !== 'workspace' && row.context !== 'mothership') return true
return isModelSafeWorkspaceFileRow(row, options.workspaceId)
const classification = classifyModelSafeWorkspaceFileRow(row, options.workspaceId)
if (classification === 'safe') return true
if (classification === 'unsafe') return false
unrecorded += 1
return !isDurableSecretProvenanceEnforced('workspace-file')
})
/** One report for the whole set of attachments, which is one read, rather than one per file. */
if (unrecorded > 0) mayReadUnrecordedWorkspaceFile(options.workspaceId, unrecorded)
return kept
}
function isModelSafeWorkspaceFileRow(
/**
* Classifies one row without deciding it. `unrecorded` is kept apart from `unsafe` because only the
* first is the same statement an untracked file makes, and only the first is the surface's policy
* to relax — a stale binding describes content that has since changed, and a malformed sidecar is a
* fault no policy relaxes.
*/
type ModelSafeWorkspaceFileClassification = 'safe' | 'unrecorded' | 'unsafe'
function classifyModelSafeWorkspaceFileRow(
row: ModelSafeWorkspaceFileRow,
workspaceId?: string
): boolean {
if (workspaceId && row.workspaceId !== workspaceId) return false
if (row.secretProvenanceVersion === null) return true
if (row.secretProvenanceVersion !== 1) return false
if (
row.provenanceContentUpdatedAt?.getTime() !== row.fileContentUpdatedAt.getTime() ||
row.status !== 'exact' ||
!isValidStoredEntries(row.entries)
) {
return false
}
return row.entries.length === 0
): ModelSafeWorkspaceFileClassification {
if (workspaceId && row.workspaceId !== workspaceId) return 'unsafe'
if (row.secretProvenanceVersion === null) return 'safe'
if (row.secretProvenanceVersion !== 1) return 'unsafe'
const bindingIsCurrent =
row.provenanceContentUpdatedAt?.getTime() === row.fileContentUpdatedAt.getTime()
if (!bindingIsCurrent || !isValidStoredEntries(row.entries)) return 'unsafe'
if (row.status === 'unrecorded') return 'unrecorded'
if (row.status !== 'exact') return 'unsafe'
return row.entries.length === 0 ? 'safe' : 'unsafe'
}
async function loadModelSafeWorkspaceFileRows(
@@ -879,9 +1015,13 @@ export async function areModelSafeWorkspaceFileKeys(
const rows = await loadModelSafeWorkspaceFileRows(uniqueKeys)
return rows.every(
(row) =>
(row.context !== 'workspace' && row.context !== 'mothership') ||
isModelSafeWorkspaceFileRow(row, options.workspaceId)
)
let unrecorded = 0
for (const row of rows) {
if (row.context !== 'workspace' && row.context !== 'mothership') continue
const classification = classifyModelSafeWorkspaceFileRow(row, options.workspaceId)
if (classification === 'unsafe') return false
if (classification === 'unrecorded') unrecorded += 1
}
/** One report for the batch, not one per key: a caller checking many keys is one read. */
return unrecorded === 0 || mayReadUnrecordedWorkspaceFile(options.workspaceId, unrecorded)
}
@@ -0,0 +1,4 @@
-- migration-safe: replaces the status CHECK with a strictly wider one, adding 'unrecorded' to the existing ('exact', 'unknown'). Every stored row and every write the currently-deployed code can make already satisfies the replacement, so dropping it opens no window in which an old write is rejected, and it is re-added in the same transaction below.
ALTER TABLE "workspace_file_secret_provenance" DROP CONSTRAINT "workspace_file_secret_provenance_status_check";--> statement-breakpoint
ALTER TABLE "workspace_file_secret_provenance" ADD CONSTRAINT "workspace_file_secret_provenance_status_check" CHECK ("workspace_file_secret_provenance"."status" IN ('exact', 'unknown', 'unrecorded')) NOT VALID;--> statement-breakpoint
ALTER TABLE "workspace_file_secret_provenance" VALIDATE CONSTRAINT "workspace_file_secret_provenance_status_check";
File diff suppressed because it is too large Load Diff
@@ -2108,6 +2108,13 @@
"when": 1787365065919,
"tag": "0301_backfill_table_default_views",
"breakpoints": true
},
{
"idx": 302,
"version": "7",
"when": 1787432959648,
"tag": "0302_fat_sentry",
"breakpoints": true
}
]
}
+1 -1
View File
@@ -2263,7 +2263,7 @@ export const workspaceFileSecretProvenance = pgTable(
(table) => ({
statusCheck: check(
'workspace_file_secret_provenance_status_check',
sql`${table.status} IN ('exact', 'unknown')`
sql`${table.status} IN ('exact', 'unknown', 'unrecorded')`
),
})
)
@@ -443,6 +443,7 @@ describe('script migration registry', () => {
'0004_backfill_fork_kb_file_ownership',
'0005_repair_unknown_table_row_provenance',
'0006_repair_unknown_table_row_provenance_second_pass',
'0007_repair_unknown_workspace_file_provenance',
])
})
})
@@ -0,0 +1,154 @@
/**
* @vitest-environment node
*/
import type { Sql } from 'postgres'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { repairUnknownTableRowProvenance } from './script-migrations/0005_repair_unknown_table_row_provenance'
import { repairUnknownWorkspaceFileProvenance } from './script-migrations/0007_repair_unknown_workspace_file_provenance'
/** Flattens a tagged-template call back to comparable statement text. */
function toStatement(strings: TemplateStringsArray): string {
return strings.join(' ').replace(/\s+/g, ' ').trim()
}
interface RecordingSql {
sql: Sql
statements: string[]
}
/**
* A `postgres` stand-in that records statement order and answers each query by shape.
*
* The candidate select yields one id on the first page and nothing on the second, so the keyset
* walk terminates; the delete reports that id as cleared. Enough for the repair to complete one
* full page, which is all the ordering assertions need.
*/
function createRecordingSql(sidecarTable: string): RecordingSql {
const statements: string[] = []
let candidatePages = 0
const respond = (statement: string): unknown[] => {
if (statement.includes(`FROM ${sidecarTable}`) && statement.includes('LIMIT')) {
candidatePages += 1
return candidatePages === 1 ? [{ rowId: 'id-1', fileId: 'id-1' }] : []
}
if (statement.startsWith('DELETE')) return [{ rowId: 'id-1', fileId: 'id-1' }]
if (statement.startsWith('UPDATE')) return [{ id: 'id-1' }]
return []
}
const query = (strings: TemplateStringsArray): Promise<unknown[]> => {
const statement = toStatement(strings)
statements.push(statement)
return Promise.resolve(respond(statement))
}
const sql = Object.assign(query, {
begin: (fn: (tx: unknown) => Promise<unknown>) => fn(sql),
unsafe: (statement: string) => {
statements.push(statement)
return Promise.resolve([])
},
}) as unknown as Sql
return { sql, statements }
}
/**
* Both repairs clear a sidecar row and then clear its parent's version marker, which the live
* writers do in the opposite order — parent first, sidecar second, one transaction. Taking two
* locks in two orders is a deadlock, and Postgres resolves it by aborting somebody: either the
* deployment running the repair or a tenant's write. This has now been introduced twice, once per
* migration, so it is pinned rather than reasoned about.
*/
describe('unknown provenance repair lock order', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('locks user_table_rows before deleting the row sidecar', async () => {
const { sql, statements } = createRecordingSql('user_table_row_secret_provenance')
await repairUnknownTableRowProvenance.up(sql)
const lockIndex = statements.findIndex(
(statement) => statement.includes('FROM user_table_rows') && statement.includes('FOR UPDATE')
)
const deleteIndex = statements.findIndex((statement) => statement.startsWith('DELETE'))
expect(lockIndex).toBeGreaterThanOrEqual(0)
expect(deleteIndex).toBeGreaterThanOrEqual(0)
expect(lockIndex).toBeLessThan(deleteIndex)
})
it('locks workspace_files before deleting the file sidecar', async () => {
const { sql, statements } = createRecordingSql('workspace_file_secret_provenance')
await repairUnknownWorkspaceFileProvenance.up(sql)
const lockIndex = statements.findIndex(
(statement) => statement.includes('FROM workspace_files') && statement.includes('FOR UPDATE')
)
const deleteIndex = statements.findIndex((statement) => statement.startsWith('DELETE'))
expect(lockIndex).toBeGreaterThanOrEqual(0)
expect(deleteIndex).toBeLessThan(statements.length)
expect(lockIndex).toBeLessThan(deleteIndex)
})
/**
* The lock has to cover every id the delete will touch, not just the first. A per-id lock taken
* inside the delete's own scan would order the two operations correctly for one row and still
* deadlock across a page.
*/
it('locks the same page the delete clears, in id order', async () => {
const { sql, statements } = createRecordingSql('workspace_file_secret_provenance')
await repairUnknownWorkspaceFileProvenance.up(sql)
const lock = statements.find(
(statement) => statement.includes('FROM workspace_files') && statement.includes('FOR UPDATE')
)
expect(lock).toContain('id = ANY(')
expect(lock).toContain('ORDER BY id')
})
/**
* 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. A status-only predicate is wider than
* that, and the extra rows are faults rather than absences — clearing one sets the version to
* NULL, which reads back as exact-empty, promoting a refused file to positively vouched for with
* no audit entry. Both the candidate query and the delete carry every condition.
*/
it('targets only what the reader calls unrecorded, in the select and the delete', async () => {
const { sql, statements } = createRecordingSql('workspace_file_secret_provenance')
await repairUnknownWorkspaceFileProvenance.up(sql)
const candidateSelect = statements.find(
(statement) =>
statement.includes('FROM workspace_file_secret_provenance') && statement.includes('LIMIT')
)
const deleteStatement = statements.find((statement) => statement.startsWith('DELETE'))
for (const statement of [candidateSelect, deleteStatement]) {
expect(statement).toContain("status = 'unknown'")
expect(statement).toContain('secret_provenance_version = 1')
expect(statement).toContain('content_updated_at = f.content_updated_at')
expect(statement).toContain("jsonb_typeof(p.entries) = 'array'")
}
})
/**
* Re-checking `status = 'unknown'` under the parent lock is what stops the repair clearing a
* sidecar a writer just made exact — which would strand a genuinely secret-bearing file reading
* as legacy, provenance destroyed by the repair meant to make provenance safe.
*/
it('re-checks the unknown status inside the delete', async () => {
const { sql, statements } = createRecordingSql('workspace_file_secret_provenance')
await repairUnknownWorkspaceFileProvenance.up(sql)
const deleteStatement = statements.find((statement) => statement.startsWith('DELETE'))
expect(deleteStatement).toContain("status = 'unknown'")
})
})
@@ -0,0 +1,144 @@
import type { Sql } from 'postgres'
import type { ScriptMigration } from './types'
export const UNKNOWN_FILE_PROVENANCE_REPAIR_BATCH_SIZE = 1000
interface RepairPage {
candidates: number
repaired: number
lastFileId: string | null
}
/**
* Returns one page of files the reader calls `unrecorded` to the untracked state.
*
* The predicate is the reader's own, condition for condition: version 1, a sidecar bound to the
* file's current bytes, a well-formed entries array, and a status of `unknown`. That is precisely
* the branch `getBoundWorkspaceFileSecretProvenance` answers `unrecorded` to, and a status-only
* query is wider than it. A sidecar left bound to bytes the file no longer holds reads as `unknown`
* — a fault the surface refuses and no policy relaxes — but clearing it here would set
* `secret_provenance_version` to NULL, and a NULL version reads as *exact-empty*: the file would go
* from refused to positively vouched for, silently and with no audit entry, on the strength of a
* sidecar that described different bytes. Relaxing an absence is this migration's whole purpose;
* promoting a fault is not, and the same three conditions guard the delete so the distinction
* cannot be lost to a concurrent write.
*
* Takes the parent row lock before touching the sidecar, in `id` order, because that is the order
* a content write takes them: `workspace-file-manager` updates `workspace_files` and only then
* replaces the sidecar, inside the same transaction. Deleting the sidecar first and updating the
* parent after is the opposite order, so an overlapping upload would deadlock and Postgres would
* resolve it by aborting either the deployment or somebody's file write.
*
* Holding that lock is also what makes the re-check decisive: a content write moves
* `content_updated_at` and rewrites the sidecar under the same lock, so once it is held the write
* is either wholly done or has not begun, and a file it has meanwhile made exact or rebound stops
* matching.
*/
async function repairUnknownFileProvenancePage(
sql: Sql,
batchSize: number,
afterFileId: string
): Promise<RepairPage> {
const candidates = await sql<{ fileId: string }[]>`
SELECT p.file_id AS "fileId"
FROM workspace_file_secret_provenance p
JOIN workspace_files f ON f.id = p.file_id
WHERE p.status = 'unknown'
AND f.secret_provenance_version = 1
AND p.content_updated_at = f.content_updated_at
AND jsonb_typeof(p.entries) = 'array'
AND p.file_id > ${afterFileId}
ORDER BY p.file_id
LIMIT ${batchSize}
`
if (candidates.length === 0) return { candidates: 0, repaired: 0, lastFileId: null }
const fileIds = candidates.map((candidate) => candidate.fileId)
const repaired = await sql.begin(async (tx) => {
await tx`
SELECT id FROM workspace_files
WHERE id = ANY(${fileIds}::text[])
ORDER BY id
FOR UPDATE
`
const cleared = await tx<{ fileId: string }[]>`
DELETE FROM workspace_file_secret_provenance p
USING workspace_files f
WHERE p.file_id = f.id
AND p.file_id = ANY(${fileIds}::text[])
AND p.status = 'unknown'
AND f.secret_provenance_version = 1
AND p.content_updated_at = f.content_updated_at
AND jsonb_typeof(p.entries) = 'array'
RETURNING p.file_id AS "fileId"
`
if (cleared.length === 0) return 0
const marked = await tx<{ id: string }[]>`
UPDATE workspace_files
SET secret_provenance_version = NULL
WHERE id = ANY(${cleared.map((row) => row.fileId)}::text[])
RETURNING id
`
return marked.length
})
return {
candidates: fileIds.length,
repaired: repaired as number,
lastFileId: fileIds[fileIds.length - 1],
}
}
/**
* Restores files whose secret provenance nobody recorded to the untracked state.
*
* These files were unreadable. A workspace file reading `unknown` was refused by every model and
* runtime boundary at once — attachments, mounts, and the tool routes that parse, transcribe or
* describe a file — while a file that was never tracked at all returned exact-empty and worked
* fine. The two say the same thing about their contents, which is nothing, so the second was a
* permanent penalty for having tried to record provenance and failed.
*
* The surface now reads such a file and records an audit entry instead, which is what the other
* durable surfaces already do. That fixes the behaviour going forward but not the files already in
* the state, which would otherwise report on every read forever; this clears them so the trail
* carries only what happens next.
*
* Files whose sidecar is stale, unversioned, or malformed are faults rather than absences: they
* stay refused, exactly as the surface refuses them, and recover the way they always have — on the
* next content write, which rebinds the sidecar. Idempotent: a repaired file has no sidecar row, so
* it leaves the candidate set and a re-run costs one empty query.
*
* One-time amnesty, and deliberately so. Until the release that ships this, storage collapsed two
* different claims into `unknown`: bytes nobody recorded, and bytes a writer refused on purpose —
* a child of a secret-bearing archive, a generated asset whose safety decision came back false.
* Writers now record those separately, so the distinction holds from here on and this population
* cannot grow. It cannot be recovered retroactively, though: the rows that already exist do not say
* which they were, and no column separates them. Clearing them therefore accepts that a few may
* have been refusals rather than absences — an accepted risk for a bounded, days-old population on
* a feature with no dependents yet, taken so the workspaces holding unusable files recover.
*
* Runs before the image that ships the distinction is promoted, so the rows it sees are exactly the
* collapsed legacy population and none of the ones written afterwards.
*/
export const repairUnknownWorkspaceFileProvenance: ScriptMigration = {
name: '0007_repair_unknown_workspace_file_provenance',
async up(sql: Sql): Promise<void> {
let repaired = 0
let skipped = 0
let afterFileId = ''
for (;;) {
const page = await repairUnknownFileProvenancePage(
sql,
UNKNOWN_FILE_PROVENANCE_REPAIR_BATCH_SIZE,
afterFileId
)
if (page.candidates === 0 || page.lastFileId === null) break
repaired += page.repaired
skipped += page.candidates - page.repaired
afterFileId = page.lastFileId
}
console.log(
`Unknown workspace file provenance repair complete: ${repaired} file(s) repaired, ${skipped} left to a concurrent writer.`
)
},
}
+2
View File
@@ -5,6 +5,7 @@ import { backfillWorkspaceStorageUsage } from './0003_backfill_workspace_storage
import { backfillForkKnowledgeBaseFileOwnership } from './0004_backfill_fork_kb_file_ownership'
import { repairUnknownTableRowProvenance } from './0005_repair_unknown_table_row_provenance'
import { repairUnknownTableRowProvenanceSecondPass } from './0006_repair_unknown_table_row_provenance_second_pass'
import { repairUnknownWorkspaceFileProvenance } from './0007_repair_unknown_workspace_file_provenance'
import type { ScriptMigration } from './types'
export type { ScriptMigration } from './types'
@@ -21,6 +22,7 @@ export const scriptMigrations: readonly ScriptMigration[] = [
backfillForkKnowledgeBaseFileOwnership,
repairUnknownTableRowProvenance,
repairUnknownTableRowProvenanceSecondPass,
repairUnknownWorkspaceFileProvenance,
]
/**