Skip to content

Commit 45c7d05

Browse files
Merge remote-tracking branch 'origin/main' into improvement/v2-endpoints
# Conflicts: # apps/sim/app/api/table/[tableId]/columns/route.ts # apps/sim/app/api/table/[tableId]/route.ts # apps/sim/app/api/table/[tableId]/rows/[rowId]/route.ts # apps/sim/app/api/v1/tables/[tableId]/columns/route.ts # apps/sim/app/api/v1/tables/[tableId]/rows/[rowId]/route.ts # apps/sim/app/api/v1/tables/[tableId]/rows/upsert/route.ts # apps/sim/lib/copilot/tools/server/table/user-table.ts # apps/sim/lib/folders/lifecycle.ts # apps/sim/lib/table/service.ts # scripts/check-api-validation-contracts.ts
2 parents 29b64c6 + 86486d5 commit 45c7d05

286 files changed

Lines changed: 57215 additions & 2768 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.

.devcontainer/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ services:
1919
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your_auth_secret_here}
2020
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-your_encryption_key_here}
2121
- COPILOT_API_KEY=${COPILOT_API_KEY}
22+
- NEXT_PUBLIC_CHAT_DISABLED=${NEXT_PUBLIC_CHAT_DISABLED:-}
2223
- SIM_AGENT_API_URL=${SIM_AGENT_API_URL}
2324
- OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434}
2425
- NEXT_PUBLIC_SOCKET_URL=${NEXT_PUBLIC_SOCKET_URL:-}

apps/docs/content/docs/en/integrations/managed_agent.mdx

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,200 @@ Open a Claude Platform Managed Agent session and return the assistant response a
6464
| `inputTokens` | number | Cumulative input tokens for the session. |
6565
| `outputTokens` | number | Cumulative output tokens for the session. |
6666

