Skip to content

Commit a0e5f46

Browse files
fix(mothership): admit stored content to Chat per item through one gate (#8941)
* test(mothership): record non-Chat responses of shared content routes before gating Seeds one clean, secret-bearing, unknown and unrecorded item per content store and snapshots every shared content route as an API-key caller sees it, before any Chat delivery gate changes. * fix(mothership): admit stored content to Chat per item through one gate Content from workspace files, run files, knowledge and table rows now reaches Chat's model only after each item is classified and admitted by the gate of the surface it leaves through: the registry gate (Chat tools, CLI, mounts) or the session gate (run_code sandbox proxy). Unknown items are excluded per item instead of withholding whole results; items with recorded secrets are redacted where text, refused where opaque. Non-Chat callers are unchanged. Every operation that admits Copilot declares contentDelivery, and a sourced use case must account for what it returns, or Chat's transports refuse the response. Closes the bulk-download, search, run-file, base64, knowledge, share, export, table and tools-execute paths, and redacts grep lines before clipping them. * fix(mothership): address review round on content delivery Committed writes answer saved-not-shown instead of a retryable error, keep their panel refresh, and account per operation. Chat streams pin inline bytes off, taint needs a registry, compile admits before its cache, owned files bind to their context, and count/truncation oracles are closed for Chat in knowledge, files search and row search. Table selection cells are projected before escaping and coerced import cells keep their taint. * fix(mothership): close sharing, line-window and snapshot gaps in content delivery Chat may share or see links only to clean files whatever the surface, text is redacted before a line window cuts it, accounting follows a decision, cached compiles and knowledge metadata bind to the versions they show, row-search matches classify at their matched revision, and table counts follow admitted rows. * fix(mothership): refuse Chat table predicates over unsafe cells A table read whose rows, counts, cursor or affected count depend on cell values is refused for Chat, before the predicate runs, when the table has an unvouched row or a secret in a referenced column. Inline run-file bytes are collected per storage key, and table-selection fallbacks are projected. * fix(mothership): keep content delivery scoped and consistent * fix(mothership): preserve protection across content transformations * fix(mothership): preserve file read semantics * fix(mothership): preserve sharing and search semantics * fix(mothership): bind media admission to loaded files * fix(mothership): preserve content revisions and archive budgets * fix(mothership): preserve partial downloads during compilation * fix(sandbox): preserve broker error identity --------- Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
1 parent ac8e215 commit a0e5f46

247 files changed

Lines changed: 12819 additions & 2564 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
description: How stored content (workspace files, run files, knowledge, table rows) reaches Chat's model, and the per-path policy. Read before changing a use case, route or Chat surface that returns such content.
3+
paths:
4+
- "apps/sim/lib/core/application/**"
5+
- "apps/sim/lib/mothership/content-delivery/**"
6+
- "apps/sim/lib/workspace-files/application/**"
7+
- "apps/sim/lib/knowledge/application/**"
8+
- "apps/sim/lib/table/application/**"
9+
- "apps/sim/lib/workflows/application/**"
10+
- "apps/sim/lib/mothership/chat/application/**"
11+
- "apps/sim/lib/**/application/operations.ts"
12+
- "apps/sim/lib/mothership/agent-cli/**"
13+
- "apps/sim/lib/mothership/tools/**"
14+
- "apps/sim/app/api/v2/**"
15+
---
16+
17+
# Sim Content Delivery to Chat
18+
19+
Content derived from a provenance-bearing store reaches Chat's model only after **each item** it
20+
came from is admitted by the gate of the surface it leaves through. The stores are workspace files
21+
(and their versions), run files, knowledge documents and chunks, and table rows. "Derived" covers
22+
bytes, text, excerpts, chunks, cells, previews, base64, archives, rendered or compiled output,
23+
error text that quotes content, and public links. Counts, ordering, and match locations are ordinary
24+
metadata; this boundary protects supported literal secret values, not arbitrary inference. Intentional
25+
human uploads remain ordinary input and are not scanned.
26+
27+
Only Copilot principals (`delegated` / `organization_delegated` with `serviceId: 'copilot'`) are
28+
classified. Every other caller is admitted without a provenance read, so the UI, API keys, MCP and
29+
the workflow executor see byte-identical results.
30+
31+
## The pieces
32+
33+
- `lib/core/application/content-delivery.ts`: the primitive.
34+
- `admitContent(principal, sources)` returns per-item verdicts. `requireAdmittedContent` is the
35+
single-item form and throws `ContentRefusedError` (403).
36+
- `admitWrittenContent` is for a committed write that returns content. A refusal or gate failure
37+
does not undo the write: audit and effects run, then the operation answers with the refusal.
38+
Settled tool results use the same deferral, but do not claim that a mutation was saved.
39+
- A source is `{ kind, id, provenance, view }`. `view` is `'complete'` (text whose every entry
40+
may appear), `{ derived: value }` (only entries occurring in `value`), `'opaque'` (bytes no
41+
redaction reaches: binary, base64, archives, images, public links), or `'excerpt'` (text already
42+
cut before admission, where a secret fragment cannot be redacted). Both gates withhold an
43+
excerpt with known secret entries; raw opaque bytes may still enter the workbench with history.
44+
- `redactBeforeCutting` projects complete text before selecting a line window. The session gate
45+
creates the existing redactor from the operation's bounded evidence only when a cut needs it;
46+
ordinary byte reads do not decrypt a second time or reload history.
47+
- Gates, installed by the surface: the **registry gate** (Chat tools, CLI, mounts) imports entries
48+
into the registry egress projects with; the **session gate** (run_code sandbox proxy) records
49+
them into the workbench history. Neither admits `unknown`, with one exception: the CLI's direct
50+
single-file download (`files get`) admits it under `unknown: 'taint'`, which latches the call's
51+
registry so the whole output is withheld and the workbench copy stays unknown. It needs a
52+
registry to latch; without one it refuses. One refused item never latches the shared registry.
53+
- `observeContentOutsideModel` reads for Chat without classifying, for content that never reaches
54+
the model (the user's own edit preview; a derived file that carries its sources' provenance).
55+
Name why at each use.
56+
57+
## Rules
58+
59+
1. **Every operation that admits Copilot declares `contentDelivery`**: `'sourced'` when its result
60+
can carry content from a store, `'none'` otherwise. Definition throws
61+
without it, and `copilot-route-inventory.test.ts` lists every Chat-reachable route without one.
62+
2. **A `sourced` use case accounts for what it returns**, before it loads content: call
63+
`admitContent` (with `[]` when this call returns none). Otherwise Chat gets
64+
`ContentDeliveryUnaccountedError`, and the transports turn the response into a 403.
65+
3. **Classify before loading bytes**, and exclude refused items per item. Withhold a whole result
66+
only when its items cannot be separated (one file, one render, one compile).
67+
4. **A new `sourced` route needs a case in `lib/mothership/__integration__/content-delivery.integration.ts`.**
68+
`copilot-content-routes.json` lists them; the suite fails for a listed route it does not drive.
69+
Refresh the list with `vitest run lib/api/server/routes/copilot-route-inventory.test.ts --update=all`.
70+
5. **Do not reach a store's bytes around a use case for Chat.** A helper that reads content for
71+
Chat goes through the gated use case, or names `observeContentOutsideModel` and why.
72+
73+
## Policy
74+
75+
U = `unknown`, E = exact with secret entries, R/C = unrecorded or clean. Each item is decided on its
76+
own unless noted.
77+
78+
| Path | U | E | R / C |
79+
|---|---|---|---|
80+
| File download (CLI `files get`) | Delivered once; invocation withheld from the model; workbench copy stays unknown | Redacted | Delivered |
81+
| File download (run_code proxy) | Refused before bytes enter workbench history | Recorded in workbench history | Delivered |
82+
| Requested mounts | Refused before code executes, listed in `excludedInputs`; later calls remain usable | Recorded in workbench history | Delivered |
83+
| File text, version text, `files read`, grep | 403 | Redacted (grep redacts before clipping) | Delivered |
84+
| Bulk download zip | Left out, reported in the archive manifest | Left out (opaque), reported in the archive manifest | Included |
85+
| `files search` | Match location kept; clipped preview withheld | Same; read/grep provides safely redacted text | Returned |
86+
| File edit (`PATCH` content) | 403 before reading | Diagnostics redacted | Unchanged |
87+
| Chat public share activation, and seeing an existing link (any surface, run_code included) | 403 | 403 (a link leaves every gate) | Shares |
88+
| Rendered view, streamed image, inline attachments | Refused | Refused (opaque) | Delivered |
89+
| Doc compile | Actual file read refused | Evidence imported before broker bytes/errors return | Compiles, including dynamic file references |
90+
| Run file download | 403 | Redacted | Delivered |
91+
| Run read / execute / resume `base64` | `base64` removed, descriptor kept | Same | Inlined (sync runs only) |
92+
| KB chunks, search, documents, tags | Item dropped (single read: 403) | Redacted | Delivered |
93+
| KB export | Stream fails before refused source bytes; no successful archive receipt | Refused (opaque) | Streams originals/chunks with admission at each source |
94+
| Table export download | Existing unsupported Chat operation | | |
95+
| Table rows, query, read, `table_selection` | Row dropped (single read: 403) | Redacted | Delivered |
96+
| Row search | Locations returned without cell values | Same | Returned |
97+
| Table import from a file | Rows stored unknown | Per-cell entries; a row whose coercion rewrote a secret, or whose raw cells cannot be checked, is stored unknown | Exact-empty, as before |
98+
| `tools execute` | Settled result withheld, with no automatic retry | Named masking through the existing registry | Returned, subject to existing tool authorization |
99+
100+
Search pagination and aggregate totals retain their database meaning even when an item or preview is
101+
withheld. Do not scan unrelated table rows or whole document collections to conceal counts.
102+
A refused requested mount stops code before it runs, avoiding an older file at the same sandbox path.
103+
Registry admission commits only when the complete item fits the existing capacity; a refused item
104+
must not consume capacity or permanently disable the parent registry.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
description: "How stored content (workspace files, run files, knowledge, table rows) reaches Chat's model, and the per-path policy. Read before changing a use case, route or Chat surface that returns such content."
3+
globs: ["apps/sim/lib/core/application/**","apps/sim/lib/mothership/content-delivery/**","apps/sim/lib/workspace-files/application/**","apps/sim/lib/knowledge/application/**","apps/sim/lib/table/application/**","apps/sim/lib/workflows/application/**","apps/sim/lib/mothership/chat/application/**","apps/sim/lib/**/application/operations.ts","apps/sim/lib/mothership/agent-cli/**","apps/sim/lib/mothership/tools/**","apps/sim/app/api/v2/**"]
4+
---
5+
6+
<!-- Generated from .claude/rules/sim-content-delivery.md by `bun run skills:sync`. Edit the source, not this file. -->
7+
8+
# Sim Content Delivery to Chat
9+
10+
Content derived from a provenance-bearing store reaches Chat's model only after **each item** it
11+
came from is admitted by the gate of the surface it leaves through. The stores are workspace files
12+
(and their versions), run files, knowledge documents and chunks, and table rows. "Derived" covers
13+
bytes, text, excerpts, chunks, cells, previews, base64, archives, rendered or compiled output,
14+
error text that quotes content, and public links. Counts, ordering, and match locations are ordinary
15+
metadata; this boundary protects supported literal secret values, not arbitrary inference. Intentional
16+
human uploads remain ordinary input and are not scanned.
17+
18+
Only Copilot principals (`delegated` / `organization_delegated` with `serviceId: 'copilot'`) are
19+
classified. Every other caller is admitted without a provenance read, so the UI, API keys, MCP and
20+
the workflow executor see byte-identical results.
21+
22+
## The pieces
23+
24+
- `lib/core/application/content-delivery.ts`: the primitive.
25+
- `admitContent(principal, sources)` returns per-item verdicts. `requireAdmittedContent` is the
26+
single-item form and throws `ContentRefusedError` (403).
27+
- `admitWrittenContent` is for a committed write that returns content. A refusal or gate failure
28+
does not undo the write: audit and effects run, then the operation answers with the refusal.
29+
Settled tool results use the same deferral, but do not claim that a mutation was saved.
30+
- A source is `{ kind, id, provenance, view }`. `view` is `'complete'` (text whose every entry
31+
may appear), `{ derived: value }` (only entries occurring in `value`), `'opaque'` (bytes no
32+
redaction reaches: binary, base64, archives, images, public links), or `'excerpt'` (text already
33+
cut before admission, where a secret fragment cannot be redacted). Both gates withhold an
34+
excerpt with known secret entries; raw opaque bytes may still enter the workbench with history.
35+
- `redactBeforeCutting` projects complete text before selecting a line window. The session gate
36+
creates the existing redactor from the operation's bounded evidence only when a cut needs it;
37+
ordinary byte reads do not decrypt a second time or reload history.
38+
- Gates, installed by the surface: the **registry gate** (Chat tools, CLI, mounts) imports entries
39+
into the registry egress projects with; the **session gate** (run_code sandbox proxy) records
40+
them into the workbench history. Neither admits `unknown`, with one exception: the CLI's direct
41+
single-file download (`files get`) admits it under `unknown: 'taint'`, which latches the call's
42+
registry so the whole output is withheld and the workbench copy stays unknown. It needs a
43+
registry to latch; without one it refuses. One refused item never latches the shared registry.
44+
- `observeContentOutsideModel` reads for Chat without classifying, for content that never reaches
45+
the model (the user's own edit preview; a derived file that carries its sources' provenance).
46+
Name why at each use.
47+
48+
## Rules
49+
50+
1. **Every operation that admits Copilot declares `contentDelivery`**: `'sourced'` when its result
51+
can carry content from a store, `'none'` otherwise. Definition throws
52+
without it, and `copilot-route-inventory.test.ts` lists every Chat-reachable route without one.
53+
2. **A `sourced` use case accounts for what it returns**, before it loads content: call
54+
`admitContent` (with `[]` when this call returns none). Otherwise Chat gets
55+
`ContentDeliveryUnaccountedError`, and the transports turn the response into a 403.
56+
3. **Classify before loading bytes**, and exclude refused items per item. Withhold a whole result
57+
only when its items cannot be separated (one file, one render, one compile).
58+
4. **A new `sourced` route needs a case in `lib/mothership/__integration__/content-delivery.integration.ts`.**
59+
`copilot-content-routes.json` lists them; the suite fails for a listed route it does not drive.
60+
Refresh the list with `vitest run lib/api/server/routes/copilot-route-inventory.test.ts --update=all`.
61+
5. **Do not reach a store's bytes around a use case for Chat.** A helper that reads content for
62+
Chat goes through the gated use case, or names `observeContentOutsideModel` and why.
63+
64+
## Policy
65+
66+
U = `unknown`, E = exact with secret entries, R/C = unrecorded or clean. Each item is decided on its
67+
own unless noted.
68+
69+
| Path | U | E | R / C |
70+
|---|---|---|---|
71+
| File download (CLI `files get`) | Delivered once; invocation withheld from the model; workbench copy stays unknown | Redacted | Delivered |
72+
| File download (run_code proxy) | Refused before bytes enter workbench history | Recorded in workbench history | Delivered |
73+
| Requested mounts | Refused before code executes, listed in `excludedInputs`; later calls remain usable | Recorded in workbench history | Delivered |
74+
| File text, version text, `files read`, grep | 403 | Redacted (grep redacts before clipping) | Delivered |
75+
| Bulk download zip | Left out, reported in the archive manifest | Left out (opaque), reported in the archive manifest | Included |
76+
| `files search` | Match location kept; clipped preview withheld | Same; read/grep provides safely redacted text | Returned |
77+
| File edit (`PATCH` content) | 403 before reading | Diagnostics redacted | Unchanged |
78+
| Chat public share activation, and seeing an existing link (any surface, run_code included) | 403 | 403 (a link leaves every gate) | Shares |
79+
| Rendered view, streamed image, inline attachments | Refused | Refused (opaque) | Delivered |
80+
| Doc compile | Actual file read refused | Evidence imported before broker bytes/errors return | Compiles, including dynamic file references |
81+
| Run file download | 403 | Redacted | Delivered |
82+
| Run read / execute / resume `base64` | `base64` removed, descriptor kept | Same | Inlined (sync runs only) |
83+
| KB chunks, search, documents, tags | Item dropped (single read: 403) | Redacted | Delivered |
84+
| KB export | Stream fails before refused source bytes; no successful archive receipt | Refused (opaque) | Streams originals/chunks with admission at each source |
85+
| Table export download | Existing unsupported Chat operation | | |
86+
| Table rows, query, read, `table_selection` | Row dropped (single read: 403) | Redacted | Delivered |
87+
| Row search | Locations returned without cell values | Same | Returned |
88+
| Table import from a file | Rows stored unknown | Per-cell entries; a row whose coercion rewrote a secret, or whose raw cells cannot be checked, is stored unknown | Exact-empty, as before |
89+
| `tools execute` | Settled result withheld, with no automatic retry | Named masking through the existing registry | Returned, subject to existing tool authorization |
90+
91+
Search pagination and aggregate totals retain their database meaning even when an item or preview is
92+
withheld. Do not scan unrelated table rows or whole document collections to conceal counts.
93+
A refused requested mount stops code before it runs, avoiding an older file at the same sandbox path.
94+
Registry admission commits only when the complete item fits the existing capacity; a refused item
95+
must not consume capacity or permanently disable the parent registry.

‎apps/sim/AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Applies to `apps/sim/**` on top of the root [AGENTS.md](/AGENTS.md), which holds
99
- Styling and emcn chrome, UI copy, settings pages: `sim-styling.md`, `sim-ui-copy.md`, `sim-settings-pages.md`
1010
- URL state, list and menu order, caching: `sim-url-state.md`, `sim-list-ordering.md`, `sim-caching.md`
1111
- Tools, blocks, triggers: `sim-integrations.md`; isolated-vm sandbox worker: `sim-sandbox.md`
12+
- Anything that returns stored file, run-file, knowledge or table content to Chat: `sim-content-delivery.md`
1213
- Tests: `sim-testing.md` and the `test-audit` skill
1314
- Landing pages: `app/(landing)/CLAUDE.md`, `landing-seo-geo.md`, `constitution.md` (product language)
1415

‎apps/sim/app/api/v2/files/bulk-download/route.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ describe('GET /api/v2/files/bulk-download', () => {
7676
folderPaths: new Map<string, string>(),
7777
renderedDocuments: new Map<string, Buffer>(),
7878
declaredBytes: 6,
79+
excludedFileIds: [],
7980
})
8081
})
8182

