Skip to content

Commit 3bb1031

Browse files
committed
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.
1 parent 875a3fb commit 3bb1031

195 files changed

Lines changed: 6412 additions & 2012 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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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/content-delivery.ts"
5+
- "apps/sim/lib/mothership/content-delivery/**"
6+
- "apps/sim/lib/**/application/admit-*.ts"
7+
- "apps/sim/lib/**/application/operations.ts"
8+
- "apps/sim/lib/mothership/agent-cli/**"
9+
- "apps/sim/lib/mothership/tools/sandbox-resource-transport.ts"
10+
---
11+
12+
# Sim Content Delivery to Chat
13+
14+
Content derived from a provenance-bearing store reaches Chat's model only after **each item** it
15+
came from is admitted by the gate of the surface it leaves through. The stores are workspace files
16+
(and their versions), run files, knowledge documents and chunks, and table rows. "Derived" covers
17+
bytes, text, excerpts, chunks, cells, previews, base64, archives, rendered or compiled output,
18+
error text that quotes content, public links, and match results.
19+
20+
Only Copilot principals (`delegated` / `organization_delegated` with `serviceId: 'copilot'`) are
21+
classified. Every other caller is admitted without a provenance read, so the UI, API keys, MCP and
22+
the workflow executor see byte-identical results.
23+
24+
## The pieces
25+
26+
- `lib/core/application/content-delivery.ts`: the primitive.
27+
- `admitContent(principal, sources)` returns per-item verdicts. `requireAdmittedContent` is the
28+
single-item form and throws `ContentRefusedError` (403).
29+
- `admitWrittenContent` is for a committed write that returns content. A refusal or gate failure
30+
does not undo the write: audit and effects run, then the operation answers with the refusal.
31+
- A source is `{ kind, id, provenance, view }`. `view` is `'complete'` (text whose every entry
32+
may appear), `{ derived: value }` (only entries occurring in `value`), or `'opaque'` (bytes no
33+
redaction reaches: binary, base64, archives, images, public links).
34+
- Gates, installed by the surface: the **registry gate** (Chat tools, CLI, mounts) imports entries
35+
into the registry egress projects with; the **session gate** (run_code sandbox proxy) records
36+
them into the workbench history. Neither ever admits `unknown`, and one refused item never
37+
latches the shared registry.
38+
- `observeContentOutsideModel` reads for Chat without classifying, for content that never reaches
39+
the model (the user's own edit preview; a derived file that carries its sources' provenance).
40+
Name why at each use.
41+
42+
## Rules
43+
44+
1. **Every operation that admits Copilot declares `contentDelivery`**: `'sourced'` when its result
45+
can carry content from a store (including a match oracle), `'none'` otherwise. Definition throws
46+
without it, and `copilot-route-inventory.test.ts` lists every Chat-reachable route without one.
47+
2. **A `sourced` use case accounts for what it returns**, before it loads content: call
48+
`admitContent` (with `[]` when this call returns none). Otherwise Chat gets
49+
`ContentDeliveryUnaccountedError`, and the transports turn the response into a 403.
50+
3. **Classify before loading bytes**, and exclude refused items per item. Withhold a whole result
51+
only when its items cannot be separated (one file, one render, one compile).
52+
4. **A new `sourced` route needs a case in `lib/mothership/__integration__/content-delivery.integration.ts`.**
53+
`copilot-content-routes.json` lists them; the suite fails for a listed route it does not drive.
54+
Refresh the list with `vitest run -u lib/api/server/routes/copilot-route-inventory.test.ts`.
55+
5. **Do not reach a store's bytes around a use case for Chat.** A helper that reads content for
56+
Chat goes through the gated use case, or names `observeContentOutsideModel` and why.
57+
58+
## Policy
59+
60+
U = `unknown`, E = exact with secret entries, R/C = unrecorded or clean. Each item is decided on its
61+
own unless noted.
62+
63+
| Path | U | E | R / C |
64+
|---|---|---|---|
65+
| File download (CLI `files get`) | Delivered once; invocation withheld from the model; workbench copy stays unknown | Redacted | Delivered |
66+
| File download (run_code proxy) and mounts | Refused, not recorded, listed in `excludedInputs` | Recorded in workbench history | Delivered |
67+
| File text, version text, `files read`, grep | 403 | Redacted (grep redacts before clipping) | Delivered |
68+
| Bulk download zip | Left out | Left out (opaque) | Included |
69+
| `files search` | Results dropped | Results dropped (a match is an oracle) | Returned |
70+
| File edit (`PATCH` content) | 403 before reading | Diagnostics redacted | Unchanged |
71+
| Chat public share activation | 403 | 403 (a link is opaque) | Shares |
72+
| Rendered view, streamed image, inline attachments | Refused | Refused (opaque) | Delivered |
73+
| Doc compile | Compile refused | Error text redacted | Compiles |
74+
| Run file download | 403 | Redacted | Delivered |
75+
| Run read / execute / resume `base64` | `base64` removed, descriptor kept | Same | Inlined (sync runs only) |
76+
| KB chunks, search, documents, tags | Item dropped (single read: 403) | Redacted | Delivered |
77+
| KB export, table export download | Not available to Chat | | |
78+
| Table rows, query, read, `table_selection` | Row dropped (single read: 403) | Redacted | Delivered |
79+
| Row search | Match dropped | Match in a secret column dropped | Returned |
80+
| Table import from a file | Rows stored unknown | Per-cell entries | Exact-empty, as before |
81+
| `tools execute` for file, knowledge, table, memory, function, custom, MCP tools | Not available to Chat | | |
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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/content-delivery.ts","apps/sim/lib/mothership/content-delivery/**","apps/sim/lib/**/application/admit-*.ts","apps/sim/lib/**/application/operations.ts","apps/sim/lib/mothership/agent-cli/**","apps/sim/lib/mothership/tools/sandbox-resource-transport.ts"]
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, public links, and match results.
15+
16+
Only Copilot principals (`delegated` / `organization_delegated` with `serviceId: 'copilot'`) are
17+
classified. Every other caller is admitted without a provenance read, so the UI, API keys, MCP and
18+
the workflow executor see byte-identical results.
19+
20+
## The pieces
21+
22+
- `lib/core/application/content-delivery.ts`: the primitive.
23+
- `admitContent(principal, sources)` returns per-item verdicts. `requireAdmittedContent` is the
24+
single-item form and throws `ContentRefusedError` (403).
25+
- `admitWrittenContent` is for a committed write that returns content. A refusal or gate failure
26+
does not undo the write: audit and effects run, then the operation answers with the refusal.
27+
- A source is `{ kind, id, provenance, view }`. `view` is `'complete'` (text whose every entry
28+
may appear), `{ derived: value }` (only entries occurring in `value`), or `'opaque'` (bytes no
29+
redaction reaches: binary, base64, archives, images, public links).
30+
- Gates, installed by the surface: the **registry gate** (Chat tools, CLI, mounts) imports entries
31+
into the registry egress projects with; the **session gate** (run_code sandbox proxy) records
32+
them into the workbench history. Neither ever admits `unknown`, and one refused item never
33+
latches the shared registry.
34+
- `observeContentOutsideModel` reads for Chat without classifying, for content that never reaches
35+
the model (the user's own edit preview; a derived file that carries its sources' provenance).
36+
Name why at each use.
37+
38+
## Rules
39+
40+
1. **Every operation that admits Copilot declares `contentDelivery`**: `'sourced'` when its result
41+
can carry content from a store (including a match oracle), `'none'` otherwise. Definition throws
42+
without it, and `copilot-route-inventory.test.ts` lists every Chat-reachable route without one.
43+
2. **A `sourced` use case accounts for what it returns**, before it loads content: call
44+
`admitContent` (with `[]` when this call returns none). Otherwise Chat gets
45+
`ContentDeliveryUnaccountedError`, and the transports turn the response into a 403.
46+
3. **Classify before loading bytes**, and exclude refused items per item. Withhold a whole result
47+
only when its items cannot be separated (one file, one render, one compile).
48+
4. **A new `sourced` route needs a case in `lib/mothership/__integration__/content-delivery.integration.ts`.**
49+
`copilot-content-routes.json` lists them; the suite fails for a listed route it does not drive.
50+
Refresh the list with `vitest run -u lib/api/server/routes/copilot-route-inventory.test.ts`.
51+
5. **Do not reach a store's bytes around a use case for Chat.** A helper that reads content for
52+
Chat goes through the gated use case, or names `observeContentOutsideModel` and why.
53+
54+
## Policy
55+
56+
U = `unknown`, E = exact with secret entries, R/C = unrecorded or clean. Each item is decided on its
57+
own unless noted.
58+
59+
| Path | U | E | R / C |
60+
|---|---|---|---|
61+
| File download (CLI `files get`) | Delivered once; invocation withheld from the model; workbench copy stays unknown | Redacted | Delivered |
62+
| File download (run_code proxy) and mounts | Refused, not recorded, listed in `excludedInputs` | Recorded in workbench history | Delivered |
63+
| File text, version text, `files read`, grep | 403 | Redacted (grep redacts before clipping) | Delivered |
64+
| Bulk download zip | Left out | Left out (opaque) | Included |
65+
| `files search` | Results dropped | Results dropped (a match is an oracle) | Returned |
66+
| File edit (`PATCH` content) | 403 before reading | Diagnostics redacted | Unchanged |
67+
| Chat public share activation | 403 | 403 (a link is opaque) | Shares |
68+
| Rendered view, streamed image, inline attachments | Refused | Refused (opaque) | Delivered |
69+
| Doc compile | Compile refused | Error text redacted | Compiles |
70+
| Run file download | 403 | Redacted | Delivered |
71+
| Run read / execute / resume `base64` | `base64` removed, descriptor kept | Same | Inlined (sync runs only) |
72+
| KB chunks, search, documents, tags | Item dropped (single read: 403) | Redacted | Delivered |
73+
| KB export, table export download | Not available to Chat | | |
74+
| Table rows, query, read, `table_selection` | Row dropped (single read: 403) | Redacted | Delivered |
75+
| Row search | Match dropped | Match in a secret column dropped | Returned |
76+
| Table import from a file | Rows stored unknown | Per-cell entries | Exact-empty, as before |
77+
| `tools execute` for file, knowledge, table, memory, function, custom, MCP tools | Not available to Chat | | |

‎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/lib/response.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
InsufficientScopeError,
1515
OAuthAccessTokenExpiredError,
1616
} from '@/lib/core/application'
17+
import { ContentDeliveryUnobservedError } from '@/lib/core/application/content-delivery'
1718
import {
1819
asOrchestrationError,
1920
OrchestrationError,
@@ -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
}

‎apps/sim/ee/workspace-forking/application/operations.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const forkOperations = {
1414
discover: defineWorkspaceOperation({
1515
...adminPolicy,
1616
capability: 'none',
17+
contentDelivery: 'none',
1718
id: 'workspaces.fork.discover',
1819
oauthScope: 'api:read',
1920
}),
@@ -23,6 +24,7 @@ export const forkOperations = {
2324
preview: defineWorkspaceOperation({
2425
...adminPolicy,
2526
capability: 'none',
27+
contentDelivery: 'none',
2628
id: 'workspaces.fork.preview',
2729
oauthScope: 'api:write',
2830
}),
@@ -32,6 +34,7 @@ export const forkOperations = {
3234
create: defineWorkspaceOperation({
3335
...adminPolicy,
3436
capability: 'none',
37+
contentDelivery: 'none',
3538
id: 'workspaces.fork.create',
3639
oauthScope: 'api:write',
3740
}),
@@ -41,6 +44,7 @@ export const forkOperations = {
4144
syncPreview: defineWorkspaceOperation({
4245
...adminPolicy,
4346
capability: 'none',
47+
contentDelivery: 'none',
4448
id: 'workspaces.fork.sync.preview',
4549
oauthScope: 'api:write',
4650
}),
@@ -50,6 +54,7 @@ export const forkOperations = {
5054
sync: defineWorkspaceOperation({
5155
...adminPolicy,
5256
capability: 'none',
57+
contentDelivery: 'none',
5358
id: 'workspaces.fork.sync',
5459
oauthScope: 'api:write',
5560
}),
@@ -59,6 +64,7 @@ export const forkOperations = {
5964
mappingsRead: defineWorkspaceOperation({
6065
...adminPolicy,
6166
capability: 'none',
67+
contentDelivery: 'none',
6268
id: 'workspaces.fork.mappings.read',
6369
oauthScope: 'api:read',
6470
}),
@@ -68,6 +74,7 @@ export const forkOperations = {
6874
mappingsUpdate: defineWorkspaceOperation({
6975
...adminPolicy,
7076
capability: 'none',
77+
contentDelivery: 'none',
7178
id: 'workspaces.fork.mappings.update',
7279
oauthScope: 'api:write',
7380
}),
@@ -77,6 +84,7 @@ export const forkOperations = {
7784
rollback: defineWorkspaceOperation({
7885
...adminPolicy,
7986
capability: 'none',
87+
contentDelivery: 'none',
8088
id: 'workspaces.fork.rollback',
8189
oauthScope: 'api:write',
8290
}),
@@ -86,6 +94,7 @@ export const forkOperations = {
8694
unlink: defineWorkspaceOperation({
8795
...adminPolicy,
8896
capability: 'none',
97+
contentDelivery: 'none',
8998
id: 'workspaces.fork.unlink',
9099
oauthScope: 'api:write',
91100
}),
@@ -95,6 +104,7 @@ export const forkOperations = {
95104
exclusions: defineWorkspaceOperation({
96105
...adminPolicy,
97106
capability: 'none',
107+
contentDelivery: 'none',
98108
id: 'workspaces.fork.exclusions',
99109
oauthScope: 'api:write',
100110
}),
@@ -107,6 +117,7 @@ export const forkOperations = {
107117
syncDefault: defineWorkspaceOperation({
108118
...adminPolicy,
109119
capability: 'none',
120+
contentDelivery: 'none',
110121
id: 'workspaces.fork.sync_default',
111122
oauthScope: 'api:write',
112123
}),

‎apps/sim/lib/api-key/application/operations.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const apiKeyOperations = {
3333
minimumRole: 'admin',
3434
workspaceApiKey: 'deny',
3535
capability: 'api_keys.manage',
36+
contentDelivery: 'none',
3637
principalKinds: ['delegated'],
3738
delegatedServices: ['copilot'],
3839
}),
@@ -45,6 +46,7 @@ export const byokKeyOperations = {
4546
minimumRole: 'read',
4647
workspaceApiKey: 'deny',
4748
capability: 'none',
49+
contentDelivery: 'none',
4850
principalKinds: ['session', 'delegated'],
4951
delegatedServices: ['copilot'],
5052
}),
@@ -54,12 +56,14 @@ export const byokKeyOperations = {
5456
minimumRole: 'admin',
5557
workspaceApiKey: 'deny',
5658
capability: 'none',
59+
contentDelivery: 'none',
5760
principalKinds: ['session', 'delegated'],
5861
delegatedServices: ['copilot'],
5962
}),
6063
// permission-group-exempt: BYOK is its own entitlement-gated section, and api_keys.manage names the Sim API Keys tab instead
6164
listOrganization: defineOrganizationByokOperation({
6265
id: 'byok_keys.organization.list',
66+
contentDelivery: 'none',
6367
capability: 'none',
6468
authority: 'organization_admin',
6569
minimumRole: 'admin',
@@ -84,6 +88,7 @@ export const byokKeyOperations = {
8488
// permission-group-exempt: BYOK is its own entitlement-gated section, and api_keys.manage names the Sim API Keys tab instead
8589
deleteOrganization: defineOrganizationByokOperation({
8690
id: 'byok_keys.organization.delete',
91+
contentDelivery: 'none',
8792
capability: 'none',
8893
authority: 'organization_admin',
8994
minimumRole: 'admin',
@@ -100,6 +105,7 @@ export const byokKeyOperations = {
100105
minimumRole: 'read',
101106
workspaceApiKey: 'deny',
102107
capability: 'none',
108+
contentDelivery: 'none',
103109
principalKinds: ['session', 'delegated'],
104110
delegatedServices: ['copilot'],
105111
}),

‎apps/sim/lib/api-key/application/workspace-api-keys.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,22 @@ export const workspaceApiKeyOperations = {
3131
id: 'api_keys.workspace.list',
3232
minimumRole: 'read',
3333
capability: 'api_keys.manage',
34+
contentDelivery: 'none',
3435
}),
3536
rename: defineWorkspaceOperation({
3637
...principalPolicy,
3738
id: 'api_keys.workspace.rename',
3839
minimumRole: 'admin',
3940
capability: 'api_keys.manage',
41+
contentDelivery: 'none',
4042
}),
4143
/** permission-group-exempt: admins must retain the ability to revoke exposed credentials. */
4244
revoke: defineWorkspaceOperation({
4345
...principalPolicy,
4446
id: 'api_keys.workspace.revoke',
4547
minimumRole: 'admin',
4648
capability: 'none',
49+
contentDelivery: 'none',
4750
}),
4851
} as const
4952
const authorizationOptions = { delegation: { audience: 'sim:settings', isWithinScope: () => true } }

0 commit comments

Comments
 (0)