Skip to content

Commit e919e4c

Browse files
authored
fix(mothership): follow-ups for v1.0.0 regressions (#8215)
* fix(execution): resolve stored file references only for workspace members Workflow file inputs can reference stored files by id, key, or internal URL, and every resolved file is added to the run's explicit file grants. Any caller could use this, so an anonymous public-API, public MCP, chat, or webhook caller could pull another workflow's run files or workspace files into a run and receive a presigned URL for them. Execution now derives a stored-file reference scope from the run's principal: authorized member principals (session, personal/workspace API key, OAuth token, delegated) resolve workspace-wide; system principals may only reference files already stored under the current execution, which keeps chat and webhook uploads working. The default is the restricted scope, so a new caller fails closed. Input-format defaults are workflow-authored and still resolve workspace-wide. Outside workspace scope, an upload whose URL is an internal file URL is resolved as the stored reference it names rather than downloaded with the run user's access, and a key from another execution is refused before it is looked up, so the refusal does not reveal whether the file exists. * fix(mothership): withhold unregistered table secrets from sim_cli results Table rows read through sim_cli reached the model without their persisted secret provenance, so stored secrets in cells were never redacted. Row use cases now report the provenance of the rows they return to an observing transport (mirroring the workspace-file delivery observer), and the agent CLI table transport imports it into the tool call's registry, answers 503 without a registry, and marks the registry incomplete when a row-bearing table route returns without reporting provenance. Provenance reported by detached work after the call settles is ignored. Export download links are refused outright: a signed link to the whole table as plaintext CSV cannot carry provenance once fetched. Run-state and enrichment error text (runState.error, blockErrors, enrichment provider errors) is captured from executor output without its secret provenance, so a read that returns any of it is withheld as well; reads whose run state carries no error text are unaffected. * fix(search): run live Calendar service search on the crawl scope PR #8208 widened Google Calendar's domain-wide delegation scopes to include calendar.readonly for every service-account token. Google refuses the whole token exchange when any requested scope is not authorized for the client, so indexed admin-mode Calendar syncs configured for calendar.events.readonly failed every run and were eventually disabled. Live service search only needed the wider scope to read an all-day event's calendar time zone. events.list returns the calendar's timeZone under calendar.events.readonly, so live search now reads it there and delegates with the same single scope as the indexed crawl, matching Drive and Gmail. The admin calendar picker keeps its own calendar.readonly request, as documented. * fix(search): keep results painted when Show more widens the page * fix(chat): use the defined brand token for the resource activity dot * fix(knowledge): document connector auth in the v2 contract and reject $NAME secrets * fix(mothership): resolve Copilot env-reference passwords for chat deploy and file share Copilot now deploys chats and shares files through the v2 API, which stored a whole-value {{NAME}} password literally (or 400'd a short one on length). The application use cases now resolve the reference from the effective environment when, and only when, the caller is an admitted Copilot workspace invocation and the password will actually be stored (password mode; for a file share, only while enabling it), refuse an unset variable by name, and hold the resolved value to the password rules. Every other principal keeps literal semantics. The v2 password fields admit a whole-value reference below the password minimum as one refined string (not a union, which would make the CLI flag JSON-only), still capped at the password maximum; the use cases enforce the 15-character rule on the value actually stored. Connector API-key references share the same principal environment lookup, and the exact reference regex now lives in one module. Unlike the removed Copilot tool path, the resolved password is not recorded in a resolved-secret trace registry: the v2 use case has none, and the password is write-only and never echoed back.
1 parent 11cba52 commit e919e4c

58 files changed

Lines changed: 2083 additions & 168 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.

‎apps/docs/content/docs/cli/files.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ Enable or disable sharing for a file (OAuth login or personal API key required)
427427
| --- | --- | --- |
428428
| `--is-active <true\|false>` | Yes | Whether the share should resolve. Disabling preserves the token and the whole access configuration, so re-enabling restores the share as it was; enabling rewrites the credentials the resulting mode does not use. Accepted values: `true`, `false`. |
429429
| `--auth-type <value>` | No | How access to the share is gated. The stored mode is kept when omitted. Enabling `public` clears the stored password and empties `allowedEmails`; `password` empties `allowedEmails`; `email` and `sso` clear the stored password. Accepted values: `public`, `password`, `email`, `sso`. |
430-
| `--password <value>` | No | Password for a password-gated share. Kept when omitted; enabling `password` with neither a supplied nor a stored password is a 400. |
430+
| `--password <value>` | No | Password of 15 to 1024 characters for a password-gated share. Kept when omitted; enabling `password` with neither a supplied nor a stored password is a 400. Taken literally, except that a request from the Sim agent resolves a whole-value `&#123;&#123;ENV_VAR&#125;&#125;` reference to that variable before the rules apply. |
431431
| `--allowed-emails <value...>` | No | Allowed addresses or `@domain` patterns for email and SSO shares. Kept when omitted; enabling `email` or `sso` with an empty resulting list is a 400. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
432432

433433
</CommandTable>

‎apps/docs/content/docs/cli/knowledge.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,8 +657,8 @@ Create Knowledge Connector (OAuth login or personal API key required)
657657
| Option | Required | Description |
658658
| --- | --- | --- |
659659
| `--connector-type <value>` | Yes | Registered connector type. |
660-
| `--credential-id <value>` | No | OAuth credential identifier for connectors that require OAuth. |
661-
| `--api-key <value>` | No | Write-only API key for connectors that use API-key authentication. |
660+
| `--credential-id <value>` | No | OAuth credential identifier for connector types whose `auth.mode` is `oauth` (see connector types); omit it for `apiKey` connectors. |
661+
| `--api-key <value>` | No | Write-only API key for connector types whose `auth.mode` is `apiKey` (see connector types), or a personal access token for an OAuth connector that also accepts one, such as GitHub. Send it instead of `credentialId`. Pass a raw key, or a secret reference written as the whole value `&#123;&#123;SECRET_NAME&#125;&#125;`, which the server resolves; `$SECRET_NAME` is not a reference. |
662662
| `--source-config <json\|@file>` | Yes | Connector-specific source selection and filtering configuration. (JSON, or @path / @- to read a file or stdin). |
663663
| `--sync-interval-minutes <value>` | No | Scheduled synchronization interval in minutes; zero disables scheduling. |
664664

‎apps/docs/content/docs/cli/reference.mdx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,7 @@ sim files share set <fileId> [options]
11851185
| --- | --- | --- |
11861186
| `--is-active <true\|false>` | Yes | Whether the share should resolve. Disabling preserves the token and the whole access configuration, so re-enabling restores the share as it was; enabling rewrites the credentials the resulting mode does not use. Accepted values: `true`, `false`. |
11871187
| `--auth-type <value>` | No | How access to the share is gated. The stored mode is kept when omitted. Enabling `public` clears the stored password and empties `allowedEmails`; `password` empties `allowedEmails`; `email` and `sso` clear the stored password. Accepted values: `public`, `password`, `email`, `sso`. |
1188-
| `--password <value>` | No | Password for a password-gated share. Kept when omitted; enabling `password` with neither a supplied nor a stored password is a 400. |
1188+
| `--password <value>` | No | Password of 15 to 1024 characters for a password-gated share. Kept when omitted; enabling `password` with neither a supplied nor a stored password is a 400. Taken literally, except that a request from the Sim agent resolves a whole-value `&#123;&#123;ENV_VAR&#125;&#125;` reference to that variable before the rules apply. |
11891189
| `--allowed-emails <value...>` | No | Allowed addresses or `@domain` patterns for email and SSO shares. Kept when omitted; enabling `email` or `sso` with an empty resulting list is a 400. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
11901190

11911191
</CommandTable>
@@ -2162,8 +2162,8 @@ sim knowledge connectors create <knowledgeBaseId> [options]
21622162
| Option | Required | Description |
21632163
| --- | --- | --- |
21642164
| `--connector-type <value>` | Yes | Registered connector type. |
2165-
| `--credential-id <value>` | No | OAuth credential identifier for connectors that require OAuth. |
2166-
| `--api-key <value>` | No | Write-only API key for connectors that use API-key authentication. |
2165+
| `--credential-id <value>` | No | OAuth credential identifier for connector types whose `auth.mode` is `oauth` (see connector types); omit it for `apiKey` connectors. |
2166+
| `--api-key <value>` | No | Write-only API key for connector types whose `auth.mode` is `apiKey` (see connector types), or a personal access token for an OAuth connector that also accepts one, such as GitHub. Send it instead of `credentialId`. Pass a raw key, or a secret reference written as the whole value `&#123;&#123;SECRET_NAME&#125;&#125;`, which the server resolves; `$SECRET_NAME` is not a reference. |
21672167
| `--source-config <json\|@file>` | Yes | Connector-specific source selection and filtering configuration. (JSON, or @path / @- to read a file or stdin). |
21682168
| `--sync-interval-minutes <value>` | No | Scheduled synchronization interval in minutes; zero disables scheduling. |
21692169

@@ -6469,7 +6469,7 @@ sim workflows chat publish <workflowId> [options]
64696469
| `--description <value>` | No | Description shown to visitors. Omitted clears it. |
64706470
| `--customizations <json\|@file>` | No | Presentation overrides. Omitted fields take platform defaults. (JSON, or @path / @- to read a file or stdin). |
64716471
| `--auth-type <value>` | No | How visitors are gated. `public` leaves the chat open to anyone holding the URL. Accepted values: `public`, `password`, `email`, `sso`. |
6472-
| `--password <value>` | No | Write-only password. Required whenever `authType` is `password`, and rejected otherwise. Never readable back. |
6472+
| `--password <value>` | No | Write-only password of 15 to 1024 characters, not only whitespace. Required whenever `authType` is `password`, and rejected otherwise. Never readable back. Taken literally, except that a request from the Sim agent resolves a whole-value `&#123;&#123;ENV_VAR&#125;&#125;` reference to that variable before the rules apply. |
64736473
| `--allowed-emails <json\|@file>` | No | Email addresses or domains admitted under `email` and `sso` gating. At least one is required for those modes. (JSON, or @path / @- to read a file or stdin). |
64746474
| `--output-configs <json\|@file>` | No | Block outputs to surface to visitors. Omitted surfaces none. (JSON, or @path / @- to read a file or stdin). |
64756475
| `--include-thinking` | No | Allow visitors to receive provider thinking events. |

‎apps/docs/content/docs/cli/workflows.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ Publish or replace a workflow’s chat deployment (OAuth login or personal API k
441441
| `--description <value>` | No | Description shown to visitors. Omitted clears it. |
442442
| `--customizations <json\|@file>` | No | Presentation overrides. Omitted fields take platform defaults. (JSON, or @path / @- to read a file or stdin). |
443443
| `--auth-type <value>` | No | How visitors are gated. `public` leaves the chat open to anyone holding the URL. Accepted values: `public`, `password`, `email`, `sso`. |
444-
| `--password <value>` | No | Write-only password. Required whenever `authType` is `password`, and rejected otherwise. Never readable back. |
444+
| `--password <value>` | No | Write-only password of 15 to 1024 characters, not only whitespace. Required whenever `authType` is `password`, and rejected otherwise. Never readable back. Taken literally, except that a request from the Sim agent resolves a whole-value `&#123;&#123;ENV_VAR&#125;&#125;` reference to that variable before the rules apply. |
445445
| `--allowed-emails <json\|@file>` | No | Email addresses or domains admitted under `email` and `sso` gating. At least one is required for those modes. (JSON, or @path / @- to read a file or stdin). |
446446
| `--output-configs <json\|@file>` | No | Block outputs to surface to visitors. Omitted surfaces none. (JSON, or @path / @- to read a file or stdin). |
447447
| `--include-thinking` | No | Allow visitors to receive provider thinking events. |

‎apps/docs/content/docs/search/google-calendar.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ https://www.googleapis.com/auth/calendar.readonly
2222
https://www.googleapis.com/auth/admin.directory.user.readonly
2323
```
2424

25-
`calendar.readonly` is needed for calendar metadata and the CalendarList picker; event-read scope alone is insufficient. See [CalendarList authorization](https://developers.google.com/workspace/calendar/api/v3/reference/calendarList/list).
25+
Search reads events with `calendar.events.readonly`. The calendar picker also needs `calendar.readonly` to list the delegated administrator's calendars. See [CalendarList authorization](https://developers.google.com/workspace/calendar/api/v3/reference/calendarList/list).
2626

2727
3. In **Settings → Sources → Google Calendar**, choose **Service account**, then **Add connection**. Add or select the Google credential and set **Delegated administrator** to an active Workspace administrator with Directory user-read access.
2828
4. Select calendars, users, a date range, and optional event-text and attendee settings. Save and select this connection as the service source.
@@ -44,7 +44,7 @@ Sim does not copy the organizer's richer event details into an attendee's view o
4444

4545
| Problem | Next step |
4646
| --- | --- |
47-
| Calendar picker or metadata read fails | Include `calendar.readonly` in delegation, along with the other scopes above. |
47+
| Calendar picker fails | Include `calendar.readonly` in delegation, along with the other scopes above. |
4848
| A member gets no matches | Check their personal connection, primary Workspace identity, Users selection, calendars, and source date window. |
4949
| Shared calendar is missing | Confirm both the member and delegated source identity can access that calendar ID. |
5050
| Event is outside the configured range | Adjust the service source's range or narrow the user's query to its allowed window. |

‎apps/docs/openapi-v2-files-audit.json‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5785,9 +5785,8 @@
57855785
"enum": ["public", "password", "email", "sso"]
57865786
},
57875787
"password": {
5788-
"description": "Password for a password-gated share. Kept when omitted; enabling `password` with neither a supplied nor a stored password is a 400.",
5788+
"description": "Password of 15 to 1024 characters for a password-gated share. Kept when omitted; enabling `password` with neither a supplied nor a stored password is a 400. Taken literally, except that a request from the Sim agent resolves a whole-value `{{ENV_VAR}}` reference to that variable before the rules apply.",
57895789
"type": "string",
5790-
"minLength": 15,
57915790
"maxLength": 1024
57925791
},
57935792
"allowedEmails": {

‎apps/docs/openapi-v2-knowledge.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5449,13 +5449,13 @@
54495449
"description": "Registered connector type."
54505450
},
54515451
"credentialId": {
5452-
"description": "OAuth credential identifier for connectors that require OAuth.",
5452+
"description": "OAuth credential identifier for connector types whose `auth.mode` is `oauth` (see connector types); omit it for `apiKey` connectors.",
54535453
"type": "string",
54545454
"minLength": 1,
54555455
"maxLength": 255
54565456
},
54575457
"apiKey": {
5458-
"description": "Write-only API key for connectors that use API-key authentication.",
5458+
"description": "Write-only API key for connector types whose `auth.mode` is `apiKey` (see connector types), or a personal access token for an OAuth connector that also accepts one, such as GitHub. Send it instead of `credentialId`. Pass a raw key, or a secret reference written as the whole value `{{SECRET_NAME}}`, which the server resolves; `$SECRET_NAME` is not a reference.",
54595459
"type": "string",
54605460
"minLength": 1,
54615461
"maxLength": 10000

‎apps/docs/openapi-v2-resources.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15414,7 +15414,7 @@
1541415414
"mode": {
1541515415
"type": "string",
1541615416
"const": "oauth",
15417-
"description": "Authenticates with an OAuth credential."
15417+
"description": "Authenticates with an OAuth credential; pass `credentialId` when creating the connector."
1541815418
},
1541915419
"provider": {
1542015420
"type": "string",
@@ -15437,7 +15437,7 @@
1543715437
"mode": {
1543815438
"type": "string",
1543915439
"const": "apiKey",
15440-
"description": "Authenticates with a stored API key."
15440+
"description": "Authenticates with a stored API key; pass `apiKey`, and no `credentialId`, when creating the connector."
1544115441
},
1544215442
"label": {
1544315443
"description": "Label shown above the key field.",
@@ -15456,7 +15456,7 @@
1545615456
"additionalProperties": false
1545715457
}
1545815458
],
15459-
"description": "How the connector authenticates against its source."
15459+
"description": "How the connector authenticates against its source: `oauth` connectors take `credentialId` (GitHub also accepts a personal access token as `apiKey`), `apiKey` connectors take `apiKey`."
1546015460
},
1546115461
"configFields": {
1546215462
"type": "array",
@@ -15634,7 +15634,7 @@
1563415634
"mode": {
1563515635
"type": "string",
1563615636
"enum": ["oauth", "apiKey"],
15637-
"description": "How the connector authenticates against its source."
15637+
"description": "How the connector authenticates against its source: `oauth` connectors take `credentialId` (GitHub also accepts a personal access token as `apiKey`), `apiKey` connectors take `apiKey`."
1563815638
}
1563915639
},
1564015640
"required": ["mode"],

‎apps/docs/openapi-v2-workflows.json‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11558,9 +11558,8 @@
1155811558
"enum": ["public", "password", "email", "sso"]
1155911559
},
1156011560
"password": {
11561-
"description": "Write-only password. Required whenever `authType` is `password`, and rejected otherwise. Never readable back.",
11561+
"description": "Write-only password of 15 to 1024 characters, not only whitespace. Required whenever `authType` is `password`, and rejected otherwise. Never readable back. Taken literally, except that a request from the Sim agent resolves a whole-value `{{ENV_VAR}}` reference to that variable before the rules apply.",
1156211562
"type": "string",
11563-
"minLength": 1,
1156411563
"maxLength": 1024
1156511564
},
1156611565
"allowedEmails": {

‎apps/sim/app/api/v2/workflows/[workflowId]/deployments/chat/route.test.ts‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
* @vitest-environment node
33
*/
44
import {
5+
environmentUtilsMockFns,
56
MockV2ApiKeyUnauthenticatedError,
67
resetDbChainMock,
8+
resetEnvironmentUtilsMock,
79
resetEnvMock,
810
setEnv,
911
V2_OPERATION_RATE_LIMIT_ALLOWED,
@@ -72,6 +74,8 @@ vi.mock('@/ee/access-control/utils/permission-check', () => ({
7274
vi.mock('@/lib/api/server/routes/v2-api-key-auth', () => v2ApiKeyAuthModuleMock)
7375
vi.mock('@/lib/core/rate-limiter', () => v2RateLimiterModuleMock)
7476

77+
import { markCopilotRequest } from '@/lib/api/server/routes/copilot-request'
78+
import { performChatDeploy as realPerformChatDeploy } from '@/lib/workflows/orchestration/chat-deploy'
7579
import { DELETE, GET, PUT } from '@/app/api/v2/workflows/[workflowId]/deployments/chat/route'
7680

7781
const WORKSPACE_ID = 'workspace-1'
@@ -481,6 +485,91 @@ describe('/api/v2/workflows/[workflowId]/deployments/chat', () => {
481485

482486
expect(mocks.validateChatDeployAuth).not.toHaveBeenCalled()
483487
})
488+
489+
describe('password references', () => {
490+
const passwordBody = (password: string) => ({ ...validBody, authType: 'password', password })
491+
492+
/** Admitted exactly as the Sim agent's in-process CLI transport admits its calls. */
493+
const agentPut = (body: unknown) => {
494+
const request = new NextRequest(PATH, {
495+
method: 'PUT',
496+
headers: { 'content-type': 'application/json' },
497+
body: JSON.stringify(body),
498+
})
499+
markCopilotRequest(request, { userId: 'user-1', workspaceId: WORKSPACE_ID, chatId: 'c-1' })
500+
return PUT(request, routeContext)
501+
}
502+
503+
const environment = (variables: Record<string, string>) =>
504+
environmentUtilsMockFns.mockResolveEffectiveEnvironmentVariables.mockResolvedValueOnce(
505+
Object.fromEntries(
506+
Object.entries(variables).map(([name, value]) => [
507+
name,
508+
{ value, scope: 'workspace', visible: false },
509+
])
510+
)
511+
)
512+
513+
afterEach(resetEnvironmentUtilsMock)
514+
515+
it("deploys with the value of the agent's referenced variable", async () => {
516+
environment({ CHAT_PW: 'resolved-chat-password' })
517+
518+
const response = await agentPut(passwordBody('{{CHAT_PW}}'))
519+
520+
expect(response.status).toBe(200)
521+
expect(
522+
environmentUtilsMockFns.mockResolveEffectiveEnvironmentVariables
523+
).toHaveBeenCalledWith('user-1', WORKSPACE_ID, ['CHAT_PW'])
524+
expect(mocks.performChatDeploy.mock.calls[0][0].password).toBe('resolved-chat-password')
525+
})
526+
527+
it('refuses an unset variable by name instead of deploying the placeholder', async () => {
528+
const response = await agentPut(passwordBody('{{CHAT_PW}}'))
529+
530+
expect(response.status).toBe(400)
531+
expect((await response.json()).error.message).toBe(
532+
'Environment variable "CHAT_PW" referenced by password is not set for this workspace or user. Set it first, or pass the raw value.'
533+
)
534+
expect(mocks.performChatDeploy).not.toHaveBeenCalled()
535+
})
536+
537+
it('holds the resolved value to the chat password rules', async () => {
538+
mocks.performChatDeploy.mockImplementation(realPerformChatDeploy)
539+
environment({ CHAT_PW: 'short' })
540+
541+
const response = await agentPut(passwordBody('{{CHAT_PW}}'))
542+
543+
expect(response.status).toBe(400)
544+
expect((await response.json()).error.message).toBe(
545+
'Password must be at least 15 characters'
546+
)
547+
})
548+
549+
it('keeps a reference literal for an API key caller, under the same rules', async () => {
550+
mocks.performChatDeploy.mockImplementation(realPerformChatDeploy)
551+
552+
const response = await put(passwordBody('{{SHORT}}'))
553+
554+
expect(response.status).toBe(400)
555+
expect((await response.json()).error.message).toBe(
556+
'Password must be at least 15 characters'
557+
)
558+
expect(
559+
environmentUtilsMockFns.mockResolveEffectiveEnvironmentVariables
560+
).not.toHaveBeenCalled()
561+
})
562+
563+
it('stores a long literal reference verbatim for an API key caller', async () => {
564+
const response = await put(passwordBody('{{A_LONG_LITERAL_NAME}}'))
565+
566+
expect(response.status).toBe(200)
567+
expect(mocks.performChatDeploy.mock.calls[0][0].password).toBe('{{A_LONG_LITERAL_NAME}}')
568+
expect(
569+
environmentUtilsMockFns.mockResolveEffectiveEnvironmentVariables
570+
).not.toHaveBeenCalled()
571+
})
572+
})
484573
})
485574

486575
describe('DELETE', () => {

0 commit comments

Comments
 (0)