‎apps/sim/app/api/v2/files/bulk-download/route.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,16 @@ export const GET = defineV2BinaryRoute({
5454
folderPaths: query.folderPaths,
5555
}),
5656
useCase: downloadWorkspaceFileItems,
57-
present: ({ filesToZip, folderPaths, renderedDocuments }) => {
58-
const entryPaths = buildZipEntryPaths(
59-
filesToZip.map((file) => ({
57+
present: ({ filesToZip, folderPaths, renderedDocuments, excludedFileIds }) => {
58+
const report = excludedFileIds.length > 0 ? { partial: true, excludedFileIds } : undefined
59+
const entryPaths = buildZipEntryPaths([
60+
...filesToZip.map((file) => ({
6061
name: file.name,
6162
folderPath: file.folderId ? folderPaths.get(file.folderId) : null,
6263
contentType: file.type,
63-
}))
64-
)
64+
})),
65+
...(report ? [{ name: 'sim-download-report.json', contentType: 'application/json' }] : []),
66+
])
6567
const archive = new ZipArchive({ store: true })
6668
archive.on('warning', (error: Error) => {
6769
logger.warn('Archive warning while streaming workspace files', { error })
@@ -71,6 +73,9 @@ export const GET = defineV2BinaryRoute({
7173
name: entryPaths[index],
7274
})
7375
})
76+
if (report) {
77+
archive.append(JSON.stringify(report, null, 2), { name: entryPaths[filesToZip.length] })
78+
}
7479
archive.finalize().catch((error) => {
7580
logger.error('Failed to finalize workspace file archive', { error })
7681
})

‎apps/sim/app/api/v2/lib/response.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { type CursorKey, INVALID_CURSOR_MESSAGE } from '@/lib/api/list-query'
1010
import { getValidationErrorMessage, serializeZodIssues } from '@/lib/api/server'
1111
import { ADMISSION_RETRY_AFTER_SECONDS } from '@/lib/core/admission/transient-failure'
1212
import {
13+
ContentDeliveryUnobservedError,
1314
forbiddenErrorDetails,
1415
InsufficientScopeError,
1516
OAuthAccessTokenExpiredError,
@@ -530,6 +531,10 @@ export function v2ErrorForOrchestration(
530531
* one its failure class maps to.
531532
*/
532533
export function v2CaughtOrchestrationError(error: unknown): NextResponse | null {
534+
/** Chat reached stored content outside any classifying surface: transient, never a content leak. */
535+
if (error instanceof ContentDeliveryUnobservedError) {
536+
return v2Error('SERVICE_UNAVAILABLE', error.message)
537+
}
533538
const classified = asOrchestrationError(error)
534539
if (!classified) return null
535540
if (classified instanceof InsufficientScopeError) return v2InsufficientScope(classified)

‎apps/sim/ee/access-requests/lib/application/operations.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ function defineAccessRequestOperation(
5151
* permission-group-exempt: requests never grant a withheld capability without administrator review.
5252
*/
5353
capability: 'none',
54+
contentDelivery: 'none',
5455
})
5556
return Object.freeze({ ...workspaceOperation, organizationOperation, admin })
5657
}

0 commit comments

Comments
 (0)