67+
### `managed_agent_create_session`
68+
69+
Create a Claude Platform Managed Agent session and return its id without waiting for a reply.
70+
71+
#### Input
72+
73+
| Parameter | Type | Required | Description |
74+
| --------- | ---- | -------- | ----------- |
75+
| `agent` | string | Yes | Managed-agent id inside the linked Claude workspace. |
76+
| `environment` | string | Yes | Environment id inside the linked Claude workspace. |
77+
| `environmentType` | string | No | Environment execution model hint \('cloud' \| 'self_hosted'\). |
78+
| `userMessage` | string | No | Optional first message; seeds initial_events and starts the agent immediately. |
79+
| `vaults` | array | No | Zero or more vault ids for MCP tool auth. |
80+
| `vaultsAck` | boolean | No | Acknowledgement that the author may use the attached vaults. |
81+
| `memoryStoreId` | string | No | Optional Agent Memory Store id. |
82+
| `memoryAccess` | string | No | Memory store access mode: 'read_write' \(default\) or 'read_only'. |
83+
| `memoryInstructions` | string | No | Per-attachment guidance for how the agent should use the memory store. |
84+
| `files` | array | No | File attachments \(cloud envs only\), as \[\{fileId, mountPath?\}\]. |
85+
| `sessionParameters` | object | No | Key/value session metadata forwarded to the session. |
86+
87+
#### Output
88+
89+
| Parameter | Type | Description |
90+
| --------- | ---- | ----------- |
91+
| `sessionId` | string | Anthropic session id \(sesn_...\). |
92+
| `started` | boolean | True when a first message was seeded, so the agent is already running. |
93+
94+
### `managed_agent_send_message`
95+
96+
Send a user message to an existing Claude Platform Managed Agent session.
97+
98+
#### Input
99+
100+
| Parameter | Type | Required | Description |
101+
| --------- | ---- | -------- | ----------- |
102+
| `userMessage` | string | Yes | The user message to send to the session. |
103+
104+
#### Output
105+
106+
| Parameter | Type | Description |
107+
| --------- | ---- | ----------- |
108+
| `sessionId` | string | The session the message was sent to. |
109+
| `sent` | boolean | True when the event was accepted by the API. |
110+
111+
### `managed_agent_get_session`
112+
113+
Read a Managed Agent session: status, stop reason, token usage, metadata, and any tool calls awaiting approval.
114+
115+
#### Input
116+
117+
| Parameter | Type | Required | Description |
118+
| --------- | ---- | -------- | ----------- |
119+
120+
#### Output
121+
122+
| Parameter | Type | Description |
123+
| --------- | ---- | ----------- |
124+
| `sessionId` | string | The session that was read. |
125+
| `status` | string | Session status — 'idle', 'running', 'rescheduling', or 'terminated'. |
126+
| `stopReason` | string | Why the session last stopped, e.g. 'end_turn' or 'requires_action'. |
127+
| `requiresAction` | boolean | True when the session is waiting on a tool confirmation or custom tool result. If this is true while pendingTools is empty, the session is blocked but the API named no blocking events — surface it rather than treating the session as done. |
128+
| `pendingTools` | json | Blocking tool calls — \[\{id, eventType, kind, name, input\}\]. Route by kind: 'confirmation' ids go to Respond To Tool Confirmation, 'custom_tool_result' ids go to Respond To Custom Tool. |
129+
| `metadata` | json | Session metadata. |
130+
| `title` | string | Session title. |
131+
| `inputTokens` | number | Cumulative input tokens. |
132+
| `outputTokens` | number | Cumulative output tokens. |
133+
134+
### `managed_agent_list_events`
135+
136+
Read a Managed Agent session's event history and the agent's reply text.
137+
138+
#### Input
139+
140+
| Parameter | Type | Required | Description |
141+
| --------- | ---- | -------- | ----------- |
142+
| `eventTypes` | array | No | Optional event-type filter, e.g. \['agent.message'\]. Omit to return every event. |
143+
| `limit` | number | No | Maximum events to return, keeping the most recent \(default 500\). |
144+
145+
#### Output
146+
147+
| Parameter | Type | Description |
148+
| --------- | ---- | ----------- |
149+
| `sessionId` | string | The session that was read. |
150+
| `events` | json | Session events, oldest first. |
151+
| `count` | number | Number of events returned. |
152+
| `assistantText` | string | Concatenated text of every persisted agent.message, in order. |
153+
| `truncated` | boolean | True when the limit was hit and older events were dropped. |
154+
155+
### `managed_agent_update_session`
156+
157+
Update a Managed Agent session's title or metadata.
158+
159+
#### Input
160+
161+
| Parameter | Type | Required | Description |
162+
| --------- | ---- | -------- | ----------- |
163+
| `title` | string | No | New session title. |
164+
| `sessionParameters` | object | No | Replacement metadata map \(replaces all stored metadata, not merged\). Leaving it empty leaves the stored metadata unchanged — use clearMetadata to remove it. |
165+
| `clearMetadata` | boolean | No | Removes all of the session's stored metadata. Overrides any map supplied above. |
166+
167+
#### Output
168+
169+
| Parameter | Type | Description |
170+
| --------- | ---- | ----------- |
171+
| `sessionId` | string | The session that was updated. |
172+
| `updated` | boolean | True when the update was accepted. |
173+
| `metadata` | json | Metadata after the update. |
174+
| `title` | string | Title after the update. |
175+
176+
### `managed_agent_interrupt_session`
177+
178+
Stop a running Managed Agent session; it stays usable afterwards.
179+
180+
#### Input
181+
182+
| Parameter | Type | Required | Description |
183+
| --------- | ---- | -------- | ----------- |
184+
185+
#### Output
186+
187+
| Parameter | Type | Description |
188+
| --------- | ---- | ----------- |
189+
| `sessionId` | string | The session that was interrupted. |
190+
| `interrupted` | boolean | True when the interrupt was accepted. |
191+
192+
### `managed_agent_respond_tool_confirmation`
193+
194+
Allow or deny the tool calls a Managed Agent session is waiting on before it can continue.
195+
196+
#### Input
197+
198+
| Parameter | Type | Required | Description |
199+
| --------- | ---- | -------- | ----------- |
200+
| `toolUseIds` | array | Yes | Blocking tool-use EVENT ids, from Get Session pendingTools\[\].id where kind is 'confirmation' \(not toolu_ ids\). |
201+
| `decision` | string | Yes | 'allow' to let the tools run, or 'deny' to reject them. |
202+
| `denyMessage` | string | No | Reason surfaced to the agent. Only sent when the decision is deny. |
203+
204+
#### Output
205+
206+
| Parameter | Type | Description |
207+
| --------- | ---- | ----------- |
208+
| `sessionId` | string | The session that was answered. |
209+
| `decision` | string | The decision applied — 'allow' or 'deny'. |
210+
| `confirmedToolUseIds` | json | The tool-use event ids that were answered. |
211+
212+
### `managed_agent_respond_custom_tool`
213+
214+
Return the result of a custom tool a Managed Agent session is waiting on so it can continue.
215+
216+
#### Input
217+
218+
| Parameter | Type | Required | Description |
219+
| --------- | ---- | -------- | ----------- |
220+
| `customToolUseId` | string | Yes | The custom tool-use EVENT id being answered, from Get Session pendingTools\[\].id where kind is 'custom_tool_result'. |
221+
| `result` | string | Yes | The tool's output, returned to the agent as text. |
222+
| `isError` | boolean | No | Mark the result as a failure so the agent can adjust its approach. |
223+
224+
#### Output
225+
226+
| Parameter | Type | Description |
227+
| --------- | ---- | ----------- |
228+
| `sessionId` | string | The session that was answered. |
229+
| `answeredToolUseId` | string | The custom tool-use event id that was answered. |
230+
231+
### `managed_agent_archive_session`
232+
233+
Archive a Managed Agent session, preserving its history. Not reversible.
234+
235+
#### Input
236+
237+
| Parameter | Type | Required | Description |
238+
| --------- | ---- | -------- | ----------- |
239+
240+
#### Output
241+
242+
| Parameter | Type | Description |
243+
| --------- | ---- | ----------- |
244+
| `sessionId` | string | The session that was archived. |
245+
| `archived` | boolean | True when the archive was accepted. |
246+
247+
### `managed_agent_delete_session`
248+
249+
Permanently delete a Managed Agent session, its events, and its sandbox. Not reversible.
250+
251+
#### Input
252+
253+
| Parameter | Type | Required | Description |
254+
| --------- | ---- | -------- | ----------- |
255+
256+
#### Output
257+
258+
| Parameter | Type | Description |
259+
| --------- | ---- | ----------- |
260+
| `sessionId` | string | The session that was deleted. |
261+
| `deleted` | boolean | True when the delete was accepted. |
262+
67263

apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ import { Callout } from 'fumadocs-ui/components/callout'
6464
| Variable | Description |
6565
|----------|-------------|
6666
| `API_ENCRYPTION_KEY` | Encrypts stored API keys (32 hex chars): `openssl rand -hex 32` |
67-
| `COPILOT_API_KEY` | API key for copilot features |
67+
| `COPILOT_API_KEY` | API key for Chat. Without it the Sim Chat block, scheduled prompt jobs, and Inbox cannot run |
68+
| `NEXT_PUBLIC_CHAT_DISABLED` | Set to `true` to hide the Chat module: the workspace lands on your first workflow, with no chats list, scheduled tasks, or editor Chat panel. Chat is shown when unset; `bun run setup` sets it for you if you skip the chat key |
6869
| `ADMIN_API_KEY` | Admin API key for GitOps operations |
6970
| `ALLOWED_LOGIN_DOMAINS` | Restrict signups to domains (comma-separated) |
7071
| `ALLOWED_LOGIN_EMAILS` | Restrict signups to specific emails (comma-separated) |

apps/realtime/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@
3333
"@sim/workflow-types": "workspace:*",
3434
"@socket.io/redis-adapter": "8.3.0",
3535
"drizzle-orm": "^0.45.2",
36+
"lib0": "0.2.117",
3637
"postgres": "^3.4.5",
3738
"redis": "5.10.0",
3839
"socket.io": "^4.8.1",
40+
"y-protocols": "1.0.7",
41+
"yjs": "13.6.31",
3942
"zod": "4.3.6"
4043
},
4144
"devDependencies": {

apps/realtime/src/access-revalidation.test.ts

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,14 @@ function makeManager(sockets: FakeSocket[], presence: Partial<UserPresence>[] =
5050
const manager = {
5151
io: { sockets: { sockets: socketMap } },
5252
isReady: () => true,
53-
getWorkflowUsers: vi.fn().mockResolvedValue(presence),
54-
getWorkflowIdForSocket: vi.fn().mockResolvedValue(null),
55-
removeUserFromRoom: vi
56-
.fn()
57-
.mockImplementation(async (_socketId: string, workflowId?: string) => workflowId ?? null),
53+
getRoomUsers: vi.fn().mockResolvedValue(presence),
54+
getRoomForSocket: vi.fn().mockResolvedValue(null),
55+
removeUserFromRoom: vi.fn().mockResolvedValue(true),
5856
broadcastPresenceUpdate: vi.fn().mockResolvedValue(undefined),
5957
}
6058
return manager as unknown as IRoomManager & {
61-
getWorkflowUsers: ReturnType<typeof vi.fn>
62-
getWorkflowIdForSocket: ReturnType<typeof vi.fn>
59+
getRoomUsers: ReturnType<typeof vi.fn>
60+
getRoomForSocket: ReturnType<typeof vi.fn>
6361
removeUserFromRoom: ReturnType<typeof vi.fn>
6462
broadcastPresenceUpdate: ReturnType<typeof vi.fn>
6563
}
@@ -84,8 +82,11 @@ describe('access-revalidation sweep', () => {
8482
expect.objectContaining({ workflowId: 'wf-1' })
8583
)
8684
expect(socket.leave).toHaveBeenCalledWith('wf-1')
87-
expect(manager.removeUserFromRoom).toHaveBeenCalledWith('sock-1', 'wf-1')
88-
expect(manager.broadcastPresenceUpdate).toHaveBeenCalledWith('wf-1')
85+
expect(manager.removeUserFromRoom).toHaveBeenCalledWith(
86+
{ type: 'workflow', id: 'wf-1' },
87+
'sock-1'
88+
)
89+
expect(manager.broadcastPresenceUpdate).toHaveBeenCalledWith({ type: 'workflow', id: 'wf-1' })
8990
})
9091

9192
it('keeps a socket whose access is still valid', async () => {
@@ -141,7 +142,30 @@ describe('access-revalidation sweep', () => {
141142

142143
expect(mockResolveRole).toHaveBeenCalledWith('user-1', 'wf-1', 'read')
143144
// The security scan must stay Redis-free — presence is never consulted.
144-
expect(manager.getWorkflowUsers).not.toHaveBeenCalled()
145+
expect(manager.getRoomUsers).not.toHaveBeenCalled()
146+
})
147+
148+
it('never evicts a socket joined only to a non-workflow room (files/tables/file-doc)', async () => {
149+
// The sweep shares one io with the files/tables/file-doc handlers. Those rooms are
150+
// namespaced (`workspace-files:ws-1`, `table:t-1`), so treating every socket.rooms
151+
// entry as a workflow id would resolve a bogus permission → null → evict the socket
152+
// from its files/table room every pass. Non-workflow rooms must be filtered out.
153+
const filesSocket = makeSocket('sock-1', 'user-1', 'workspace-files:ws-1')
154+
const tableSocket = makeSocket('sock-2', 'user-2', 'table:t-1')
155+
const manager = makeManager([filesSocket, tableSocket])
156+
// Even if the role resolver would say "no access", these must never be swept.
157+
mockResolveRole.mockResolvedValue(null)
158+
159+
const sweep = startAccessRevalidationSweep(manager)
160+
await sweep.runOnce()
161+
sweep.stop()
162+
163+
expect(mockResolveRole).not.toHaveBeenCalled()
164+
expect(filesSocket.leave).not.toHaveBeenCalled()
165+
expect(filesSocket.emit).not.toHaveBeenCalled()
166+
expect(tableSocket.leave).not.toHaveBeenCalled()
167+
expect(tableSocket.emit).not.toHaveBeenCalled()
168+
expect(manager.removeUserFromRoom).not.toHaveBeenCalled()
145169
})
146170

147171
it('evicts only the revoked socket, not co-members of the room', async () => {
@@ -199,37 +223,37 @@ describe('access-revalidation sweep', () => {
199223
sweep.stop()
200224

201225
expect(manager.removeUserFromRoom).toHaveBeenCalledTimes(2)
202-
expect(manager.broadcastPresenceUpdate).toHaveBeenCalledWith('wf-1')
226+
expect(manager.broadcastPresenceUpdate).toHaveBeenCalledWith({ type: 'workflow', id: 'wf-1' })
203227
})
204228

205-
it('defers cleanup when removal fails with expired socket mappings', async () => {
229+
it('drops eviction cleanup when the socket is no longer mapped to the room (no infinite retry)', async () => {
206230
const socket = makeSocket('sock-1', 'user-1', 'wf-1')
207231
const manager = makeManager([socket], [{ socketId: 'sock-1', role: 'read' }])
208-
// Mapping keys already expired (lookup resolves null) AND the removal fails
209-
// (the Redis manager swallows the transport error into null) — the failed
210-
// removal must still defer instead of reading as success.
211-
manager.removeUserFromRoom.mockResolvedValueOnce(null)
232+
// A healthy lookup shows the socket is no longer mapped to any workflow room (its presence
233+
// is already gone), and removeUserFromRoom reports a no-op `false`. This is "already clean",
234+
// not a deferrable failure — the cleanup must drop it, never re-enqueue a still-connected
235+
// socket forever. (A genuine failure — still mapped + false — is covered by the next test.)
236+
manager.getRoomForSocket.mockResolvedValue(null)
237+
manager.removeUserFromRoom.mockResolvedValue(false)
212238
mockResolveRole.mockResolvedValue(null)
213239

214240
const sweep = startAccessRevalidationSweep(manager)
215241
await sweep.runOnce()
216-
217-
expect(manager.broadcastPresenceUpdate).not.toHaveBeenCalled()
218-
219242
await sweep.runOnce()
220243
sweep.stop()
221244

222-
expect(manager.removeUserFromRoom).toHaveBeenCalledTimes(2)
223-
expect(manager.broadcastPresenceUpdate).toHaveBeenCalledWith('wf-1')
245+
// Attempted once, then dropped — not re-enqueued across passes, and no broadcast.
246+
expect(manager.removeUserFromRoom).toHaveBeenCalledTimes(1)
247+
expect(manager.broadcastPresenceUpdate).not.toHaveBeenCalled()
224248
})
225249

226250
it('defers cleanup when the manager swallows a removal failure into null', async () => {
227251
const socket = makeSocket('sock-1', 'user-1', 'wf-1')
228252
const manager = makeManager([socket], [{ socketId: 'sock-1', role: 'read' }])
229253
// Live mapping but the removal reports nothing removed — the Redis manager
230254
// swallows transport errors into null, so this is the only failure signal.
231-
manager.getWorkflowIdForSocket.mockResolvedValue('wf-1')
232-
manager.removeUserFromRoom.mockResolvedValueOnce(null)
255+
manager.getRoomForSocket.mockResolvedValue({ type: 'workflow', id: 'wf-1' })
256+
manager.removeUserFromRoom.mockResolvedValueOnce(false)
233257
mockResolveRole.mockResolvedValue(null)
234258

235259
const sweep = startAccessRevalidationSweep(manager)
@@ -243,15 +267,15 @@ describe('access-revalidation sweep', () => {
243267
sweep.stop()
244268

245269
expect(manager.removeUserFromRoom).toHaveBeenCalledTimes(2)
246-
expect(manager.broadcastPresenceUpdate).toHaveBeenCalledWith('wf-1')
270+
expect(manager.broadcastPresenceUpdate).toHaveBeenCalledWith({ type: 'workflow', id: 'wf-1' })
247271
})
248272

249273
it('skips removal when the socket has since moved to a different workflow', async () => {
250274
const socket = makeSocket('sock-1', 'user-1', 'wf-1')
251275
const manager = makeManager([socket], [{ socketId: 'sock-1', role: 'read' }])
252276
// Between the membership snapshot and cleanup, the socket switched to a
253277
// workflow it can still access — removal must not touch its new presence.
254-
manager.getWorkflowIdForSocket.mockResolvedValue('wf-2')
278+
manager.getRoomForSocket.mockResolvedValue({ type: 'workflow', id: 'wf-2' })
255279
mockResolveRole.mockResolvedValue(null)
256280

257281
const sweep = startAccessRevalidationSweep(manager)
@@ -357,7 +381,7 @@ describe('access-revalidation sweep', () => {
357381
const manager = makeManager([socket], [{ socketId: 'sock-1', role: 'read' }])
358382
// A Redis outage where commands hang in the offline queue instead of
359383
// failing: the cleanup lane stalls, but scans must keep running.
360-
manager.getWorkflowIdForSocket.mockReturnValue(new Promise(() => {}))
384+
manager.getRoomForSocket.mockReturnValue(new Promise(() => {}))
361385
mockResolveRole.mockResolvedValue(null)
362386

363387
const sweep = startAccessRevalidationSweep(manager)

0 commit comments

Comments
 (0)