feat(managed-agent): Claude Platform Managed Agents workflow blocks#5769
feat(managed-agent): Claude Platform Managed Agents workflow blocks#5769rdematos wants to merge 138 commits into
Conversation
…ership workflow edits via sockets, ui improvements
…ng improvements, posthog, secrets mutations
…ration, signup method feature flags, SSO improvements
…nts, secrets performance, polling refactors, drag resources in mothership
…y invalidation, HITL docs
…endar triggers, docs updates, integrations/models pages improvements
…ions, jira forms endpoints
…mat, logs performance improvements fix(csp): add missing analytics domains, remove unsafe-eval, fix workspace CSP gap (#4179) fix(landing): return 404 for invalid dynamic route slugs (#4182) improvement(seo): optimize sitemaps, robots.txt, and core web vitals across sim and docs (#4170) fix(gemini): support structured output with tools on Gemini 3 models (#4184) feat(brightdata): add Bright Data integration with 8 tools (#4183) fix(mothership): fix superagent credentials (#4185) fix(logs): close sidebar when selected log disappears from filtered list; cleanup (#4186)
v0.6.46: mothership streaming fixes, brightdata integration
…m integration, atlassian triggers
Ship two workflow blocks that let a workflow author invoke a Claude Platform Managed Agent (cloud or self-hosted) as a first-class node, plus the workspace-scoped connection layer that stores the linked Anthropic API key. Blocks - **Claude Managed Agents** (`managed_agent_cloud`) — cloud environments. Session-create payload carries `agent`, `environment_id`, `vault_ids`, `resources` (memory + files) and free-form `metadata` tags. - **Claude Managed Agents (self-hosted)** (`managed_agent_self_hosted`) — self-hosted environments. Session `metadata` is forwarded as env vars to the deployer's self-hosted agent sandbox; the memory fields are gated behind `NEXT_PUBLIC_MANAGED_AGENT_SELF_HOSTED_MEMORY_ENABLED` because Claude self-hosted environments do not currently support the memory-store resource attach on the session API. Default seed rows for the Session-parameters table come from `NEXT_PUBLIC_MANAGED_AGENT_SELF_HOSTED_DEFAULTS` (JSON object); the seeded keys stay out of source so this block is deployment- neutral in the public tree. Connections - New `managed_agent_connection` table (workspace-scoped, encrypted API key). All CRUD paths go through `getUserEntityPermissions(userId, 'workspace', workspaceId)`; `admin`/`write` to create, rotate, or delete; any workspace member can list/browse. Plaintext keys never cross the client boundary — list responses return only a masked preview. - Settings surface under `settings/components/managed-agents/` lets admins add / rotate / remove connections with an inline Anthropic-API verify (`GET /v1/agents`). - 8 server routes proxy the Claude Platform read endpoints (agents, environments, memory stores, vaults, environment detail) so the block picker resolves options against the linked workspace. Tool + session client - `tools/managed_agent/run_session.ts` (client-safe skeleton) + `run_session.server.ts` (server-only impl, self-registered on boot via `globalThis` so the client bundle never pulls `postgres` / `fs` / encryption). Reconnect + `events.list` catch-up loop matches the docs' two-step session lifecycle. - `lib/managed-agents/session-client.ts` — the reusable HTTP client that shapes the cloud vs. self-hosted request bodies. - `tools/managed_agent/normalizers.ts` — pure input normalizers that coerce the workflow block's runtime shapes (table rows, JSON strings, flat objects, comma-lists) into typed values. Node UX - Optional `BlockConfig.nodeWidth` (default 250, MA blocks use 400) threaded through `calculateWorkflowBlockDimensions` for both the workflow-block hook and autolayout so long Anthropic IDs are not ellipsis-truncated in the collapsed row. - Friendly-name hydration on the collapsed row resolves connection / agent / environment / vault / memory-store IDs to human labels via the React Query hooks. New `blockType` prop on `SubBlockRow` gates the lookup. - New optional `defaultRows` prop on the `Table` subblock so any block can seed a fresh table with initial rows. Test coverage Aligned with the repo's existing patterns (`@vitest-environment node`, `vi.hoisted` + `vi.mock` + static imports, `@sim/testing` helpers). 86 tests across 5 files: - `tools/managed_agent/normalizers.test.ts` (27) — every shape the block subblocks hand off + bad-input paths. - `lib/managed-agents/session-client.test.ts` (17) — cloud vs. self-hosted branch: memory routing (resource vs. metadata), file resources cloud-only, `vault_ids` only when non-empty, user metadata does not overwrite memory keys, envType-omitted default. - `lib/managed-agents/connections.test.ts` (17) — CRUD via captured DB-chain args: encryption on write (plaintext never lands in row), masking on read, workspace-scoped `WHERE` on every op, `verify` gate blocks persistence, error truncation to 500 chars. - `app/api/managed-agent-connections/route.test.ts` (15) — permission gates on GET/POST/DELETE: 401 unauthenticated, 400 missing params, 403 read-tier callers on create/delete, 404 for missing rows, plaintext never in POST response, both `write` and `admin` accepted. - `blocks/blocks/managed_agent_self_hosted.test.ts` (10) — env-driven `readSessionMetadataDefaults` and `isSelfHostedMemoryEnabled` (truthy/falsy/whitespace forms). Documentation - New env vars documented in `apps/sim/.env.example`: `MANAGED_AGENT_DEBUG_PAYLOAD`, `NEXT_PUBLIC_MANAGED_AGENT_SELF_HOSTED_DEFAULTS`, `NEXT_PUBLIC_MANAGED_AGENT_SELF_HOSTED_MEMORY_ENABLED`.
|
Someone is attempting to deploy a commit to the Sim Team on Vercel. A member of the Team first needs to authorize it. |
PR SummaryHigh Risk Overview Connections & API — New workspace-scoped connection storage (encrypted keys, masked in list responses) with Settings UI to link, rotate, and remove workspaces after Execution — Shared Editor UX — Optional Reviewed by Cursor Bugbot for commit 8e9f8a1. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThis PR adds Claude Platform Managed Agents as workflow blocks. The main changes are:
Confidence Score: 4/5Active Anthropic streams can outlive cancelled workflows, and connection decryption can bypass the normal tool response path. Public self-hosted defaults can also disclose sensitive values.
apps/sim/tools/managed_agent/run_session.server.ts; apps/sim/blocks/blocks/managed_agent_self_hosted.ts
|
| Filename | Overview |
|---|---|
| apps/sim/tools/managed_agent/run_session.server.ts | Adds server-side session execution and reconnect handling, but cancellation is dropped and decryption can reject outside the tool response path. |
| apps/sim/lib/managed-agents/connections.ts | Adds encrypted, workspace-scoped persistence with composite workspace predicates and masked client responses. |
| apps/sim/lib/managed-agents/session-client.ts | Adds Managed Agents HTTP and payload handling for cloud and self-hosted sessions. |
| apps/sim/blocks/blocks/managed_agent_self_hosted.ts | Adds the self-hosted block and deployment defaults, whose public environment source can expose sensitive values. |
| apps/sim/app/api/managed-agent-connections/route.ts | Adds authenticated connection listing, creation, and deletion with workspace permission checks. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant W as Workflow executor
participant T as Managed Agent tool
participant DB as Connection store
participant A as Anthropic API
W->>T: Run block with workspace context
T->>DB: Load and decrypt connection
DB-->>T: API key
T->>A: Create session
T->>A: Send user message
T->>A: Open SSE stream
loop Reconnect and catch up
A-->>T: Session events
T->>A: List events after cursor
end
T-->>W: Content and session status
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant W as Workflow executor
participant T as Managed Agent tool
participant DB as Connection store
participant A as Anthropic API
W->>T: Run block with workspace context
T->>DB: Load and decrypt connection
DB-->>T: API key
T->>A: Create session
T->>A: Send user message
T->>A: Open SSE stream
loop Reconnect and catch up
A-->>T: Session events
T->>A: List events after cursor
end
T-->>W: Content and session status
Reviews (1): Last reviewed commit: "feat(managed-agent): Claude Platform Man..." | Re-trigger Greptile
| const streamResp = await openSessionStream({ apiKey, sessionId }) | ||
| await readSSEEvents<AnthropicSessionEvent>(streamResp, { |
There was a problem hiding this comment.
Cancellation Leaves Stream Running
The workflow abort signal is not passed to openSessionStream or readSSEEvents. When a workflow is cancelled or times out while Anthropic keeps the stream open, this invocation continues reading and processing events in the background instead of releasing the HTTP connection.
| } | ||
| } | ||
|
|
||
| const apiKey = await getDecryptedApiKey({ id: params.connection, workspaceId }) |
There was a problem hiding this comment.
Decryption Error Escapes Tool Contract
getDecryptedApiKey throws when stored ciphertext cannot be decrypted, such as after an encryption-key rotation, and this call occurs before the session error-handling block. The tool promise rejects instead of returning its declared success: false response, so the workflow receives an unhandled execution failure rather than an actionable connection error.
…ires_action, public defaults) Four fixes to PR #5769 review comments. - `getDecryptedApiKey` throws when stored ciphertext cannot be decrypted (typically after an `ENCRYPTION_KEY` rotation). Wrapped the call in the tool's error-handling path so the failure returns the tool's declared `{success:false, output:{}, error:'...'}` shape with an actionable message pointing at Settings → Managed Agents → rotate, rather than an unhandled promise rejection. - Added the workflow's abort signal to `_context.abortSignal` on the `directExecution` path in `executeTool`, so any tool that wants to propagate cancellation can. This is opt-in — existing tools are unaffected. - Read the signal in `run_session.server.ts` and thread it into `createSession`, `sendUserMessage`, `openSessionStream`, `readSSEEvents`, and `listEventsAfter`. Every long-lived HTTP hop now cancels cleanly. - Guard all loop entrances with `signal?.aborted` and translate any abort-caused exception into a clean `{success:false, error:'aborted'}` response — no scary stack traces on cancel. - Server-side / vault-backed MCP tools legitimately hold the session in `requires_action` with no client-visible events until the tool finishes. My previous logic returned a terminal error on the first empty `listEventsAfter` catch-up, failing those workflows before the agent could complete. - New policy: `requires_action` idle is NEVER terminal by itself. If the outer reconnect loop finds no new events, back off (500ms, doubling to 5s max) and re-poll `listEventsAfter`. Only fail after a hard `MAX_REQUIRES_ACTION_WAIT_MS` cap (~5 min) — with a clear error message — or on an explicit `session.status_terminated` / `session.error`. - `EventState` gains `requiresActionEnteredAt` (first entry into the busy stretch) and `currentBackoffMs`; both reset when the session makes progress (non-empty catch-up). Reconnect ceiling raised to 60 since the loop now sleeps between polls. - Deleted `NEXT_PUBLIC_MANAGED_AGENT_SELF_HOSTED_DEFAULTS` — any deployer who accidentally seeded a token into the JSON would leak it to every browser via the client bundle. - Replaced with server-only `MANAGED_AGENT_SELF_HOSTED_DEFAULTS` + `GET /api/managed-agent-defaults` route that returns the parsed rows. Values never enter the client bundle at build time. - Extended the `Table` sub-block component with an optional `fetchDefaultRows` async prop (mirrors the `fetchOptions` pattern used by comboboxes). The block config points at `fetchManagedAgentSelfHostedDefaults` in `subblock-options.ts`, which calls the new API route via `requestJson(contract, ...)`. - Removed the deleted function's tests; added coverage for the new API route (`app/api/managed-agent-defaults/route.test.ts` — 7 tests). - Updated `.env.example` docs to make the safety property explicit. Verified: 86 tests pass across the 6 managed-agent test files. Pre-existing tsc errors (integration-tag enum, environments-route type mismatch, etc.) are unchanged.
Followup on PR #5769 review: Cursor Bugbot flagged that `getDecryptedApiKey` still throws on ciphertext decrypt failure, so the five proxy routes (agents / environments / environment-detail / vaults / memory-stores) surface a 500 to the client instead of a controlled response. My prior tool-level try/catch only handled the workflow-block invocation path. Change: `getDecryptedApiKey` now returns a discriminated result — `{ ok: true, apiKey } | { ok: false, reason: 'not_found' | 'decrypt_failed' }` — so callers can render each failure mode with the correct status: - `not_found` → 404 "Connection not found" (unchanged) - `decrypt_failed` → 502 with an actionable "rotate the API key to re-encrypt" hint, matching the tool-side message New `apiKeyFailureResponse` helper in `lib/managed-agents/proxy.ts` keeps the mapping in one place — each proxy route now reads: const keyResult = await getDecryptedApiKey({ id, workspaceId }) if (!keyResult.ok) return apiKeyFailureResponse(keyResult) const apiKey = keyResult.apiKey Tool call site (`run_session.server.ts`) collapses its previous try/catch to the same `keyResult.ok` check — cleaner and shares the same error phrasing. Tests updated: `getDecryptedApiKey` cases now assert the shape; added a third case that mocks `decryptSecret` to reject and asserts `{ ok: false, reason: 'decrypt_failed' }`. All 87 tests pass.
…selected Both blocks now render a "Vault authorization" switch immediately below the Credential vaults picker: "I own or am authorized to use these vaults. I understand this means this agent can assume the identity granted by them." The tool rejects execution with an actionable error when `vaults` is non-empty and the ack is not checked, so a workflow author cannot silently attach an OAuth vault to an agent they aren't authorized to run under. Why runtime, not subblock-level required: Sim's `condition` engine uses strict equality against a single value, so it cannot natively express "field is a non-empty array". Enforcing at execute time keeps the ack always visible (users see the warning up front) and fails closed on run. Also lifted the truthy-check into a shared `isTruthyAck` helper in `tools/managed_agent/normalizers.ts` — handles the real boolean, plus the `"true" | "1" | "yes"` string forms that switch values can arrive as depending on serialization. Four cases covered: - `true` accepted - `"true"` / `"True"` / `"1"` / `"yes"` accepted (case-insensitive, whitespace-trimmed) - Every other string / `false` / `undefined` / non-string non-boolean rejected Tests: 91 pass (4 new for `isTruthyAck`).
Reviewer feedback: the previous version buried the security disclaimer under a generic "Vault authorization" title with the full text below in the description. Users would only see "Vault authorization" next to the toggle and might not read the description. Move the whole "I own or am authorized to use these vaults. I understand this means this agent can assume the identity granted by them." string to the switch's title so it sits right next to the toggle. Description line collapses to the run-time constraint: "Required when at least one vault is selected above." No functional change; the run-time enforcement (tool rejects when `vaults` is non-empty and the ack is falsy) is unchanged.
…w node
Previously Claude Managed Agent sessions opened untitled, which made
them hard to trace back from the Claude Platform side to the workflow
that created them.
Sessions now open with a human-legible title composed from the Sim
workspace, workflow, and node names:
sim.ai - <workspace name> - <workflow name> - <block name>
Segments whose DB lookup fails (deleted workspace/workflow, unnamed
block) are silently dropped so the session still opens with whatever
context is available. If nothing beyond the "sim.ai" prefix resolves,
we skip the title entirely rather than create a placeholder-titled
session.
Plumbing:
- The generic block handler now also passes `blockId` and `blockName`
on `_context` when it invokes a tool. `WorkflowToolExecutionContext`
gains matching optional fields — opt-in for any tool that wants to
emit externally-visible records tied to their origin node.
- `run_session.server.ts` looks up the workspace/workflow names via
`@sim/db` at execute time (a two-column SELECT per name; wrapped in
a defensive try/catch so a DB blip never blocks session creation).
- `buildSessionCreatePayload` already accepts `title` — no session-
client change needed.
Follow-up on Cursor Bugbot's review of the last few commits. Three
related, but independent, correctness fixes.
Files were silently dropped from cloud sessions
- The cloud block declares `columns: ['File ID', 'Mount path']`. The
table subblock stores each row's cells keyed by exactly those
column strings — but `normalizeFiles` only knew about `Key`/`Value`
(older shape) and flat `{fileId, mountPath}`, so every file row was
dropped and no files ever reached the session.
- Fix: `normalizeFiles` now recognizes `File ID` / `Mount path`
(block's declared columns), `file_id` / `mount_path` (snake case),
the older `Key` / `Value`, and the flat `{fileId, mountPath}`
shapes. Regression test added.
Stale collapsed-row labels when Claude Workspace changes
- The managed-agent collapsed rows hydrate friendly names by reading
the SIBLING `connection` value from `allSubBlockValues`. But
`SubBlockRow`'s memo compare only checked this row's own value, so
the memo would keep showing agent/env/vault/memory labels tied to
the previous connection until each row's own stored value happened
to change.
- Fix: extend `areSubBlockRowPropsEqual` to also compare a small set
of known sibling-dependency keys (`connection`, `oauthCredential`,
`credential`). Any of them changing invalidates the memo so the
hydration hooks re-run with the new sibling value.
Empty tables re-seeded deployer defaults on remount
- The table init effect early-exited when `storeValue.length > 0`,
but treated `[]` as "unseeded" and re-ran `fetchDefaultRows` /
`defaultRows`. A self-hosted block whose Session-parameters table
was intentionally cleared to zero rows would get repopulated on
every remount, clobbering the author's choice.
- Fix: only seed when `storeValue === undefined || storeValue === null`.
A stored `[]` — the author's explicit "no rows" — is now preserved.
Tests: 92 pass (30 normalizer, up from 29; regression test for the
`File ID` / `Mount path` shape).
| emptyCellsTemplate, | ||
| defaultRows, | ||
| fetchDefaultRows, | ||
| ]) |
There was a problem hiding this comment.
Empty table shows phantom row
Medium Severity
The mount effect no longer seeds when the subblock store is [], but the rows memo still synthesizes a default row whenever the value array is empty. After an author clears every row, the UI can show an editable row that is not persisted in the store, which conflicts with the new “empty array is intentional” rule and can confuse session-parameter input.
Reviewed by Cursor Bugbot for commit b8e5039. Configure here.
| }) | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Rotate connection hook unused
Low Severity
useRotateManagedAgentConnection is exported and wired to the PATCH rotate route, but nothing in the settings UI or elsewhere imports or calls it, so key rotation is only partially shipped.
Reviewed by Cursor Bugbot for commit b8e5039. Configure here.
Two follow-up fixes for Cursor Bugbot's most recent review. #11 Defaults API lacks authentication - `GET /api/managed-agent-defaults` returned every value from `MANAGED_AGENT_SELF_HOSTED_DEFAULTS` with no session check. Docs and the block copy steer deployers toward putting sandbox-scoped secrets there (because they never enter the client bundle at build time), but an unauthenticated caller could still dump the JSON. - Fixed: route now runs `checkSessionOrInternalAuth` and returns 401 for anonymous callers. Same-origin fetches from the block editor automatically carry the session cookie, so no client change needed. - Test added: `route.test.ts` now covers the 401 unauthenticated case. #13 `useRotateManagedAgentConnection` was unused - The hook + PATCH route existed but nothing called them, so the only way to change a stored API key was delete + re-link — awkward when the deployer's just cycling the key. - Wired into the settings UI: - New `RotateKeyModal` component (mirrors the create modal but with only the key field; connection name stays put) runs the same verify-then-save flow. - Added a "Rotate key" action to each connection row's `RowActionsMenu`, gated on `canEdit` (workspace `write`/`admin` tiers). Success shows a toast; errors surface inline in the modal. Not fixed / addressed inline: - #9 (stale labels after connection change) and #10 (empty table re-seeds defaults) — Bugbot re-fired these on the previous commit by signature-match against a static code shape. The actual fixes (extra sibling-dep comparison in `areSubBlockRowPropsEqual`; only seed when `storeValue` is null/undefined) are in place and functional. No change needed. - #12 (empty table shows a phantom row) — theoretical. The table component's delete-row guard (`rows.length === 1`) prevents users from ever reaching `[]` through the UI, so the mismatch it warns about isn't reachable in normal use.
Cursor Bugbot on 9704cec flagged a race in the `Table` sub-block init effect: `fetchDefaultRows` is async, and the effect's guard on `storeValue !== undefined && storeValue !== null` was captured at effect setup time. Between the fetch start and its resolution, a user could type into the phantom row — persisting the row via `handleCellChange`'s `setStoreValue([newRow])` — and then the resolved fetch would call `setStoreValue(defaultRows)` and clobber their edit. Fix: re-read the current value from the subblock store inside `seedWith` right before writing (`useSubBlockStore.getState() .getValue(blockId, subBlockId)`), and skip the write if the value has already been populated since the effect started. The closure-captured `storeValue` was stale; the store's `getState` returns the LATEST value synchronously. Other Bugbot comments on this commit are re-fires of items already fixed on prior commits (they signature-match against static code shapes without noticing the corrective code that lives beside them): `Stale labels after connection change` (fixed via `depsEqual` in `areSubBlockRowPropsEqual`), `Empty table re-seeds defaults` (fixed via `storeValue !== undefined && storeValue !== null` guard), `Defaults API lacks authentication` (fixed via `checkSessionOrInternalAuth`), `Rotate connection hook unused` (fixed via the new Rotate-key modal wired into the settings UI). `Empty table shows phantom row` is theoretical — the delete-row guard (`rows.length === 1`) prevents users from reaching `[]` through the UI, so the mismatch it warns about isn't reachable in normal use.
| if (fetchDefaultRows) { | ||
| fetchDefaultRows() | ||
| .then((rows) => seedWith(rows.length > 0 ? rows : defaultRows)) | ||
| .catch(() => seedWith(defaultRows)) |
There was a problem hiding this comment.
Async seed race on save
Low Severity
When fetchDefaultRows is async, a quick save or navigation after opening the block can persist workflow state before defaults land in the subblock store, leaving Session parameters empty even though the author opened the table once.
Reviewed by Cursor Bugbot for commit 2766139. Configure here.
Pre-seeding the self-hosted block's Session parameters table from the server-only MANAGED_AGENT_SELF_HOSTED_DEFAULTS env var is better served by Sim's workflow blocks. Remove the capability end-to-end: - Delete GET /api/managed-agent-defaults route + test - Drop the MANAGED_AGENT_SELF_HOSTED_DEFAULTS env var - Remove getManagedAgentDefaultsContract + its schemas/types - Remove the fetchManagedAgentSelfHostedDefaults client fetcher - Unwire fetchDefaultRows from the self-hosted block and fix the two stale comments that referenced the removed env var Revert the shared table subblock seeding machinery (table.tsx, sub-block.tsx, blocks/types.ts) to its pre-feature behavior so no other block is affected — table.tsx is now byte-identical to main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2766139 to
8e9f8a1
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8e9f8a1. Configure here.
| 'anthropic-beta': MANAGED_AGENTS_BETA, | ||
| }, | ||
| signal, | ||
| }) |
There was a problem hiding this comment.
Wrong beta for memory stores
High Severity
proxyManagedAgentsGet always sends anthropic-beta: managed-agents-2026-04-01, including for GET /v1/memory_stores. Anthropic’s Memory Stores API requires agent-memory-2026-07-22, so the Memory Store combobox proxy fails and the cloud block cannot list stores.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 8e9f8a1. Configure here.
…ds on self-hosted Collapse what #5769 split into two blocks into one, natively: - Add an Environment type selector (Cloud / Self-hosted) that filters the environment list to the matching type and gates cloud-only fields - Memory store, memory access/instructions, and files are cloud-only (self- hosted rejects the resources[] attach — verified live, 400) and are now hidden on self-hosted instead of silently dropped. A self-hosted worker that uses a memory store reads its id from a Metadata key the author sets explicitly - Expose each environment's config.type on the list options so the picker can filter by mode; pass the selected type as a routing hint (server still re- resolves the authoritative type via getEnvironmentType)
* feat(managed-agents): add Claude Managed Agents workflow block
* improvement(managed-agents): complete session inputs/outputs; trim block templates
- add memory instructions + file mount_path inputs; bound metadata (16 pairs)
- surface cumulative token usage (inputTokens/outputTokens) as outputs
- validate the full session-create schema against live docs
- remove BlockMeta templates
* improvement(managed-agents): select a Claude Platform credential instead of BYOK
- register Claude Platform as a token-paste service-account credential (descriptor + validator)
- add no-OAuth OAUTH_PROVIDERS entry; generalize the shared credential picker with a 'service-account' kind
- block: oauth-input credential picker + dependsOn dropdowns; list route resolves the key server-side (audit-logged)
- run via directExecution with the executor-injected key; drop the internal run route
- remove the interim claude-platform BYOK provider
* fix(managed-agents): harden reconnect loop; fix credential-picker regression
- drive completion off terminal events + authoritative session status (drop the fragile busy-clock)
- drain full event history so a long session's tail is never cut off
- skip idless events in catch-up; require an id before replying to custom_tool_use
- gate the shared credential-selector service-account lookup on credentialKind and use the non-throwing helper (was crashing multi-service OAuth pickers)
- audit-log the list route's credential access; refresh stale tool docs
* fix(managed-agents): address review round 2
- don't complete on idle status while a requires_action is still outstanding
- vaults: combobox → dropdown so multiSelect actually attaches multiple vaults
- auto-select a freshly-pasted service-account credential (onCreated through the connect modal)
* fix(managed-agents): propagate cancellation, fix reconnect ordering, classify advanced fields
- Thread the executor abort signal into directExecution (additive ToolConfig
change) so a cancelled workflow stops the session immediately; best-effort
user.interrupt releases the Anthropic session past cancel/wall-clock cap
- Recompute the requires_action pending state from the chronological history
so an older agent.message recovered on catch-up can't clear a newer pause
- Retry a custom-tool error reply that failed to send instead of stranding the
session (mark the event seen only once handled)
- Mark optional fields (vaults, memory, files, metadata) mode: advanced
- Title the session with the workflow id for Claude-console traceability
* fix(managed-agents): stop leaked sessions on all give-up paths; harden event ordering and normalizers
- Interrupt on sendUserMessage failure and on the reconnect-cap exit, matching
the abort/wall-clock paths, so no give-up path leaves a session running
- Order the events list by processed_at (list page order isn't guaranteed
chronological) so catch-up accumulates text and reads the latest lifecycle
event correctly regardless of API sort
- Don't reset reconnect backoff on a failing custom-tool reply (stays unseen
for retry) — prevents a no-delay reconnect storm
- Treat only end_turn as a complete status_idle event; an unspecified idle
defers to the sawActivity-gated status check (no empty completion pre-turn)
- normalizeFiles parses a JSON-stringified table instead of dropping it;
normalizeStringList returns [] on malformed JSON; metadata keeps scalar values
- Declare the injected accessToken as a hidden param for convention parity
* fix(managed-agents): interrupt the session on a mid-run stream/API failure
The non-abort error catch path returned without stopping the session, so a
mid-run network/API failure could leave the Anthropic session running against
the workspace key. Interrupt on that path too — completing the invariant that
every give-up exit (abort, cap, send failure, reconnect cap, stream error)
stops the session. Still fails fast; no retry added.
* fix(managed-agents): skip idless stream previews; resolve service-account provider icons
- Skip idless events in the live SSE handler (mirroring catch-up). event_start/
event_delta previews carry no id, are never deduped, and final text always
arrives as a persisted id-bearing agent.message — so appending previews could
double the block's content output
- Map serviceAccountProviderId to its base provider in PROVIDER_ID_TO_BASE_PROVIDER
so parseProvider resolves 'claude-platform-service-account' to 'claude-platform'
(a two-segment base the hyphen split can't recover), fixing the credential-row
icon falling back to the generic external-link glyph
* fix(managed-agents): keep idless terminals and preserve live pending state
Refine the round-6 idless-event fix, which was too broad:
- Process idless events again (revert the blanket stream skip) so an idless
session.status_idle(end_turn)/session.error delivered only on the live stream
still registers as terminal instead of reconnecting to a timeout. Only the
agent.message TEXT append is now id-gated, so preview text still can't double
- When catch-up history has no lifecycle event, restore the live-observed
requires_action state instead of trusting a stale older agent.message that
cleared it — prevents a false completion with partial output while a tool
result is still pending
* fix(managed-agents): route memory via metadata on self-hosted environments
Live API testing revealed self-hosted environments reject the `resources`
array with a 400 ("resources are not supported with self-hosted
environments"), so the prior universal-resources[] payload would have failed
any self-hosted session that attached a memory store or files.
Restore env-type-aware routing: resolve the environment's config.type via a new
getEnvironmentType() before session create, and for self_hosted send the memory
store through metadata.memory_store_ids/memory_access (the worker consumes it)
and drop file attachments. Cloud environments keep resources[]. Verified end-to-
end against the live Managed Agents API (cloud resources[] 200, self-hosted
metadata 200, self-hosted resources[] 400).
* feat(managed-agents): environment-type selector; hide cloud-only fields on self-hosted
Collapse what #5769 split into two blocks into one, natively:
- Add an Environment type selector (Cloud / Self-hosted) that filters the
environment list to the matching type and gates cloud-only fields
- Memory store, memory access/instructions, and files are cloud-only (self-
hosted rejects the resources[] attach — verified live, 400) and are now hidden
on self-hosted instead of silently dropped. A self-hosted worker that uses a
memory store reads its id from a Metadata key the author sets explicitly
- Expose each environment's config.type on the list options so the picker can
filter by mode; pass the selected type as a routing hint (server still re-
resolves the authoritative type via getEnvironmentType)
* fix(managed-agents): track requires_action by processed_at, not history position
Persisted history can lag the live stream, so the last lifecycle event in the
history array may be OLDER than a requires_action the stream already observed.
Deriving the pending state from history position (findLastLifecycleEvent) could
then clear a newer pause and let an idle snapshot complete a still-waiting
session with partial text.
Track requires_action from the NEWEST lifecycle event by processed_at across
both the live stream and catch-up, so an older/lagging event can never override
a newer pause. Removes the pendingBeforeCatchup snapshot and history-position
recompute. New test covers lagging history holding only an older running event.
* fix(managed-agents): treat missing processed_at as oldest, not newest
A lifecycle event without processed_at mapped to +Infinity, poisoning the
high-water mark: once seen, no later timestamped event could update the pending
state (at >= Infinity always false), stranding a pause or clearing one wrongly.
Map missing/unparseable processed_at to -Infinity so an untimestamped lifecycle
event can never outrank a timestamped one in either direction — it neither
blocks later real events nor clears a timestamped requires_action. Persisted
lifecycle events always carry processed_at; this is purely defensive. New test
covers a stray untimestamped running not clearing a timestamped pause.


Summary
Ship two workflow blocks that let a workflow author invoke a Claude Platform Managed Agent (cloud or self-hosted) as a first-class node, plus the workspace-scoped connection layer that stores the linked Anthropic API key.
Blocks
managed_agent_cloud) — cloud environments. Session-create payload carriesagent,environment_id,vault_ids,resources(memory + files) and free-formmetadatatags.managed_agent_self_hosted) — self-hosted environments. Sessionmetadatais forwarded as env vars to the deployer's self-hosted agent sandbox; memory fields are gated behindNEXT_PUBLIC_MANAGED_AGENT_SELF_HOSTED_MEMORY_ENABLED(Claude self-hosted environments do not currently support memory-store resource attach on the session API). Default seed rows for the Session-parameters table come fromNEXT_PUBLIC_MANAGED_AGENT_SELF_HOSTED_DEFAULTS(JSON); seeded keys stay out of source so the block is deployment-neutral in the public tree.Connections
managed_agent_connectiontable (workspace-scoped, encrypted API key). All CRUD paths go throughgetUserEntityPermissions(userId, 'workspace', workspaceId);admin/writeto create, rotate, or delete; any workspace member can list/browse. Plaintext keys never cross the client boundary — list responses return only a masked preview.settings/components/managed-agents/lets admins add / rotate / remove connections with an inline Anthropic-API verify (GET /v1/agents).Tool + session client
tools/managed_agent/run_session.ts(client-safe skeleton) +run_session.server.ts(server-only impl, self-registered on boot viaglobalThisso the client bundle never pullspostgres/fs/ encryption). Reconnect +events.listcatch-up loop matches the docs' two-step session lifecycle.lib/managed-agents/session-client.ts— reusable HTTP client that shapes the cloud vs. self-hosted request bodies.tools/managed_agent/normalizers.ts— pure input normalizers that coerce the block's runtime shapes (table rows, JSON strings, flat objects, comma-lists) into typed values.Node UX
BlockConfig.nodeWidth(default 250, MA blocks use 400) threaded throughcalculateWorkflowBlockDimensionsfor both the workflow-block hook and autolayout so long Anthropic IDs aren't ellipsis-truncated in the collapsed row.blockTypeprop onSubBlockRowgates the lookup.defaultRowsprop on theTablesubblock so any block can seed a fresh table with initial rows.Type of Change
Testing
Added 86 tests across 5 files, following the repo's patterns (
@vitest-environment node,vi.hoisted+vi.mock+ static imports,@sim/testinghelpers):tools/managed_agent/normalizers.test.ts(27) — every shape the block subblocks hand off + bad-input paths.lib/managed-agents/session-client.test.ts(17) — cloud vs. self-hosted branch: memory routing (resource vs. metadata), file resources cloud-only,vault_idsonly when non-empty, user metadata does not overwrite memory keys, envType-omitted default.lib/managed-agents/connections.test.ts(17) — CRUD via captured DB-chain args: encryption on write (plaintext never lands in row), masking on read, workspace-scopedWHEREon every op,verifygate blocks persistence, error truncation to 500 chars.app/api/managed-agent-connections/route.test.ts(15) — permission gates on GET/POST/DELETE: 401 unauthenticated, 400 missing params, 403 read-tier callers on create/delete, 404 for missing rows, plaintext never in POST response, bothwriteandadminaccepted.blocks/blocks/managed_agent_self_hosted.test.ts(10) — env-drivenreadSessionMetadataDefaultsandisSelfHostedMemoryEnabled(truthy/falsy/whitespace forms).Reviewer focus: (1) the client/server split in
run_session*.ts— confirm no server-only deps leak into the client bundle; (2) the workspace-scoped permission checks on every connection CRUD path; (3) that plaintext API keys never cross the client boundary.Run locally:
bun run type-check,bun run test,bun run check:api-validation.Checklist
Screenshots/Videos
Documentation
New env vars documented in
apps/sim/.env.example:MANAGED_AGENT_DEBUG_PAYLOAD,NEXT_PUBLIC_MANAGED_AGENT_SELF_HOSTED_DEFAULTS,NEXT_PUBLIC_MANAGED_AGENT_SELF_HOSTED_MEMORY_ENABLED.