v0.8.3: sim cli, new tools, credential groups, splunk, finder-like files refactor, security hardening - #6765
v0.8.3: sim cli, new tools, credential groups, splunk, finder-like files refactor, security hardening#6765waleedlatif1 wants to merge 35 commits into
Conversation
waleedlatif1
commented
Aug 16, 2026
- feat(library): Automation Anywhere Alternative: AI Agents vs. RPA for Real Reasoning (feat(library): Automation Anywhere Alternative: AI Agents vs. RPA for Real Reasoning #6728)
- improvement(credential-groups): align settings surface with the shared page patterns (improvement(credential-groups): align settings surface with the shared page patterns #6727)
- feat(credentials): add v2 credential lifecycle APIs (feat(credentials): add v2 credential lifecycle APIs #6664)
- feat(credential-groups): complete managed account enrollment (feat(credential-groups): complete managed account enrollment #6729)
- fix(tables): auto-scroll during column drag (fix(tables): auto-scroll during column drag #6722)
- feat(cli): Sim CLI with AWS-style profiles and a platform key exchange (feat(cli): Sim CLI with AWS-style profiles and a platform key exchange #6147)
- fix(credentials): conceal inaccessible credential reads (fix(credentials): conceal inaccessible credential reads #6730)
- feat(resources): multiselect on tables and knowledge, spring-loaded folders (feat(resources): multiselect on tables and knowledge, spring-loaded folders #6721)
- fix(knowledge): bound chunking separators so one config can't stall processing (fix(knowledge): bound chunking separators so one config can't stall processing #6735)
- fix(docker): prune the app package by manifest name (fix(docker): prune the app package by manifest name #6736)
- fix(credentials): let workspace admins disconnect a teammate's OAuth credential (fix(credentials): let workspace admins disconnect a teammate's OAuth credential #6737)
- fix(media): prevent drawtext filtergraph injection in add_text (fix(media): prevent drawtext filtergraph injection in add_text #6734)
- fix(auth): scope SSO account linking to the verified domain and fence plugin provider mutations (fix(auth): scope SSO account linking to the verified domain and fence plugin provider mutations #6738)
- refactor(resources): converge Files onto the shared drag hook and batch bulk authorization (refactor(resources): converge Files onto the shared drag hook and batch bulk authorization #6748)
- feat(okta): add System Log, MFA, sessions, apps, roles, and group rules (feat(okta): add System Log, MFA, sessions, apps, roles, and group rules #6741)
- improvement(tables): make Cmd+F search as you type and clear on close (improvement(tables): make Cmd+F search as you type and clear on close #6733)
- feat(splunk): add Splunk Enterprise and Cloud integration (feat(splunk): add Splunk Enterprise and Cloud integration #6743)
- fix(okta): stop partial updates erasing stored profile data (fix(okta): stop partial updates erasing stored profile data #6751)
- fix(credentials): restore reliable OAuth connections (fix(credentials): restore reliable OAuth connections #6753)
- fix(deploy): resolve the error-output flag from edges on both sides of change detection (fix(deploy): resolve the error-output flag from edges on both sides of change detection #6754)
- fix(tables): re-check the find match at the reveal, not just before paging (fix(tables): re-check the find match at the reveal, not just before paging #6750)
- fix(fork): preserve folder structure across a fork edge for files, tables, and knowledge bases (fix(fork): preserve folder structure across a fork edge for files, tables, and knowledge bases #6752)
- feat(crowdstrike): add alerts, host response, IOC, Spotlight, RTR, and case tools (feat(crowdstrike): add alerts, host response, IOC, Spotlight, RTR, and case tools #6746)
- fix(workflow): derive the webhook URL only where a sub-block shows one (fix(workflow): derive the webhook URL only where a sub-block shows one #6758)
- feat(servicenow): semantic incident, change, catalog, approval, CMDB, and knowledge tools (feat(servicenow): semantic incident, change, catalog, approval, CMDB, and knowledge tools #6747)
- fix(forking): hide satisfied dependent configuration (improvement(forking): hide satisfied dependent configuration #6723)
- feat(datadog): extend to 40 tools and align every operation with the published OpenAPI specs (feat(datadog): extend to 40 tools and align every operation with the published OpenAPI specs #6745)
- feat(cloudflare): add WAF rulesets, rate limiting, Zero Trust Access, R2, Workers, and Tunnels (feat(cloudflare): add WAF rulesets, rate limiting, Zero Trust Access, R2, Workers, and Tunnels #6740)
- fix(sandbox): undefine the raw fetch host bridge before user code runs (fix(sandbox): undefine the raw fetch host bridge before user code runs #6761)
- feat(tooling): enforce docs freshness and modernize agent skills (feat(tooling): enforce docs freshness and modernize agent skills #6756)
- feat(mssql): add Microsoft SQL Server integration (feat(mssql): add Microsoft SQL Server integration #6739)
- docs(cli): add a CLI docs section generated from the command tree (docs(cli): add a CLI docs section generated from the command tree #6762)
- feat(microsoft_ad): licensing, security, audit, role, and device operations (feat(microsoft_ad): licensing, security, audit, role, and device operations #6742)
- fix(docs): serve JSON-LD in the HTML, fix sidebar spacing, and tighten the CLI guides (fix(docs): serve JSON-LD in the HTML, fix sidebar spacing, and tighten the CLI guides #6763)
- fix(integrations): close regressions found in the final validation sweep (fix(integrations): close regressions found in the final validation sweep #6764)
… Real Reasoning (#6728) Co-authored-by: Sim Pi Agent <pi@sim.ai>
…d page patterns (#6727) * improvement(credential-groups): align settings surface with the shared page patterns - drop the row "..." menu; a row opening a detail page carries the chevron only, and Delete moves to the detail header behind a confirm modal - replace the hand-rolled Save chip with saveDiscardActions, and wire useSettingsUnsavedGuard so detail edits survive tab switches - fix swapped staleTime constants: the list carried Infinity, which combined with the app-wide retryOnMount:false to cache one transient failure until a full page reload - evict the detail query on delete, and keep the bots prop referentially stable so a refetch cannot drop a queued Slack authorization message - reset the detail tab param on open and close so a stale link cannot open the next group on the previous group's tab - match peer rows (iconFilled + --text-icon), drop a bespoke max-w and a duplicated gap-7, align no-results copy and the Slack modal field gutter * improvement(credential-groups): hold first paint for a deep-linked group Matches the data-drains list: a deep link whose id is still resolving no longer flashes the list chrome before jumping to the detail. Keys the detail by group id so lifted draft state can never carry across groups. * fix(credential-groups): await the refetch before clearing the edit buffer The update mutation fired its invalidations without returning them, so mutateAsync resolved before the refetch landed. Callers that clear their draft on success then fell back onto the pre-save cache and flashed the old name and description until the refetch completed — or kept showing them if it failed.
* feat(credentials): add v2 OAuth connection APIs * fix(credentials): preserve active OAuth connection links * fix(credentials): bind OAuth links to connection intent * feat(credentials): complete v2 credential lifecycle * fix(credentials): make disconnect idempotent * fix(credentials): stabilize oauth draft retries * fix(credentials): bind oauth callbacks to drafts * fix(credentials): fail closed on oauth completion * fix(credentials): bind shopify completion to oauth state * fix(credentials): align custom oauth reconnects * fix(credentials): centralize application authorization * fix(credentials): keep OAuth draft intent immutable * fix(credentials): allow renamed reconnect targets * fix(credentials): close OAuth draft edge cases * fix(credentials): fail closed without breaking auth * fix(credentials): preserve migrated route behavior * feat(credentials): add provider search * fix(credentials): prevent stale secrets and drafts
* fix(credential-groups): show reconnect after authorization * feat(slack): include managed user auth in custom bots * feat(credential-groups): add enrollment completion page * fix(credential-groups): align settings order and block color * fix(credential-groups): delete credentials on access revoke * fix(credential-groups): use person icon for enrollments * fix(credential-groups): keep enrollment submit visible * fix(credential-groups): make enrollment connections optional * fix(credential-groups): simplify people actions * fix(credential-groups): preserve pagination after deletion * fix(credential-groups): delete removed enrollments * fix(credential-groups): hydrate canvas labels
* fix(tables): auto-scroll during column drag * fix(tables): keep drag targets aligned while scrolling * fix(tables): preserve column targets across drag surface * fix(tables): align workflow group drop indicators
#6147) * improvement(api): pull in the v2 external endpoint surface Cherry-picks improvement/v2-endpoints (98c85677f5) onto the current base. The v2 surface standardizes one response family across every endpoint: `{ data }`, `{ data, nextCursor }`, and `{ error: { code, message, details? } }`, rendered through apps/sim/app/api/v2/lib/response.ts. v1 auth and rate limiting are reused as-is; the workspace-access and enterprise-audit checks are split into `resolve*` cores returning structured failures, with thin v1 wrappers that render the old `{ error }` body so v1 behavior is unchanged. The branch's own /api/v2/tables/** is dropped. Staging's tables v2 (#6067, typed predicate grammar + POST /api/v2/tables/[tableId]/query) supersedes it and lands in the following merge; the two are reconciled onto the shared envelope separately. Conflict resolutions: - v1/middleware.ts: keeps resolveWorkspaceRequestActor alongside the new resolveWorkspaceAccess/resolveWorkspaceScope split - v1/audit-logs/auth.ts: keeps the newer targetOrganizationId parameter and isOrganizationBillingBlocked check inside the structured resolver - bun.lock: taken from HEAD; the branch's lock churn was unrelated lucide-react hoisting Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * feat(usage): accept X-API-Key on usage-logs list + export /api/users/me/usage-logs and /export now use checkHybridAuth — the same auth /api/users/me/usage-limits already accepts — so external monitors can read summary.bySourceCredits (the source breakdown of usage-limits' aggregate currentPeriodCost) instead of estimating Copilot spend by subtraction. Workspace-scoped keys are pinned to their own workspace's slice of the ledger: the filter defaults to the key's workspace and an explicit mismatch 403s. Both endpoints documented in openapi-core.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(cli): sim CLI with AWS-style profiles and a platform key exchange Adds `packages/sim-cli` (`@sim/cli`, bin `sim`) and extends the existing CLI key handoff so it can mint the credential the public API actually accepts. ## Key exchange The handoff already existed but only minted *copilot* keys, which do not authenticate `/api/v1` or `/api/v2` — those want a Sim platform key. The approval now carries a `scope`: - `copilot` (the default, so terminals built against the original flow are unaffected) mints as before - `platform` mints a Sim API key: workspace-scoped when the approver is a workspace admin, personal otherwise Scope and workspace are fixed at *approval*, not at poll: the poll is unauthenticated by necessity, so the browser is the only moment a human is present to consent and the only place a permission can be checked. The poll echoes back what was granted rather than what was asked for, so the CLI cannot file a copilot key under a platform profile and fail later with an opaque 401. Picking a workspace and scoping a key to it are kept separate. The terminal has no key yet, so it cannot list workspaces — the browser picker is the only place that choice can be made, and the pick comes back as the profile's default whether or not the key is bound to it. Otherwise a non-admin would pick a workspace by name and then have to go find its id by hand. Personal-key creation moves into `lib/api-key/orchestration` so the settings route and the exchange share one issuer. ## CLI Profiles work like the AWS CLI: `~/.sim/config` for settings (`[profile dev]`), `~/.sim/credentials` for keys at 0600 (`[dev]`), selected via `--profile` / `SIM_PROFILE`. Each setting resolves flag → env → file → default, and `sim whoami` reports the winning source so a surprising value is explainable. CI can skip login entirely with `SIM_API_KEY` + `SIM_WORKSPACE`. Commands cover the v2 surface pulled in earlier: workflows, logs, files, and knowledge, with `--output json` passing the API's own shapes through for `jq`. `sim tables` is deliberately absent — that surface is still in flux. ## Drift fixes The v2 routes were authored a month ago and had fallen behind their services: `checkActorUsageLimits(userId, workspaceId)` → the billing-attribution flow (which also restores correct payer attribution for workspace keys on KB upload and search), `processDocumentsWithQueue` gained a required argument, and the deploy/rollback param objects had stale fields. Caught by a cold type-check — an incremental run had reported these files clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * feat(billing): dedicated v2 usage endpoints; keep internal usage routes session-only Replaces the earlier X-API-Key enablement on /api/users/me/usage-logs with a dedicated public surface, so the internal Billing-settings endpoints can evolve with the UI while external monitors get a stable versioned contract: - GET /api/v2/billing/usage — current-billing-period summary with bySourceCredits (the source breakdown external monitors need to watch e.g. Copilot consumption without estimating by subtraction), plus limitCredits and plan - GET /api/v2/billing/usage/logs — cursor-paged credit ledger in the v2 envelope - workspace-scoped keys are pinned to their own workspace's slice; personal keys read the account ledger The public wire is credits-only: usage-logs rows now carry a hasCost boolean instead of dollarCost (the Billing UI only needed the >0 signal), and the rateLimit block is removed from the usage-limits response and docs (deploy-modal tab relabeled accordingly). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(cli): generate the CLI's v2 API from the route contracts, add tables The same endpoint was being described in three hand-maintained places: the Zod contracts the routes validate against, the OpenAPI documents, and the CLI's own TypeScript interfaces. Two of those are now derived. ## Generation `scripts/generate-v2-cli-api.ts` reads `apps/sim/lib/api/contracts/v2/**` and emits `packages/sim-cli/src/generated/v2-api.ts`: request/response types for all 44 operations plus an operation table (method, path, path params) the client dispatches through, so a route that moves or changes verb moves the CLI with it. The contracts are the right source because the routes validate against them — a shape that disagrees with a contract is a shape the server would reject. Zod 4's `z.toJSONSchema()` handles all 110 schema slots; the JSON-Schema-to-TS emitter is hand-rolled over that known-narrow subset and throws on anything unrecognized rather than degrading to `any`, since silence is how a generated client drifts. `packages/*` must not import `apps/*`, so the generated file is plain type declarations with no imports and the script does the crossing at build time. `check:cli-api` fails CI when the file is stale. The generated directory is excluded from biome: the pre-commit hook runs `check --write`, which would otherwise reformat generated output and fail that check with an unrelated message. ## OpenAPI: checked, not generated The docs specs carry ~1000 hand-written descriptions and ~400 examples that Zod schemas do not encode, so generating them would trade real documentation for mechanical accuracy. `check:openapi-drift` reconciles structure instead — every v2 path and method must exist on both sides — keeping the prose while still failing on divergence. Both currently agree on all 44 operations. ## Tables `sim tables list|get|columns|rows|insert|delete-rows`, built on the generated types. Rows go through the POST query endpoint even unfiltered, since it is the only shape carrying the predicate. Row columns are discovered at runtime and unioned across the page, so a sparse row cannot hide a column. Deletion requires an explicit `--row`/`--filter` selector *and* `--yes`; an argument-less call would otherwise empty the table. Path params are percent-encoded — an id containing `/` or `?` would otherwise retarget the request. The four existing command groups drop their hand-written interfaces for the generated ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(cli): make the generated v2 API a fixed point of the formatter The pre-commit hook rewrote the generated file immediately after it was committed, so `check:cli-api` then failed in CI reporting contract drift that had not happened — the only difference was quote style. The biome.json exclusion added alongside it does not help: lint-staged runs `biome check --write` on explicit paths, which bypasses `files.includes`. It implied protection it never provided, so it is removed. The generator now pipes its output through `biome format --stdin-file-path` instead, making the emitted file conformant by construction. The hook has nothing left to change, and the check compares like with like. A formatter failure throws rather than emitting unformatted output, since falling back silently would reopen the same loop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(cli-auth): wait for the workspace list before allowing approval The picker fell back to "No workspace (personal key)" while the workspace query was in flight, and Connect stayed live through that window. A fast click approved a personal key with no default workspace — when the same click a moment later would have issued a workspace-scoped key. The fallback read as an answer rather than a pending state, so the card could promise one outcome and deliver another. Connect is now disabled until the list resolves, the trigger shows a loading label (a placeholder would not show, since the fallback always counts as a selection), and the explanatory line no longer asserts the personal-key outcome before it is known. Failure is treated as degraded rather than fatal: the picker disables but Connect stays enabled and the copy says a personal key will be issued, so a transient list failure cannot strand a waiting terminal. Tests cover the pending, loaded, admin-binding, and error states; the two loading assertions fail against the previous implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(cli-auth): name minted keys by timestamp, not date A second login on the same day failed with `A workspace API key named "CLI (2026-07-30)" already exists` — after the user had already approved in the browser, so the whole handoff was wasted and there was no way to complete it without renaming the existing key. Key names are unique per owner, so the name has to be unique per login. Now `CLI (2026-07-30 15:42:07Z)`: second precision, UTC so it is unambiguous in a shared workspace key list and sorts chronologically. The comment claiming a same-day collision was desirable (so logins would reuse one key) was wrong — nothing reuses the key, the mint just fails. A collision at second precision now means something genuinely unexpected, so it is still surfaced rather than retried under a suffixed name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * feat(docs): validate OpenAPI specs against the Zod contracts in CI The specs in apps/docs are hand-authored because they carry what Zod never defines — error envelopes, status codes, prose, examples — so they can't be generated; check:openapi validates them instead: - spec integrity: $refs resolve, operationIds unique, 2xx documented, no orphaned component schemas - v2 conventions: every /api/v2 operation documents 401 + 429 and every 4xx/5xx resolves to the canonical { error: { code, message } } envelope - contract cross-check: contracts are auto-discovered from lib/api/contracts/v2 (each carries its method + path); doc<->contract coverage both ways, query/body/response field diffs via z.toJSONSchema - examples: documented request/response examples must parse with the matching contract's actual Zod schemas First run caught real drift, fixed here: 16 stale orphaned schemas in the core spec, the v2 billing ops referencing v1-shaped error components, deploy/rollback examples missing the required nullable lifecycle keys, CreateTableBody missing folderId, a legacy-grammar delete-rows example, and four knowledge document ops missing their required workspaceId query param. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * fix(docs): recursive field diff in check:openapi + the deep drift it found A mutation test showed the doc<->contract field diff only compared top-level properties, so a typo inside the { data } envelope passed. The diff now descends through matching object properties and array items (both sides must expose a property set — passthrough contracts and prose-only docs end the descent instead of false-positive), with the Zod JSON-schema root doubling as the $defs context. Deep drift it immediately caught, fixed here: select-column config (options/multiple) missing from every tables column schema, AddColumnBody hand-rolling a third column shape (now composed from ColumnInput, with position/workflowGroupId as the per-op extensions the contracts actually admit), chunking strategyOptions undocumented, and the deployment lifecycle fields (activeDeployment/latestDeploymentAttempt) missing from DeploymentState. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * fix(security): close the triggerType rate-limit bypass on workflow execute Caller-supplied triggerType flowed unchecked into preprocessExecution, whose checkRateLimit default turns OFF for 'manual'/'chat' — so any API-key caller, and any anonymous public-API caller billed to the workspace owner, could execute unthrottled by sending {"triggerType":"manual"} (async runs also skipped the worker-side check via admissionCompleted). External callers may now only send the redundant 'api' value; internal JWT callers ('workflow'/'mcp') are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * refactor(execution): extract enqueue/status/cancel into shared libs Prepares the v2 execution surface: handleAsyncExecution's queue logic moves to lib/workflows/executor/enqueue-execution.ts (slot/claim semantics encoded in a discriminated outcome, not HTTP statuses), the execution-status read to execution-status.ts, and the order-sensitive cancel machinery to lib/execution/cancel-workflow-execution.ts. The v1 routes re-render identically — their suites pass unmodified. Also: preprocessExecution gains rateLimitCounter ('sync'|'async') and its 429 now carries code RATE_LIMIT_EXCEEDED + retryAfterMs (previously indistinguishable from the concurrency 429 and Retry-After was discarded); and the duplicate cancel contract in contracts/logs.ts is unified on the full 5-value reason enum — its narrower copy made requestJson throw a client ZodError when cancelling a paused HITL run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(execution): callable execution service + structured error classifier executeWorkflowService composes the same libs the v1 route holds inline (call-chain guard, execution-id claim, LoggingSession, preprocessing, deployed-state load + file-field processing, timeout-bound executeWorkflowCore, output hydration/compaction) for the deployed-state caller class — the seam the v2 execute route and in-process internal callers share, making the HTTP endpoint syntactic sugar. classifyExecutionError stops discarding the block context that buildBlockExecutionError already attaches at throw sites: failed runs now yield {message, code, blockId, blockName, blockType} with a stable append-only code enum (TIMEOUT/CANCELLED/USAGE_LIMIT_EXCEEDED/ INVALID_INPUT/BLOCK_EXECUTION_FAILED/CHILD_WORKFLOW_FAILED/ OUTPUT_TOO_LARGE/EXECUTION_FAILED), so callers route on error class instead of substring-matching messages — the single place raw errors are interpreted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(api): POST /api/v2/workflows/[id]/execute Thin route over executeWorkflowService: X-API-Key or anonymous public-API auth (sync/stream only for anonymous), strict body with body-flag async (no mode headers on v2), SSE passthrough for stream, and the execution resource response — executionId always present, in-band run failures are status:'failed' with the structured {message, code, blockId, blockName, blockType} error, sync timeout is status:'failed' + TIMEOUT instead of v1's 408, and a Response block's payload stays inside output (authors never control response status/headers on this origin). Async debits the async bucket and the 202 statusUrl points at the v2 executions resource. Adds CLIENT_CLOSED_REQUEST/SERVICE_UNAVAILABLE to the v2 error codes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(api): v2 executions status + cancel with queued backfill GET /api/v2/workflows/[id]/executions/[executionId] is the single status URL for sync and async runs: before the async worker writes the durable log row, status is backfilled from the job queue (deterministic job id) as 'queued'/'running' — closing v1's 202-to-pickup 404 window — and failed runs carry the structured error object. POST .../cancel renders the shared cancellation lib in the v2 envelope with the tightened 5-value reason enum. Both authenticate via the shared resolveV2WorkflowAccess (X-API-Key, authz masked as 404, allowPersonalApiKeys honored). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(execution): workflow tool + MCP bridge run in-process workflow_executor (workflow-as-agent-tool) short-circuits in executeTool through WorkflowBlockHandler — the same invocation boundary canvas child workflows use — mirroring the deployed_block_executor precedent. The MCP serve bridge calls executeWorkflowService directly instead of fetching its own execute endpoint; deployment-version pinning, MCP response-size rejection, and the actor override become typed options instead of header sniffing. Both callers drop the double admission slot and duplicate top-level log row the HTTP hop cost, and failed child runs now surface the structured error + child executionId so parents and MCP clients can route on error class and hand providers a reproducible handle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(infra): CORS + CSP coverage for the v2 execute path /api/v2/workflows/:id/execute gets the same wildcard-origin, credential-free CORS policy as v1 (the default credentialed policy would block browser API-key calls and open a cookie CSRF surface) with X-Sim-Stream-Protocol allowed and no X-Execution-Mode (async is body-selected on v2), plus the COEP/COOP/CSP header block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(ui): deploy modal + copilot advertise the v2 execute surface All 20 API-tab snippets move to POST /api/v2/workflows/{id}/execute with the nested {"input": ...} body, async as the "async": true body flag (X-Execution-Mode gone), status polling against the v2 executions resource, the third tab renamed Usage and pointed at /api/v2/billing/usage, and {data} envelope unwraps in the printed responses. Fixes the latent baseUrl derivation (endpoint.split('/api/workflows/')) that would have silently built garbage URLs under a v2 endpoint, and deletes dead code (exampleCommand across 3 sites, getAsyncExampleTitle). Copilot deploy/manage/serializer endpoint builders and the api_trigger bestPractices example follow (the latter also drops its hardcoded staging host). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * docs(api): document the v2 execution surface Adds execute, execution status, and cancel to openapi-v2-workflows.json with the structured ExecutionError schema (append-only code enum + block attribution) and the ExecutionResource contract, documenting the rules that differ from v1: modes are body-selected, a failed run is HTTP 200 with status 'failed', an executionId always means data (never the error envelope), queued status is visible immediately, and Response-block payloads stay inside output. Registers the three pages in the generated workflows meta.json and bumps the route-count baseline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(api): gate the whole /api/v2 surface behind one flag; UI stays on v1 Every v2 route now runs exactly one check immediately after auth — v2ApiGateError — and answers 404 when the `v2-api` flag is off, so the surface is invisible until it is deliberately rolled out. The gate is keyed on userId only: a workspace/org-keyed check would have to read membership for a caller-supplied id before authorization runs, and its 404-vs-403 split would leak cohort membership (the trap the per-domain table gate worked around by running late). The two executions routes inherit it from the shared access resolver; the tables-specific gate is removed so no route checks twice. `tables-v2-api` stays, now gating only the internal predicate-grammar route /api/table/[tableId]/query — note v2 tables routes move to the unified flag, so enabling them is a `v2-api` decision now. Reverts the deploy modal, copilot handlers, and api_trigger example to the v1 execute endpoint: v1 works unchanged, and the UI must not advertise a surface most users would get a 404 from. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz * feat(cli): CLI contract for the v2 surface, incl. execution Adds `packages/sim-cli/src/contract` — the declarative definition of how the terminal maps onto the API — and folds in the v2 execution endpoints that just landed on improvement/v2-endpoints. ## The contract Read it as a diff against what is already derivable, not a listing. Method, path, path params, field types, enum values, defaults and required-ness all come from the generated operation table (which comes from the Zod contracts), and the command name derives from `<resource> [sub-resource] <verb>`. 23 of 47 operations therefore need no entry at all. The 24 that do carry only what a schema cannot express: - names, where REST overloads one path — `DELETE /rows` vs `DELETE /rows/[rowId]` becomes `batch-delete` vs `delete`, and `DELETE /deploy` becomes `undeploy` - flags, where a field's type misdescribes its meaning — `workflowIds` is `z.string()` that the route splits on commas; no generator can infer that - columns, which are editorial - confirm, for the 8 destructive operations ## Execution `executeWorkflow` / `getWorkflowExecution` / `cancelWorkflowExecution` derive badly (`/execute` and `/cancel` are verbs the deriver reads as nouns), so all three are named explicitly: `workflows run`, `workflows executions get|cancel`. `stream` is marked `omit`: it switches the response to SSE, which the JSON client would try to parse. Advertising a flag that breaks the response is worse than not offering it — a `--follow` command that renders the stream is separate and hand-written, like `files download`. ## Also - Drops `check:openapi-drift`. The branch landed `check:openapi`, which does the same path/method reconciliation plus a recursive field diff and validates doc examples against the real Zod schemas — mine was a strict subset. - Surfaces the new v2 rollout gate in the CLI: it answers 404 for callers outside the cohort, indistinguishable from a missing resource, so a 404 now carries that as a possibility rather than a diagnosis. - `executor/utils/errors.ts` widens instead of casting through `unknown`, which is both more honest (the value is an Error) and keeps the double-cast ratchet at 8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(executor): restore child-cost aggregation dropped by the staging merge Staging's custom-block rewrite deleted `aggregateChildCost` from workflow-handler.ts, and git merged that file cleanly — but this branch's workflow-tool-runner.ts, added for the v2 execute migration, still imports it. A silent semantic conflict: no marker, broken build. Taking staging's rewrite is correct, so the helper is defined locally in its one remaining consumer rather than resurrected in the file staging just rewrote. Same four lines over the still-exported `calculateCostSummary`, so a failed child workflow keeps billing the hosted-key spend it consumed instead of reporting $0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(cli): yaml and text output formats `--output` now takes table | json | yaml | text, settable per-command, via SIM_OUTPUT, or persisted per profile as before. `yaml` joins `json` in rendering the API's raw values rather than the table's formatted cells, so a duration stays `1500` instead of becoming `"1.5s"` — switching format changes the encoding, never the data. Line folding is disabled: valid YAML, but it breaks line-oriented greps and is miserable to read. `text` is tab-separated with no header and no colour — the shape `cut -f2` and `while IFS=$'\t' read` expect, so shell plumbing works on a box with no JSON tool. It uses the rendered cells rather than raw values, since it is a human-ish format for pipelines rather than something to parse. An absent value collapses to an empty field instead of the table's em-dash: `cut` returning a literal `—` would read as a value to every downstream emptiness test. A bad `--output` is now an error (commander `.choices`) rather than a silent fall back to `table`. The environment variable and the config file stay tolerant — those are ambient and set once, so a bad value should not break every command, but a flag just typed should not be quietly disregarded. Uses js-yaml 4.3.0, already a direct dependency of apps/sim, rather than adding a second YAML library to the monorepo. Also drops a stale README reference to check:openapi-drift, which the v2-endpoints merge superseded with the deeper check:openapi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * refactor(tables): make lib/table/orchestration the single implementation (#6134) * refactor(orchestration): move the shared error contract out of lib/workflows OrchestrationErrorCode and statusForOrchestrationError are the contract every lib/[resource]/orchestration module returns against, but they lived inside the workflows module, so resource-neutral code (lib/folders) already had to import from a workflow path. Moved to lib/core/orchestration/types. Adds a 'locked' class mapping to 423. Both tables and workflows have a lock that forbids a mutation, and each caller was translating that to a status itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(tables): make lib/table/orchestration the single implementation Column update was implemented four times — the UI route, v1, v2, and the copilot table tool — each calling the same column services but owning its own guards, error mapping, and audit. The copies had drifted, and the drift was the bug: v2 was missing both guards, only the copilot copy minted stable option ids, and only v1/v2 audited. performUpdateTableColumn, performDeleteTable, and performDeleteTableRow now own that logic; all ten call sites reduce to auth, parse, call, render. The guards are asserted once in lib/table/orchestration rather than four times against four routes. Behavior this consolidates, previously true on only some paths: - The typeChanging guard. updateColumnType early-returns on an unchanged type and drops any options sent with it, so restating the current type alongside new options silently discarded them. v2 had no guard at all and, since its contract shares v1's body schema, accepted options and ignored them. - The select-unique guard. Each write is its own locked transaction, so a rename or type change paired with a constraint write that is going to fail commits first and then throws, half-applying the schema change. - Stable select-option ids. Cells reference the option id, so an edit that re-sends an option by name has to reuse it or every cell holding it is orphaned. Only the copilot path did this; normalizeSelectOptionsInput moves to lib/table/select-options and now covers every caller. It preserves a supplied id, so it is a no-op for the fully-formed options the HTTP contracts accept. - required forwarded into the type and options writes, so a conversion validates against the constraint the same request is setting. - An audit on every successful update. The UI route and the copilot tool emitted none. - Single-row delete through the row service. v2 did a raw db.delete, skipping assertRowDelete and deleteOrderedRow, so a delete-locked table returned 200 and the row-count bookkeeping never ran. - The delete actor handed to deleteTable, which audits only when a row was actually archived. v1 and v2 omitted it and audited themselves outside that check, emitting TABLE_DELETED for a no-op delete of an archived table. Failure classes come back as OrchestrationErrorCode; v2 renders them through a new v2ErrorForOrchestration, mirroring statusForOrchestrationError on the v1 and UI surfaces, so a given failure maps to the same status everywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(tables): bind the column-update tests to the orchestration function The base's route tests assert which column service each payload reaches — the behavior that now lives in performUpdateTableColumn. They mocked the `@/lib/table` barrel; the orchestration module imports the service directly, so they mock that too and keep asserting the same thing through the extracted implementation. The orchestration tests move onto the base's semantics: writes address the stable column id, a rename rides inside the write it accompanies rather than running first, and the currency guards replace the non-select options guard the service now owns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(copilot): drop the column-type import the delegation made dead Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(tables): move the audit log out of the table service `lib/table/service.ts` wrote its own audit rows, so whether an operation was audited depended on which function a caller reached for rather than on a user having performed it. That is what let v1 and v2 audit a no-op delete, and what made `deleteTable`'s optional `actingUserId` double as an audit opt-out flag. Worse, most sites fell back to `actingUserId ?? createdBy`, so an unattributed call was logged against the table's *creator*. The copilot `mv` path passed no actor at all: renaming someone else's table recorded them as the renamer. Audit now lives in the orchestration functions — performDeleteTable, performRenameTable, performMoveTableToFolder, performUpdateTableLocks — and the services just write. Internal callers (folder cascade, import rollback) keep calling the service and are silent by construction rather than by remembering to omit an argument. Two services now return what the audit needs: `deleteTable` reports whether it actually archived a row, so a repeat delete logs nothing; `updateTableLocks` returns the before/after locks, since only the locked write can observe the transition its description names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tables): restore audit provenance and conflict status in orchestration Moving the audits into the orchestration functions dropped three things the routes had been carrying, and added one the orchestration now owns twice. - The v1 and v2 column-update routes passed `request` to `recordAudit`, so their audit rows recorded the caller's IP and user-agent. The orchestration function had no way to receive it. Every table orchestration function now takes an optional `OrchestrationRequestContext` and every HTTP route forwards it; the copilot and VFS callers, which have no request, omit it. - `classifyTableMutation` matched `TableConflictError` on "already exists" appearing in the message and reported it as `validation`, turning the UI route's 409 on a duplicate table rename into a 400. It now matches the type, the way `performRestoreTable` already did. - `captureServerEvent` ran on every delete while the audit was gated on a row actually being archived, so a repeat delete of an archived table still reported `table_deleted`. Both now hang off the same evidence. - The copilot delete path kept its own `captureServerEvent` from when the service did not emit one, double-counting every copilot table delete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a * fix(tables): say which type a no-op column update restated A copilot `update_column` payload whose only content was the column's current type used to return success with the live schema, while the v1, v2, and UI routes rejected the same payload with "No updates specified". Delegating to `performUpdateTableColumn` unified them onto the routes' rejection — correct, but the message tells the caller its request was empty when it named a type. The orchestration function now reports the same thing `updateColumnType` reports when it loses this race concurrently: the column is already that type, re-issue without the type change. An empty payload still reads "No updates specified". Drops the copilot's `outcome.table ?? tableForUpdate` fallback with it — the comment described the no-op that can no longer reach that line, and a success always carries a table. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a * refactor(tables): classify failures by type instead of by message text The table module decided HTTP statuses by searching error messages for phrases. `VALIDATION_MESSAGE_FRAGMENTS` and `ROW_WRITE_ERROR_PATTERNS` held 32 substrings between them, and fifteen more lists were inlined in routes — 83 matchers over 17 files, each its own copy of the guesswork and already drifted apart. It made message wording load-bearing: `TableRowLimitError`'s own doc comment noted that its text had to contain "row limit" for a route to answer 400, and adding "already exists" to a rename message silently demoted a 409 to a 400 (the bug fixed one commit ago, by adding another special case). Services now throw `OrchestrationError`, which carries the transport-neutral `OrchestrationErrorCode` the layers above already speak. Classification is one `instanceof` in `orchestrationErrorResponse` (UI + v1) and `v2CaughtOrchestrationError` (v2). Every pattern list is gone. Wording is free to change; an unclassified error still becomes a generic 500, which is what an unexpected fault should be. `asOrchestrationError` walks the `cause` chain rather than testing the caught value directly: drizzle wraps a throw raised inside a transaction callback in a `DrizzleQueryError` whose own message is the failed SQL, so a bare `instanceof` would drop every failure raised inside `withLockedTable`. That is the same reason `rootErrorMessage` had to dig for a root cause before. Three throws stay bare `Error` deliberately — `Table ID mismatch`, `Workspace ID mismatch`, and `Failed to build upsert conflict predicate` are internal invariants no consumer classified, and they keep falling through to a 500. `Insufficient capacity` was in the pattern list with no producer anywhere in the codebase. Status changes, all deliberate: - `'forbidden'` joins the code union so the table-row-limit ceiling keeps its 403; without it this refactor would have flattened it to 400. - import-async's table-limit rejection: 400 -> 403, matching the two other create routes it had drifted from. - Renaming a table to an invalid name: 500 -> 400. `validateTableName` messages don't contain "Invalid", so no matcher ever caught them. - Restoring a table that isn't archived, or into an archived workspace: 500 -> 400. - A duplicate *column* name stays `validation`/400 rather than becoming a 409 like a duplicate table name. Both v1 and the orchestration have always answered 400 for it; changing a published status is not this refactor's job. The twelve tests that changed were asserting the substring mechanism itself, constructing plain `Error`s with magic strings. They now assert the real contract, plus new cases pinning that identical wording carrying no classification stays internal and keeps its message off the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * refactor(cli): output format is a profile setting, not a flag Drops `-o, --output`. Format is set once per profile with `sim configure --set-output <format>`, or overridden ambiently with SIM_OUTPUT for a one-off (`SIM_OUTPUT=json sim logs list | jq`) and for CI, which already runs file-less on env alone. Both remaining sources are ambient — set once, then read by every later command — so an unrecognized value falls back to `table` rather than breaking the CLI. There is no longer a strict tier, because there is no longer anything typed per-invocation to be strict about. Frees `-o` for `sim files download -o <path>`, which previously had to share the short flag with a global that meant something else entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * feat(cli): runtime that builds every command from the contract Turns the CLI contract into working commands. 43 leaves across 7 groups, up from the 6 hand-written ones — every v2 operation the contract does not hide is now reachable, including `sim tables upsert`, `sim workflows run`, and the whole tables surface. ## What the generator now emits `V2_OPERATIONS` carries a field→slot map per operation: each query/body field's kind, whether it is required, its enum values, and its server-side default. Types alone could not drive this — the runtime has to *iterate* fields to build flags, and everything from argv arrives as a string, so it needs the kind to turn "50" into 50 and '{"a":1}' into an object. It also lifts each operation's one-line `summary` from the OpenAPI specs. The contracts carry validation, not prose, so `--help` had been showing raw URLs; the specs already hold a written summary per operation and `check:openapi` guarantees one exists, so this reuses documentation rather than inventing a second place to describe the same endpoint. ## The runtime `derive.ts` names a command `<resource> [sub-resource] <verb>` from the route, covering 41 of 47. `request.ts` assembles the call: path params from positional args, `workspaceId` injected from the profile into whichever slot declares it, everything else coerced and validated locally — so a bad enum, malformed JSON, missing required flag, or absent workspace fails before any network call. `build.ts` constructs the commander tree, auto-pages cursor lists up to `--limit` (0 for everything), and renders through the contract's columns or, for runtime-shaped rows, keys unioned across the page. Fixed while wiring: `new Command('upsert <tableId>')` makes the *whole string* the command name, so `sim tables upsert` never matched and fell through to the group's help. Arguments have to be declared with `.argument()`. ## What stays hand-written Two leaves, each for a reason generation cannot satisfy in principle: `files download` streams binary rather than the JSON envelope, and `tables rows list` discovers columns from user-defined row data nested under `data`. They attach onto the generated groups, so `sim files --help` lists them alongside the rest. The five previous command files are deleted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(cli): review round 1 — flag lookup, terminal controls, download safety ## CLI flags silently dropped (Cursor, High) Commander camelCases every multi-word flag, so `--min-duration-ms` is stored as `minDurationMs`. `buildRequest` looked flags up by their own kebab name, found nothing, and dropped the field — no error, it just never reached the API. That was every multi-word flag on every generated command. The unit tests passed because they fed flag values already keyed by flag name, which is not what commander produces — they validated a fiction. Added `build.test.ts`, which parses real argv through the built commands; three of its assertions fail against the previous code. The old tests now use camelCase keys with a comment saying why. ## Terminal control sequences (Greptile, P1 security) `stripAnsi` matched only SGR (`ESC [ … m`), so a knowledge document, table cell, or workflow name could carry OSC, non-SGR CSI, or `ESC c` through to an interactive terminal — setting the window title, moving the cursor to overwrite what was already printed, or resetting the terminal. Replaced with a `sanitize` covering OSC (BEL- and ST-terminated), CSI, any ESC + printable, and the bare C0/C1 range, keeping tab and newline. Applied where API values become display text, so the colour the CLI adds afterwards still works. ## Downloads (Greptile, P1 ×2) `createWriteStream` truncated silently, and the destination name usually comes from the server's content-disposition rather than anything the caller typed — so a download could irreversibly replace an unrelated local file. Now opens `wx` and fails with a message naming `--force`, which was added for the deliberate overwrite. The stream's error listener was attached after the read loop finished, so an EEXIST/EACCES/ENOSPC during writing was an unhandled 'error' event that took down the process. It is now registered before the first write and raced against the pump. ## Personal-key caption (Cursor, Low) With "No workspace (personal key)" picked, the caption still promised a default workspace the approval does not send. It now distinguishes no-pick from picked-but-not-admin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(cli): review round 2 — body-cursor paging, timestamp sanitization ## `tables rows query` printed nothing (Cursor, High) `isCursorList` only looked for `cursor` on the query slot, but `queryRows` is a POST whose whole filter — cursor included — is in the body. It therefore took the single-request path, which handed an array of rows to `printRecord` and printed an empty record, and it never auto-paged past the first page. Replaced with `cursorSlot`, which checks both slots and tells the pager where to put the cursor back. Added a defensive branch so an array reaching the single-resource path renders as a list with inferred columns rather than silently printing nothing. ## Invalid timestamps bypassed sanitization (Greptile, P1 security) `timestamp()` echoes an unparseable value verbatim, and that value is still server-supplied — so the branch was a way past every other formatter for the control sequences round 1 closed. Now sanitized on that path too. Audited the remaining formatters: no other path returns a server value unsanitized. Both fixes have tests that fail against the previous code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(cli): review round 3 — poll retry, download flush errors Both findings are flaws in round 1's fixes rather than in the original code. ## A redeemable login was thrown away (Cursor, High) `pollForKey` treated every non-429 status as terminal. But the poll route releases its mint reservation on any mint failure — its own comment says "a later poll can retry" — so a transient 5xx or a same-second name conflict ended the login after the user had already approved in the browser, forcing a full restart for something the server had deliberately left recoverable. Retryable is now 409, 429, and 5xx. Everything else stays terminal: 400 means a malformed request id or verifier and 401/403/404 mean the server is refusing on purpose, so retrying those would just spin to the 15-minute timeout. ## A failed download reported success (Greptile, P1) `file.end(resolve)` passes the flush error to the callback as its argument, so the pump fulfilled *with* the error and the command printed "Saved" for a truncated file. Confirmed against node directly — `end`'s callback receives the errno. It now rejects on that argument, which is the path an ENOSPC actually takes, since the bytes may not reach disk until the final flush. Adds `device-flow.test.ts` (11 tests: the retry matrix, transport failure, terminal refusals, and that the poll secret never enters the browser URL) and `hand-written.test.ts` covering the download's overwrite guard and flush failure. The two retry tests fail against the previous code; the flush test needs `/dev/full` and so runs in CI rather than on macOS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(cli): review round 4 — repeated flags encode per field kind `coerce` comma-joined every `list` flag, but that is only correct for the three fields whose wire type is a `string` the route splits (`workflowIds`, `folderIds`, `triggers`). The others genuinely want an array: - `rowIds` and `selectedOutputs` are `array`, so joining sent a string where the schema expects a list — `sim tables rows batch-delete --row a b` failed validation, and so did a single `--row a` - `knowledgeBaseIds` is a string-or-array union whose array branch is the right one; joining made `kb_1,kb_2` a single bogus id, so multi-`--kb` search silently searched nothing `list` now means only "accept the flag more than once" — the encoding follows the field's kind, which the generator already records. The two questions were conflated under one contract field and the `FlagSpec` doc now says so. Four tests, three of which fail against the previous code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(cli): review round 5 — header sanitization, auth ordering, stale suggestion ## Table headers stayed executable (Greptile, P1 security) Round 1 sanitized cell *values* but not the column *names*, and a table's columns are user-defined — so the same control sequences were still executable one row higher, in the header. Sanitizing is now done inside `renderTable` rather than at each call site, so a future column source cannot reopen it, with the two key-derived column builders covered as well. ## Fresh install was told the wrong first step (Cursor, Low) Generated commands read `profile.workspaceId` directly, bypassing `requireWorkspace()` — which checks the key first precisely so a new user is told to log in rather than to set a workspace they cannot use yet. That ordering was fixed for the hand-written commands earlier and reintroduced by the runtime. `sim tables list` on an empty profile now says "Not logged in" again. ## A stale suggestion shadowed the fallback (Cursor, Medium) The picker took `selected ?? suggestedWorkspaceId ?? lastActiveWorkspaceId`. The suggestion comes from a profile the CLI wrote earlier, so it can name a workspace the user has since left — and merely being truthy, it blocked the last-active fallback and left the card on "no workspace" with a perfectly good one available. It now counts only when it resolves against the loaded list. Two of the three have tests that fail against the previous code; the third is verified end-to-end (`sim tables list` on an empty profile). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * feat(api): add v2 endpoints for MCP servers, skills, custom tools, folders, and credentials (#6150) * feat(api): add v2 endpoints for MCP servers, skills, custom tools, folders, and credentials * fix(api): correct credential role, skill permission bar, MCP url identity, and custom-tool conflict mapping * fix(api): align credential mutation gating, provider-outage status, and unique-violation conflicts * fix(api): close unique-violation, revival, orphan-write, and env-rename gaps * fix(api): treat every provider-outage code as unavailable on create and update * fix(credentials): use the shared outage predicate on the session update path * fix(contracts): anchor the predicate double-cast annotation to the cast `check:api-validation:strict` counted 9 unannotated double-casts against a baseline of 8, failing CI. The predicate leaf schema was annotated, but the annotation sat above the declaration while the checker anchors on the line carrying the cast — five lines below, at the close of the object literal. The scanner walks back at most three lines and stops at the first non-comment one, so it hit `value: z.unknown().optional(),` and never saw the reason. Splitting the object schema from the cast puts them adjacent, so the existing reason binds. No behavior change — the cast, the schema, and the reasoning are unchanged. Also lowers the rawJsonReads ratchet 6 -> 5 to match the current count, which had drifted down; leaving it high lets a removed raw read silently come back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(skills): point the orchestration error contract at its moved module #6150 branched before #6134, so skill-lifecycle.ts imports @/lib/workflows/orchestration/types — the module #6134 moved to @/lib/core/orchestration/types. Git merged a file deletion on one side with a new file referencing it on the other: no textual conflict, broken build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(cli): pick up the new v2 domains; discover modules instead of listing them Merges `v2-api-spec` (#6150 — v2 endpoints for MCP servers, skills, custom tools, folders, credentials) and the newer `improvement/v2-endpoints`. ## The generator was list-driven, so none of it would have appeared `DOMAINS` and `SPEC_FILES` were hardcoded. Five new contract modules and a new `openapi-v2-resources.json` had landed, and the generator would have skipped every one — silently, with `--check` still passing, because the generated file matched a generator that never looked. Both are now discovered from disk. That is the same silent-drop class the review rounds kept surfacing, and it is the property the whole pipeline rests on: a new v2 domain should reach the CLI by regenerating, not by remembering to edit a list. Result: 47 → 72 operations, 13 contract modules, and 25 new commands (`sim skills list`, `sim mcp-servers get`, `sim folders delete`, …) with no CLI change beyond the discovery fix. Summaries for the new domains now resolve too, so their `--help` reads properly instead of falling back to `METHOD /path`. ## Confirmation gates for the new destructive operations Five new DELETEs arrived ungated. `deleteFolder` is the sharpest — the route archives the folder *and cascades to its contents* — so its message says so rather than reading like a single-item removal. Added a test asserting every DELETE carries a confirmation, with `undeployWorkflow` the one documented exception (reversible by redeploying). It fails against this commit's own starting state, so the next domain to arrive cannot land ungated the way these did. ## One fix outside the CLI `lib/skills/orchestration/skill-lifecycle.ts`, added by #6150, imports `OrchestrationErrorCode` from `@/lib/workflows/orchestration/types`, which does not exist — the type lives in `@/lib/core/orchestration/types`, where every other consumer reads it. The branch does not type-check without this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj * fix(cli): render single-key resource envelopes, and column the new domains `sim mcp-servers create` created the server, exited 0, and printed nothing. The v2 route answers `{ data: { mcpServer: {...} } }`, and the record renderer keeps only scalar fields — one key holding an object left it with none. Unwrap a lone object-valued key before rendering; a payload with siblings (`{ row, operation }` from upsert) is a real result and is left alone. The five domains that arrived with the last generation had no contract columns, so `mcp-servers list` inferred 20 including `hasOauthClientSecret`. Give each a column set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU * refactor(knowledge): make lib/knowledge/orchestration the single implementation (#6154) * refactor(knowledge): make lib/knowledge/orchestration the single implementation Knowledge base create was implemented four times — the internal route, v1, v2, and the copilot tool — and the orchestration around the shared write had drifted. Extract it the same way lib/table/orchestration was: services write, orchestration decides which writes run, guards them, audits them, and returns a transport-neutral failure. Behavior converged, not preserved: - One chunking default (DEFAULT_CHUNKING_CONFIG). The agent defaulted minSize to 1 against the API's 100, so identical input produced differently-chunked knowledge bases depending on who created it. The agent path now chunks at 100. - Every successful mutation is audited inside the orchestration function. The copilot tool called recordAudit zero times, so agent-created knowledge bases, document uploads, updates and deletes left no audit trail at all. - Failures classify by class, not by message text. The knowledge service errors are OrchestrationError subclasses and storage-quota rejections throw a shared StorageLimitExceededError, replacing four separate message greps for "already exists" / "does not have permission" / "storage limit". delete_connector reported the opposite of what happened. It reached the route through an internal HTTP self-call that sent no query string, so the route's keep-documents default always applied while the agent told the user the documents had been removed. The self-call is gone — all four connector operations run in-process — and the orchestration returns the real counts. Also: - OrchestrationErrorCode gains 'payload_too_large' (413 / PAYLOAD_TOO_LARGE). Without it, dropping the storage-limit message match would have regressed the documented 413 on knowledge base create and document upload to a 500. - messageForOrchestrationError renders a route's own wording for an unclassified fault, so a driver's message no longer reaches the client on a 500. - v1 and v2 knowledge base update now forward actorUserId, which the service requires for a workspace move; both omitted it. - The connector DELETE route reads deleteDocuments through parseRequest. Its contract declared z.boolean(), which would have rejected the string a query param actually is. - Drop the 409 from POST /api/v2/knowledge/{id}/documents in the OpenAPI spec. Nothing on the upload path throws a conflict; it was only ever reachable by the message match this change removes. Behavior change worth noting: a v1/v2 PUT carrying only the workspaceId scope field and no actual updates now returns 400 rather than 200 with the unchanged knowledge base. Deliberately deferred: document update remains internal-only. Extracting performUpdateKnowledgeDocument makes exposing it on v1/v2 a contract and a route away, but that is a new public surface rather than part of this consolidation. * fix(knowledge): make connector create atomic and stop flattening failures Review round 1 on #6154. - Resolve the billing payer before the connector is committed, not after. A malformed attribution header rejected post-commit left a live connector behind a 500, and a retry created a duplicate plus duplicate sync work. Manual sync resolves before writing its audit for the same reason. - Let the source-config validator carry its own failure class. Collapsing every rejection to `validation` flattened the connector PATCH route's 401 (stale stored credential) and 409 (missing workspace context) into a 400. - Add `unauthorized` to OrchestrationErrorCode. It is the class that 401 was already expressing on this route, and the v2 vocabulary already had UNAUTHORIZED; only the shared union was missing it. - Report a knowledge base that exists but failed to archive as failed, with the reason, rather than as not found. The copilot delete loop folded every non-not-found failure into `notFound`, telling the user it was never there. - Route copilot failures through the same message helper the HTTP surfaces use, so an unclassified fault's raw text (a driver's failed SQL) no longer reaches the agent verbatim while the UI and public APIs get the generic wording. * fix(cli): stop dropping nested fields, and emit exports as documents `sim workflows export <id>` printed `version` and `exportedAt` and nothing else. The record builder kept only scalar fields, so `workflow` and `state` — the entire export — were discarded with nothing to say they had been. Same for `workflows get`, which silently dropped `variables` and `inputs`. Record views now render every field. Nested values serialize to one line and are cut at 160 chars: visibly partial beats silently absent, and json/yaml output still prints them whole. Export is a document, not a record — it exists to be redirected to a file and fed back to `import`, and table/text flatten and truncate, so neither can round-trip it. `document: true` in the contract makes those formats fall back to JSON; yaml is honoured because it round-trips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU * feat(cli): JSON flags accept @file and @- alongside inline JSON A workflow export is hundreds of lines, and `--workflow` only took it inline. The shell makes that miserable: unquoted `$(cat wf.json)` word-splits into broken JSON, and nothing in the help said passing a file was an option. Every JSON flag now reads `@path`, or `@-` for stdin, so the round trip is `sim workflows export <id> > wf.json` then `import --workflow @wf.json` — or one pipe. `@` cannot collide with a real value because JSON only ever starts with `{ [ " -`, a digit, or t/f/n. Stdin drains with a readSync loop rather than readFileSync(0): a pipe is opened non-blocking, so the single-read form returned EAGAIN and died with a raw stack trace exactly when the upstream process had not written yet. Parse failures that look like a filename now say so — naming @path, or the file itself when the bare value turns out to exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148a65fkPhP4N8tgGPYtRTU * feat(api): expand the public v2 files surface (#6160) * feat(api): expand the public v2 files surface Adds folder support, rename/restore, move, bulk archive, share, and content replace to /api/v2/files, so managing files by API no longer stops at upload + download + archive-one. Routes are thin: auth -> parse -> perform* -> serialize. Share and content replace get their orchestration extracted first so the session routes and the public ones cannot diverge on the effective-authType resolution, the EE public-sharing gate, or the storage-quota classification. Presigned upload stays session-only: presign does an advisory quota check and the real debit happens in the separate register step, so a caller that never registers leaves unaccounted bytes with no reaper. The buffered multipart path debits inside uploadWorkspaceFile's own transaction. * fix(files): classify folder and content failures instead of 500ing them Bugbot round 1. The v2 routes map errorCode straight to a status, so every manager failure that arrived unclassified became a 500 for what is really a caller-fixable 400 or 404. - Folder manager throws OrchestrationError: missing target/folder -> not_found, reparent cycle / self-parent / restore-into-archived-workspace -> validation. - File manager does the same for the in-transaction 'File not found' paths that the earlier pass missed. - updateWorkspaceFileContent's outer catch re-wrapped everything in a bare Error, which stripped the class off StorageLimitExceededError and the new not_found alike. It now rethrows a classified failure untouched and attaches cause to the generic wrap, so asOrchestrationError can still walk the chain. - Every remaining perform* gained the asOrchestrationError branch. - renameWorkspaceFile returned the pre-update read, so the v2 PATCH reported a stale updatedAt; it now returns the timestamp it actually wrote. Docs: upload auto-suffixes a duplicate name rather than rejecting it, matching the in-app uploader. The description claimed 409 and was simply wrong. * fix(files): surface a failed upload read-back as the real error getWorkspaceFile swallows a query failure and returns null unless throwOnError is set, so a transient blip on the post-upload read reported as 'file could not be read back'. Distinguish the two: a real null after a just-committed write is an invariant break, a query failure is itself. * revert(api): drop the dedicated v2 file-folder routes File folders already live in the shared folder table as resourceType 'file' (#6045 cut them over, #6051 dropped workspace_file_folders), and the remaining file-specific folder machinery is being folded into the generic folder engine. Publishing /api/v2/files/folders/** would pin that transitional split into a public contract we'd then have to keep or break. Files stay folder-aware — folderId/folderPath on the projection, folderId on upload, and the move route — because a folder id is a folder.id and survives the unification untouched. Folder management belongs on /api/v2/folders once that surface serves resourceType 'file'; until then there is no v2 way to enumerate file folders, which is the deliberate gap. The orchestration classification fixes stay: the internal routes and the copilot file-folder tools still call those perform* functions. * fix(files): classify upload failures instead of matching their wording Bugbot round 2. uploadWorkspaceFile had the same outer-catch rewrap that updateWorkspaceFileContent did, so a blown storage quota reached the route as a bare Error and the v2 handler recovered the status by substring-matching the message. Any rewording silently demoted a 413 to a 500. - uploadWorkspaceFile rethrows a classified failure untouched and attaches cause to the generic wrap. - FileConflictError is now an OrchestrationError('conflict'), so a duplicate name classifies like every other conflict. Its 'FILE_EXISTS' discriminator had no readers and is gone; the instanceof checks elsewhere still hold. - The v2 upload handler uses v2CaughtOrchestrationError, dropping all three string matches. Also documents that bulk-archive is best-effort: unknown or already-archived ids are skipped rather than failing the call, and deletedItems is what actually happened. That asymmetry with the single-id DELETE was undocumented. * feat(cli): wire the expanded v2 files surface Regeneration picked up seven new operations (72 → 79), every one of which derived badly. `/files/move` and `/files/bulk-archive` put a verb where the deriver expects a sub-resource, so each became a group holding a lone `create`; `GET /files/[id]/share` fetches one share a…
…olders (#6721) * feat(resources): multiselect on tables and knowledge, spring-loaded folders Tables and Knowledge lists get the checkbox multiselect Files already had — selection, shift-click ranges, select-all, and a shared bulk action bar for move and delete. Dragging a resource onto a folder row and resting there now opens that folder, so nested filing is one gesture (macOS Finder spring-loading). Works on Files, Tables, and Knowledge. Selection, the action bar, the drag payload, the drag ghost, and drag teardown are extracted to shared modules; Files migrates onto them rather than keeping its own copies. Bulk move and delete land as single authorized operations that take folders and resources together, so a mixed selection commits once instead of fanning out. Fixes two latent UI bugs: the drop-target outline referenced --accent, an HSL-channel token only valid via hsl(), so it silently rendered as currentColor; and rows painted hover and selected with the same surface token, making the two states indistinguishable. * chore(audits): re-record route ratchet after merging staging * fix(bulk): reject a move target inside the moving subtree and report contained folders deterministically * improvement(resources): neutral drop affordance, longer spring delay, and a body drop target * fix(resources): drop into the open folder on Files, return on an unused spring-open, and guard bulk caps * fix(drag): end a drag on pointer resume instead of an idle timer * feat(resources): drag onto breadcrumbs to move back up, and round the drop ring * fix(resources): tint the list region on drop instead of ringing it * refactor(folders): share the spring-navigation lifecycle so Files returns too * fix(breadcrumbs): accept a drag on the open-folder crumb too * fix(files): keep the view in a spring-opened folder when an OS upload lands there
…rocessing (#6735) * fix(knowledge): bound chunking separators so one config can't stall processing `chunkingStrategyOptionsSchema.separators` accepted an arbitrary-length array of arbitrary-length strings, next to a `pattern` field already capped at 500 chars. `RecursiveChunker` splits the whole document once per separator and walks the list from the top for every oversized fragment, so a persisted config with thousands of non-matching separators cost seconds of synchronous CPU on every later document upload — work neither the processing `Promise.race` timeout nor the after-the-fact chunk-count cap can interrupt. Measured on a 21.3 MB document: 632 ms at 100 separators, 6.1 s at 1000, 36.8 s at 5000. - Bound `separators` to 32 entries of at most 100 characters on the write path. The largest built-in recipe (markdown) uses 16, so hand-tuned lists still fit. - Keep the stored/read shape tolerant, so a config written before the bound still lists instead of failing response validation. - Clamp in `RecursiveChunker` too, with a warning, so an already-persisted oversized list cannot reach the split loop. An over-long separator is dropped rather than truncated: a truncated separator matches where the configured one never did, silently re-cutting the document, while dropping it behaves like a separator that finds no match. A list left empty falls back to the recipe. - Walk non-matching separators iteratively instead of recursing, so stack depth no longer tracks the separator count. Verified behavior-preserving against the previous implementation over 4000 randomized configs — byte-identical output. - Validate in the create-base modal so the limit surfaces inline. After the fix the same 21.3 MB document costs ~300 ms at every separator count. * fix(knowledge): gate separator validation on the recursive strategy - The separator refines ran for every strategy, but the field only renders for `recursive` and only that strategy submits it, so a value left behind by a strategy switch could block submit with no visible field to clear. Gated the same way the regex-pattern refine already is. - Use absolute imports in the chunker test, per the repo convention.
…credential (#6737) * fix(credentials): let workspace admins disconnect a teammate's OAuth credential Disconnecting an OAuth credential routed through POST /api/auth/oauth/disconnect, a credential *user* operation scoped to the acting user's own `account` rows. A workspace or org admin acting on a teammate's connection matched no accounts, so the call returned `{ credentials: [] }` and the route still answered 200 — the UI navigated away and the credential was still there. Not a denial, a silent no-op. Route every type through the workspace-scoped credential delete instead, which authorizes against credential admin and already resolves workspace and org admins as derived credential admins for shared credential types. That path only deleted the `credential` row, so send `oauth` through `deleteCredentialRecord` — the manager that also tears down a credential's secret source — and teach it to revoke the backing `account` grant once no credential references it. Scoped by account id, not owner: the caller is already authorized against the credential, and the grant belongs to the teammate. Also make RoleLockTooltip layout-transparent. It wrapped locked controls in an `inline-flex` div, which let the chip shrink to its label while unwrapped controls stretched to the member row's fixed role track — so a credential's own members list rendered Admin at two different widths. A `grid` wrapper stretches like the unwrapped control, aligning the credential, secrets, and skills member lists that share the row. * refactor(credentials): consolidate credential deletion onto one path Follow-ups from review of the previous commit. - Collapse the delete use case's remaining `service_account` carve-out. Both ternary arms reached the same `deleteConnectionCredential` tail, but the carve-out skipped `deleteCredentialRecord`'s Slack custom-bot guard — and custom bots are exactly the type it guards, so the single-delete surface could orphan a credential group that the batch path refuses to. - Make `deleteOrphanedOAuthAccount` one conditional statement instead of a read then a write. `credential.accountId` is ON DELETE CASCADE, so a credential racing the gap would have been reaped by Postgres without `clearCredentialRefs` running, stranding its id in workflow state. - Give oauth its own branch in `deleteCredentialRecord`, matching the shape the env types already use, rather than a conditional tail after the return. - Point `handleReconnectCredential` at the shared helper; it carried its own copy of the same orphan-grant rule. - Invalidate the OAuth connections query on credential delete. The removed disconnect hook owned that invalidation, and the detail page reads it. This also retires the hand-dispatched `oauth-credentials-updated` event from the delete path; the connect/reconnect path still dispatches it for its listener. - Delete `useDisconnectOAuthService`, now callerless. - Trim comments to the behavior rather than the bug that motivated it.
The add_text FFmpeg operation inlined the caller's caption into a single-quoted `drawtext=text='...'` filter option. FFmpeg's av_get_token copies bytes verbatim inside a single-quoted run, so a literal quote in the caption closed the quote and the remainder was parsed as filtergraph syntax. An attacker could inject `drawtext=textfile=<path>` (arbitrary local-file read) or `movie=filename=<url>:f=tty` (read-SSRF), rendering the target file bytes or HTTP response body into the returned video. Route the caption out-of-band: write it to a file the operation owns and reference it via `drawtext=textfile=caption.txt` with `expansion=none`, so the caption bytes never re-enter the filtergraph parser. The caption is referenced by a bare relative filename with FFmpeg's working directory set to the temp dir, because FFmpeg's tokenizer cannot round-trip a single quote inside a textfile= value — an absolute temp path would break add_text whenever os.tmpdir() contains a quote (e.g. a Windows profile). The working directory is passed via execve and never parsed as graph syntax, so any character in it is safe.
… plugin provider mutations (#6738) * fix(auth): fence off plugin-served SSO provider mutation endpoints The auth catch-all forwarded every non-organization POST to the better-auth SSO plugin, leaving sso/update-provider and sso/delete-provider reachable alongside the app-owned sso/register route. update-provider is gated only on provider ownership and merges the caller's samlConfig, so a provider owner could set mapping.emailVerified — a field the register contract deliberately omits and the plugin's identity-boundary guard does not inspect, so it never trips the linked-account conflict. With trustEmailVerified enabled, a subsequent assertion carrying an arbitrary verified email auto-links to that user's account. Block SSO POST paths by default, allowing only the sso/saml2/ protocol endpoints the IdP posts to, mirroring the existing organization fence. * fix(auth): stop trusting IdP email_verified for SSO account linking Better Auth's link gate is `!isTrustedProvider && !userInfo.emailVerified`, so trustEmailVerified let a true email_verified claim stand in for the domain binding. Any principal able to register an SSO provider — an Enterprise org admin, or any signed-in user when self-hosted — could point it at an IdP they control, assert an arbitrary victim's address as verified, and auto-link into that account across tenant boundaries, persisting as an account row. With it off, linking requires isTrustedProvider, which is domainVerified plus validateEmailDomain(email, provider.domain) — a provider can only claim identities inside the domain it proved. That is the model the codebase already documents for trustProviderByName: false. The option only ever set emailVerified on the local row; it was never what made linking work, since Entra omits the claim and SAML ignores it without a mapping the register contract does not accept. * docs(auth): note that the SSO fence and trustEmailVerified are layered * test(auth): drop unnecessary any casts from the auth catch-all tests createMockRequest already returns a NextRequest and the handler mocks are untyped vi.fn()s, so every cast in the file was suppressing type checking for no reason.
…ch bulk authorization (#6748) * refactor(resources): converge Files onto the shared drag hook and batch bulk authorization Files kept a 280-line copy of the foldered-list drag logic because it also accepts OS file drops. The copies had already drifted, so the external drop becomes an option on the shared hook and the copy goes away. - Add `externalDrop` to `useFolderRowDragDrop`: folder rows highlight and spring open for an OS file drag exactly as for a move, while the body and breadcrumb decline so the page-level upload overlay owns those regions - Collapse the three drop-active booleans into one `ActiveDropTarget` union, so exactly one affordance is armed by construction rather than by hand-clearing - Keep drop-target writes identity-stable so `dragover` does not re-render the list on every event - Give each list its own drag MIME again, restoring the cross-surface isolation `drag-payload.ts` documents - Let a folder spring open more than once per drag, so a drag can walk back out through the breadcrumb and descend again; the guard against re-entering the folder already on screen moves to `useSpringNavigation`, the only layer that can state it - Resolve each bulk item against the workspace context the batch already holds, and memoize the effective-permission lookup for the batch, replacing two invariant queries per item - Fill the drop target at `--surface-active`: `--surface-4` is the button-base token and is lighter than hover in light mode, so the strongest row state read the faintest * fix(resources): dismiss the upload overlay on a folder drop and re-check permission per item The drag hook stops propagation on a drop it handles, so the page-level handler that cleared the upload overlay never ran and the chrome stayed up over the finished upload. Both consuming paths now share one dismissal. Drop the batch permission memo: each item in a bulk move or delete commits independently, so reusing one allow verdict let a revocation part-way through a batch go unseen by the remaining items. The workspace context is still resolved once per batch, which was the larger saving.
…es (#6741) Expands the Okta block from 18 to 44 operations, covering the System Log, MFA factors, sessions, applications, administrator roles, and group rules. Adds shared helpers for the SSWS auth header, Okta error parsing, and the Link-header `after` cursor, and routes every tool through them so there is one auth and error path. All eight list operations now return `nextCursor` and `hasMore`. Makes the block's param transform authoritative over the serialized inputs: the executor merges it on top of them, so a key the transform omits keeps the raw subBlock string. Assigning `undefined` is what actually drops it, which is what keeps a non-numeric `limit` from reaching Okta verbatim and stops a blank field in a partial `update_user` from overwriting the stored value with an empty string.
…#6733) * improvement(tables): make Cmd+F search as you type and clear on close * fix(tables): reset the find debounce on close and land Enter on a skipped first match * fix(tables): keep a clicked cell selected when find closes * fix(tables): strand an in-flight match jump when the search term changes * fix(tables): strand match jumps on the live query, not the debounced one * fix(tables): cancel before reveal, and release find's selection on any grid key * fix(tables): drop find's selection restore, commit the term on Enter * fix(tables): block find navigation until the results describe the term * fix(tables): clamp the find step base when a refetch shrinks the match set * fix(tables): track the find cursor by match identity, not position * fix(tables): release the find cursor when its match leaves the result set * fix(tables): skip the reveal when the target match vanishes mid-jump
Adds a Splunk block with 12 REST operations: run search (oneshot), create/get/cancel search job, get search results, list/get/dispatch saved searches, list/get fired alerts, list indexes, and list apps. Bearer-token or basic auth, with optional /servicesNS namespace scoping.
Every tool was validated against the Splunk REST reference. Results use search/v2/jobs/{sid}/results because the v1 endpoint is deprecated and disabled from Splunk Enterprise 9.0.1. A half-specified namespace fills the missing node with the documented - wildcard rather than nobody/search, which would have hidden user-private objects. Dispatching endpoints fail loudly instead of reporting success with a null sid, and Create Search Job rejects exec_mode=oneshot since that mode returns results rather than a search ID. The results and control endpoints tolerate an empty body. saved/searches sends the f field filter the reference prescribes for it.
* fix(okta): stop partial updates erasing stored profile data Post-merge audit of the Okta integration (follows #6741), verified against the OpenAPI spec bundled in okta-sdk-golang/.generator. Two updates could silently destroy data: - `update_group` targets `PUT /api/v1/groups/{groupId}`, which Okta documents as `replaceGroup` — it swaps the profile wholesale. Sending only the two fields the tool exposes erased the stored description on every rename, and dropped every org-defined custom attribute along with it. The tool now reads the group and overlays the supplied fields before replacing, matching the read-modify- write `salesforce_update_custom_field` already uses for the same hazard. - `update_user` gated its profile fields on `!== undefined`, so an empty string reached Okta and blanked the stored value. The block strips blanks before they get there, but the tool is `user-or-llm` and a model routinely emits `""` for a field it has nothing to say about, so the guard belongs on the tool. Also corrected: - `forgetDevices` defaults to true at Okta, so the unseeded switch rendered off while remembered factors were in fact being cleared. - Group rules take a plain keyword on `search`, not the SCIM-style expression the shared Search field's wand generates, so they get their own field. - `get_logs` dropped `limit=0`, which the spec documents as valid. - `get_user` emitted an activation timestamp under `activated`, which the block declares as the lifecycle boolean; the timestamp is now `activatedAt`. - Descriptions that overstated what an endpoint does: `list_users` omits DEPROVISIONED users, `delete_user` deactivates before it deletes, `delete_group_rule` answers 202, and `excludedGroupIds` is always empty because Okta does not support group exclusions. * fix(okta): forward the abort signal through the group read-modify-write * test(okta): rename the shared body-builder helper * fix(okta): key the send-email and search mappings off the operation * docs(okta): use TSDoc for the new block annotations
* fix(credentials): restore reliable OAuth connections * fix(credentials): cover OAuth handoff edge cases * fix(credentials): preserve reauthorization outcomes
…f change detection (#6754) * fix(deploy): resolve the error-output flag from edges on both sides of change detection A block can hold `errorEnabled: false` while an error edge still leaves it. `setBlockErrorEnabled` does not remove existing error edges, both block renderers draw the port on `errorEnabled || hasErrorConnection`, and the executor never reads the flag at all — the edge alone decides routing. So the two spellings are one state, and nothing about the block has functionally changed. Only the deployed side reconciled them. `materializeDeploymentState` backfills `errorEnabled: true` for any block with an error edge, while the live normalized tables are read verbatim. Change detection compared the raw flag, saw `true` against `false` for a block that had not changed, and no redeploy could clear it: deploying snapshots the live `false`, which the next read backfills straight back to `true`. The deploy button sat on "Update" permanently, and the server path (`checkNeedsRedeployment`, which reads the raw jsonb and skips the backfill) disagreed with it. Lift the rule into `@sim/workflow-types` as `collectErrorSourceBlockIds` / `resolveEffectiveErrorEnabled` so the backfill and the comparison share one definition, and apply it to both sides of the diff. Compared outside the structural gate, since the flag can match while the edges disagree. * chore(deploy): use TSDoc for the error-output comparison comments
…aging (#6750) * fix(tables): re-check the match at the reveal, not just before paging * fix(tables): only release the find cursor from the reveal that owns it
…bles, and knowledge bases (#6752) * fix(fork): carry folder structure across a fork edge for files, tables, and KBs Only workflow folders were mirrored into the target workspace on fork create and on sync. Copied files, tables, and knowledge bases were written with a hardcoded `folderId: null`, so a push or pull flattened them all into the target root and lost the source's grouping — visible as a fork sync that drops folder structure when copying files to the parent. `resolveForkFolderMapping` already did the real work (prune to folders holding copied content plus ancestors, reuse same-named target folders, remap parentId), but was pinned to `resourceType: 'workflow'` on both reads and on the folder-ceiling check. Parameterize it by resource type and run it per family, threading the resulting map into each copy instead of nulling. The four folder-bearing families own disjoint trees and folder ids are globally unique, so the per-family maps merge cleanly for the `sim:folder/<id>` content rewrite, which previously resolved only for workflow folders. Existing forks are healed on their next sync rather than by a migration: `rehomeFlattenedForkResources` re-homes mapped files/tables/KBs whose target `folder_id` is still NULL — the exact signature of the old flattening — so a placement chosen in the target is never overwritten and the pass converges to a no-op. `BlobCopyTask.targetFolderId` is optional so tasks queued by an earlier deploy replay at the root exactly as before. * refactor(fork): page the re-home lookups and reuse the plan's identity rows Self-review of the folder-transit change surfaced two scaling problems in the re-home pass, both of which grow with the size of the fork edge rather than the size of the sync: - The resource lookups built `IN (...)` lists straight from the edge's mapping rows, so a large fork could hand Postgres a list approaching the bind-parameter ceiling and a pathological query plan. Page them at 500, matching the paging the rest of the fork copy already uses. - The pass re-read the whole edge mapping via `getEdgeMappingRows`, which the promote plan had already loaded in the same transaction — a second full load of identical rows. Expose them on `ForkPromotePlan` and pass them in, which also drops a mock from the re-home tests. Also tally moved rows from `returning()` rather than the planned batch size, so the log line reports what the `folder_id IS NULL` guard actually wrote instead of what was attempted. * fix(fork): drop the sync-time re-home pass, keep folder transit forward-only Review surfaced three findings and every one of them was in the re-home pass, none in the forward-looking fix: - It keyed mapping orientation off `direction`, but the promote route resolves the edge from whichever workspace the caller is acting in, so a caller in the PARENT pushing to its child is `direction: 'push'` with the parent as source. The plan derives this as `sourceWorkspaceId === edge.parentWorkspaceId` for exactly that reason. - Moving a file into a mirrored folder can violate `workspace_files_workspace_folder_name_active_unique`, which would abort the whole promote transaction and take the workflow sync down with it. - `folder_id IS NULL` cannot distinguish "flattened by the old copy" from "the user moved this to the root", so the pass re-applied on every sync and would fight a deliberate placement indefinitely. The first two are fixable; the third is not without a one-time marker per edge, which means a migration. A heal that re-applies forever is worse than no heal, so remove the pass entirely rather than ship it half-right. Folder structure now transits correctly from this point forward, which is the actual reported bug; healing already-flattened resources can be a separate change with a marker to make it run exactly once. Reverts the `ForkPromotePlan.mappingRows` field with it — it existed only to feed this pass.
…d case tools (#6746) * feat(crowdstrike): add alerts, host response, IOC, Spotlight, RTR, and case tools CrowdStrike Falcon shipped only three read-only Identity Protection sensor tools. This adds 20 tools across the response and investigation surface SecOps teams actually automate against. Alerts (current Alerts API): query, get details, update status/assignment/ tags/comment/visibility. Hosts: contain, lift containment, hide, unhide. Host groups: query, get details, add/remove hosts. IOC Management: query, get, create, update, delete. Spotlight: query vulnerabilities, get vulnerability details. Real Time Response: init session, execute a read-only command, poll command status, delete session. Case Management: query cases, get case details. Every endpoint, request field, and response field is taken from CrowdStrike's published surface (developer.crowdstrike.com API reference, FalconPy endpoint definitions, and the swagger-generated gofalcon models). Required API scope is documented in each tool description. Deliberately not implemented: - Detects API: decommissioned 2025-09-30, superseded by Alerts. - CrowdScore Incidents API and behaviors: decommissioned 2026-03-09 and removed from the developer center entirely. Case Management is CrowdStrike's replacement, so its two documented read operations are implemented instead. - Case create/update/merge: the swagger types case `status` and `severity_info.level` as bare strings with no enum, so a correct write cannot be built without guessing. CrowdStrike answers 200 with a populated `errors` array for partial failures. Responses now surface those per-item errors, and an empty result set carrying errors is reported as a failure rather than silently succeeding. The route's shared Falcon client, response normalizers, and operation dispatch move into colocated modules so the handler stays readable at 23 operations. * fix(crowdstrike): correct the RTR read-tier commands and stop dropping the IOC delete filter Validation pass over all 23 tools against CrowdStrike's swagger-generated SDKs (gofalcon falcon/models + falcon/client, FalconPy _endpoint/*.py) turned up four real defects. The Execute RTR Command dropdown offered `csrutil` and a bare `reg`. Neither is a read-tier base command: CrowdStrike's own swagger description for RTR_ExecuteCommand enumerates cat, cd, clear, env, eventlog, filehash, getsid, help, history, ipconfig, ls, mount, netstat, ps, and "reg query". `csrutil` appears nowhere in CrowdStrike's published surface, and `reg` alone is not a base command — the registry variants are "reg query" (read) and "reg set"/"reg delete" (Active Responder). Both entries are corrected everywhere they were repeated: dropdown, tool description, and param description. Delete Indicators showed a Filter input, declared the param, accepted it in the contract, and implemented CrowdStrike's documented filter-takes-precedence rule in the route — but the block never mapped the field into the tool call, so the filter was silently discarded and a filter-only delete failed validation. The `filter` case is now mapped alongside the ID list. A 200 carrying only envelope errors was reported as HTTP 200 with success:false, which reads as a success to anything inspecting status. Failures now adopt the per-item error code the envelope supplies, falling back to 502. Alert updates gain a first-class Remove Tags By Prefix field. The spelling was previously unresolvable, so it was left to the raw action-parameter escape hatch; CrowdStrike's swagger settles it as `remove_tags_by_prefix` in both the PatchEntitiesAlertsV2 and PatchEntitiesAlertsV3 descriptions. Case Management and Spotlight scopes now name the OAuth scope string (case-templates:read, spotlight-vulnerabilities:read) alongside the label the Falcon API client UI shows, so either rendering is findable. * fix(crowdstrike): stop blank sensor filters reaching Falcon and expose the RTR outputs The falcon.ts/normalize.ts/operations.ts split routed query_sensors through the shared buildUrl helper, which skips only undefined. An empty filter or sort string therefore emitted `?filter=` / `?sort=` where the pre-split route omitted the param, sending Falcon an empty FQL expression. Reject blank values in the contract instead, matching the newer operations. Also surface the ten RTR fields the tools already return but the block never declared, and broaden the block metadata past the original sensor-only surface. * fix(crowdstrike): fail query operations on error-only envelopes and guard IOC pagination Falcon can answer 200 with an errors array and no resources. The detail operations already treated that as a failure, but the five query branches returned an empty successful result, so a failed alert query read as a valid no-match to the calling workflow. Blank FQL rejection now covers the alert, host-group, indicator, vulnerability, and case contracts too, not just sensors, and Query Indicators rejects offset combined with after instead of forwarding a pagination pair CrowdStrike refuses. * fix(crowdstrike): stop blank inputs reaching Falcon and restore the dropped output docs The executor merges `tools.config.params` over the raw block inputs, so a key the mapper omitted kept its raw subBlock value — and an untouched subBlock is stored as `null`, which the route contract rejects. Query Alerts with an empty Filter, Update Alerts without every optional field, and Delete Indicators without an audit comment all 400'd before reaching CrowdStrike. Seed every optional key as `undefined` so omission is authoritative, which also stops a value left over from another operation riding along. Shared output consts in `outputs.ts` were silently dropped from the generated docs: the generator scans tool source and resolves consts only from `types.ts`, so `errors`, `affected`, and `pagination` rows vanished from 17 tool pages and every nested property row with them. Inline the literals. Against CrowdStrike's own generated SDKs and developer portal: - add csrutil, ifconfig, users, and the eventlog subcommand forms to the read-tier RTR base commands, matching PSFalcon's ValidateSet - add detection_suppress/detection_unsuppress and cap host actions at the documented 100 ids - cap the IOC search limit at the documented 500, not 2000 - correct the Cases scope to "Cases: Read"; case-templates guards a different collection - type the IOC payload so a blank string cannot clear a stored field on PATCH - send `MsaRangeSpec` bounds capitalized, as the spec serializes them - fail the sensor and RTR-session-close paths on a 200 whose envelope carries only errors, and surface partial sensor errors - give Delete Indicators its own filter so a stale alert query cannot widen it - drop the pre-selected network-isolating host action * fix(crowdstrike): correct the RTR command tier, IOC update contract, and US-3 region Independent re-validation against gofalcon's swagger-generated models and CrowdStrike's developer center turned up several wire-level errors. - Real Time Response advertised "eventlog backup"/"export"/"list", "reg query", ifconfig, and users as base commands. base_command names a command family and subcommands belong in command_string; the eventlog write variants are Active Responder commands that would fail on scope under this Read-scoped tool, and ifconfig/users appear in neither authoritative list. The block now offers the 16 documented read-tier families and the contract enforces them. - Indicator updates accepted an entry with no id, which cannot name a record, and accepted type/value, which the update model does not expose. Creates accepted an entry with no type, value, or applied_globally -- the one property CrowdStrike marks required, and the one that decides fleet-wide scope. - CrowdStrike documents that PATCH overwrites any omitted field with a blank value. The contract can only catch blanks, so the update tool now tells the caller to read the indicator first and resend its full field set. - Added the US-3 commercial region, which was missing from every cloud list. - Aggregate queries silently dropped percents and filters_spec. - Deleted the response-envelope body unwrap: no endpoint this integration calls returns that shape, and getFalconErrorMessage never honored it anyway. - Softened the Detects and Incidents claims to what the sources actually state. A tool description longer than the docs generator's 600-character id-search window silently publishes as an empty string; three descriptions had crossed it. Shortened them and added a test that fails before the catalog goes blank. * docs(crowdstrike): name the endpoint and Identity Protection scope on the sensor tools The three sensor tools were the only ones in the family that named neither their endpoint nor their OAuth2 scope, and none of them said these are the domain controllers Falcon Identity Protection monitors rather than Falcon endpoint sensors -- a distinction an agent choosing between them and the Hosts tools has no other way to make. Identity Protection Entities: Read is also a separate product entitlement from Hosts and Alerts. * refactor(crowdstrike): say which ID caps are CrowdStrike's and which are Sim's Every bulk-ID limit claimed CrowdStrike as its source, but only the sensor (5000), host action (100), indicator batch (200), and Spotlight (400) caps are published. The alert, host group, indicator, and case caps are Sim's own bound on request size, and the validation message now says so instead of attributing a limit CrowdStrike does not document.
#6758) `sub-block.tsx` mounts `useWebhookManagement` for every sub-block in the editor panel, and `getBaseUrl()` throws when NEXT_PUBLIC_APP_URL reads empty, so a missing deployment value took down the whole workflow route instead of the one webhook field. The hook already gates its query and store writes on `useWebhookUrl`; the URL now agrees.
… and knowledge tools (#6747) * feat(servicenow): add semantic incident, change, catalog, approval, CMDB, knowledge, and directory tools The ServiceNow block only exposed generic Table API CRUD, so every real task started with "which table is that on?". This adds 27 semantic tools that wrap the same Table API plumbing under the names customers actually use. - Incidents: create, get by number or sys_id, search, update, resolve, close, and append a work note or customer-visible comment. - Change: create, get, list, update, move state, and list change tasks through the documented Change Management API. - Service catalog: browse items, order one via the Service Catalog API order_now endpoint, and list or get requested items. - Approvals: list pending approvals for an approver, approve, and reject. - CMDB: search CIs on any class, read a CI with its inbound and outbound relations through the CMDB Instance API, and list cmdb_rel_ci rows. - Knowledge: search and read articles through the Knowledge Management API. - Directory: find a user by email or user name and list group members, which is what fills assigned_to and assignment_group. Reference fields are the usual source of confusion, so every semantic read defaults to sysparm_display_value=all — a reference comes back as both its sys_id and its label — and every semantic write exposes sysparm_input_display_value so a display name can be written instead of a sys_id. Coded state values are exposed as labelled dropdowns built from one constants module rather than raw integers. The shared instance-URL, Basic Auth, sysparm, envelope, and error handling now live in tools/servicenow/utils.ts, and the existing eight generic tools were moved onto it rather than keeping their own copies. * fix(servicenow): stop per-operation subblock defaults colliding on a shared id Subblock initial values are seeded into block state keyed by subblock id, so two subblocks sharing an id leave one stored value and the last definition wins. Three ids were duplicated with differing defaults: - `displayValue` was defined twice, unset for the generic Table API tools and `all` for the semantic ones. The semantic definition won, so a new block set to Read Records or Aggregate Records sent `sysparm_display_value=all` — a wire change to two already-shipped tools. - `state` was defined four times. The Approval State definition won, so every new block carried `state=requested`, which Create Incident wrote to the incident and Move Change State used instead of its own `-5` default. Give the colliding controls their own ids and map them back to the tool params per operation, so the generic tools keep their original request shape and each semantic operation keeps its own default. Also correct descriptions that overstated what the API does: the LIKE operator is not documented as case-sensitive, List Requested Items has no requester filter, and the Change Management API task shape differs from the Table API. Adds tool tests covering the refactor invariants for the eight pre-existing Table API tools and the display-value separation. * feat(servicenow): read a change request's real next states from the instance The change tools describe state transitions using the base-system codes, which only hold on an instance that has not customized its change model. ServiceNow publishes an endpoint that answers the question directly for the record in hand, so use it rather than keep assuming. GET /api/sn_chg_rest/change/{sys_id}/nextstates returns the states reachable from the change request, the instance's own state-value-to-label map, and, for model-driven changes, each transition with the conditions it has and has not met. The tool flattens the per-target-state grouping ServiceNow returns (each transition already carries from_state and to_state, so nothing is lost) and derives the states whose conditions currently pass. Also record the sourcing for the coded values in constants.ts: the change states and close codes are published as a table, but the incident state codes are not — only 6 (Resolved) appears in the docs — so mark the rest as defaults rather than guarantees. Note that sysparm_input_display_value also reinterprets date and time values in the caller's timezone instead of GMT, which matters for the change start and end dates. * docs(servicenow): stop asserting undocumented coded values in placeholders The additional-fields examples used hold_reason with a coded value of "1". ServiceNow documents the On hold reason choices by label only — Awaiting Caller, Awaiting Change, Awaiting Problem, Awaiting Vendor — and publishes neither the column name nor the codes, so the example was asserting something unsourced. Use a field whose value is caller-supplied instead, and record the On Hold requirement on the incident state control using the labels the docs actually give, including that Awaiting Caller makes Additional Comments mandatory. * fix(servicenow): drop phantom parent fields from the catalog order output order_catalog_item read parent_id and parent_table off the order_now response. Those fields belong to submit_producer, a different Service Catalog endpoint; the documented order_now result is sys_id, number, request_number, request_id, and table. Both outputs were therefore always null. * fix(servicenow): correct what knowledge search returns as an article id Search results carry a table-prefixed identifier — "kb_knowledge:9e528db1..." — not a bare sys_id, while GET /knowledge/articles/{id} accepts only a bare sys_id or a KB number. The output described it as a sys_id and the tool description told callers it was what they needed to fetch the article, so chaining the two tools on that field would fail. Point callers at the KB number instead. Relevancy score is documented as a number, not a string. * docs(servicenow): cite the page that actually documents approval statuses The approval state constants pointed at the classic-approvals landing page, which does not list the statuses. Approval status is documented separately and names four — Requested, Approved, Rejected, and Not Requested. * fix(servicenow): stop constant interpolation leaking into tool descriptions The docs generator and the client-facing integration catalog read tool descriptions from source rather than from the evaluated module, so a template literal like `state ${INCIDENT_STATE.RESOLVED}` shipped to users verbatim: `apps/sim/lib/integrations/integrations.json` and the published ServiceNow integration page both rendered `${INCIDENT_STATE.RESOLVED}` instead of `6`. Inline the base-system coded values in the description text; the constants stay in use everywhere behavior depends on them. Also drops an escaped `\'` in the `inputDisplayValue` description for the same reason, and adds a standing guard test asserting no subBlock id carries two different seeded defaults — the invariant behind the per-operation defaulting bug, now checked structurally rather than only through the four per-operation cases. * refactor(servicenow): type the shared response boundary instead of any `parseServiceNowResponse` returned `any`, so every tool reading `data.result` did unchecked property access — a shape change on the instance side would have produced a wrong-typed output silently rather than a type error. Introduces `ServiceNowEnvelope` (`result?: unknown`) as the parser's return type and narrows the record index signatures from `any` to `unknown`. Adds `toRecordObject`, `readString`, and `readNestedNumber` so the tools that read individual fields narrow deliberately at the point of use. This surfaced five genuinely unchecked reads: Order Catalog Item, Get Knowledge Article, and Search Knowledge were declaring `string | null` / `number | null` outputs while emitting whatever the instance sent, and Get Change Next States assigned an unvalidated object to `Record<string, string>`. Each now coerces or drops a non-matching value rather than passing it through. * fix(servicenow): publish the shared tool params and stop offering inert controls The docs generator reads tool source rather than importing it, so the shared `params.ts` consts the semantic tools spread were dropped from every published Input table — 27 of 35 ServiceNow tools listed no instance URL, username, or password at all. Follow a spread into the module it is imported from so those rows are published; ten other integrations gain the rows they were missing for the same reason. Two controls were dead on arrival: Additional Fields was offered on Move Change State and Add Incident Comment, and neither tool read it. Wire it through the change transition, which needs it, and drop it from the comment tool, whose body is exactly one journal field. Every coded-value control was a select-only dropdown, so a customized instance's state or close code was unreachable — sharpest on Move Change State, whose target state is required and whose real codes come from Get Change Next States. Make them comboboxes. Also correct two doc claims ServiceNow does not publish (the incident state citation pointed at a page that does not exist and compares the legacy incident_state field; closing an incident is not documented as requiring itil_admin), replace Record<string, any> with checked narrowing that surfaced two unsound widenings, and document that List Change Tasks returns a fixed {value, display_value} shape under `tasks` rather than `records`. * fix(servicenow): stop one subblock id from carrying two value spaces Subblock values are stored per block keyed by id, so an id reused across operations keeps its value when the operation changes. Incident and change shared `state`, and `closeCode`, `closeNotes`, `comments`, and the knowledge search phrase were each reused for a different value space — so an incident state could be written onto a change request, an incident close code sent as a change close code, or an encoded query searched as knowledge text. Give each value space its own subblock and republish it to the tool param from the operation that owns it, the way targetState and approvalState already work. The generic Table API ids stay exactly as they are, since renaming one would orphan the stored value of every workflow already using those shipped tools. The previous guard only compared seeded defaults, which is why this class stayed hidden; the new one asserts against the merged params a tool actually receives. * fix(servicenow): point the canvas sentences at the renamed subblocks The split of the colliding subblock ids left the operation sentences anchored on ids that no longer exist, so those clauses would silently drop from the card. * fix(servicenow): validate collection members and split the fields projection toRecordArray cast every member of a successful response, so a null or scalar in a collection was handed to the next block as a record while the tool reported success and its declared output said that could not happen. Members that are not plain objects are now dropped, and knowledge articles and change transitions get the same narrowing. The two response types that described an unverified inner shape now say what is actually checked. The 'fields' subblock also carried two value spaces: a JSON body on Create and Update Record, a comma-separated projection everywhere else. Operations added since read a separate returnFields control, so a body can no longer arrive as a projection or the reverse. The shipped ids are untouched, since renaming one orphans the stored value of every workflow already using those tools.
* fix(forking): hide satisfied dependent configuration * fix(forking): keep dependent chains configurable * fix(forking): invalidate stale dependent selectors
…published OpenAPI specs (#6745) * feat(datadog): add incidents, SLOs, dashboards, synthetics, Cloud SIEM, and APM tools Extends the Datadog block from 12 to 39 operations, all verified against Datadog's published OpenAPI specs: - Incidents (v2, public beta): list, get, create, update, add todo - SLOs (v1): list, get, create, update, delete, history - Dashboards (v1): list, get, create, delete - Synthetics (v1): list tests, get test, latest results, trigger, pause/resume - Cloud SIEM (v2): search signals, get signal, update triage state, assign, list detection rules - APM: search spans (v2), list Service Catalog definitions (v2) Adds tools/datadog/utils.ts so every tool builds its URL from the configured site/region and shares the JSON:API-aware error extraction, and handles the v1 flat vs v2 envelope shapes and cursor pagination per endpoint. * fix(datadog): align every operation with the published OpenAPI specs Validated all 39 shipped operations (plus the 12 pre-existing ones that had never been spec-checked) against the DataDog v1 and v2 OpenAPI schemas. - `POST /api/v2/downtime` requires `monitor_identifier`, so a downtime created without a monitor id was rejected. Default to the `*` monitor tag. - A one-time downtime schedule declares `additionalProperties: false` and accepts only `start`/`end`; the timezone moves to `display_timezone`. - `GET /api/v2/downtime` has no `monitor_id` filter, and the response carries no `disabled` attribute. Downtime ids are UUID strings, not numbers. - Drop scaffold types for operations that do not exist (metric metadata, event query, monitor update/delete/unmute, host listing) along with their fields. - Note that monitor mute is no longer published in the v1 specification. - Add browser Synthetic test results, which the browser-specific endpoint returns with its own camelCase step-count shape. - Replace every `any` with a spec-derived interface, keeping the polymorphic service-definition schema opaque. * fix(datadog): remove remaining any types and declare every returned output field Replace the six surviving `Record<string, any>` request-body and response-cast sites with concrete spec-derived shapes, and declare the output fields that transformResponse already returned but outputs omitted: - create_downtime / list_downtimes: timezone, created, modified - create_monitor / get_monitor: options, creator - list_monitors: message, priority, options, created, modified, creator - query_logs: content.attributes, content.tags - update_security_signal_state / _assignee: type; assignee also gained the archiveReason/archiveComment pair its sibling already declared - query_timeseries: series gained the items shape it never described * fix(datadog): stop dropping downtime targeting inputs in the block mapping create_downtime accepts monitorTags, timezone and muteFirstRecoveryNotification, but the block exposed no inputs for them and never forwarded them. Monitor-tag targeting silently fell back to the `*` tag, so a downtime meant for one team's monitors muted every monitor in scope. Adds the three advanced sub-blocks and wires them through. Also routes list_downtimes' currentOnly through toSwitchBoolean. A switch yields the strings 'true'/'false', and 'false' is truthy, so turning the toggle off still sent current_only=true. Every other switch in the block already used the helper; this was the last raw one. * fix(datadog): correct metric type codes, stop SLO update data loss, drop unpublished mute Independent re-validation of all 39 operations against the DataDog/datadog-api-client-go generator specs (v1 and v2 openapi.yaml) rather than the client-rendered docs site. Correctness: - submit_metrics sent inverted MetricIntakeType codes (gauge as 0/unspecified, rate as 1/count, count as 2/rate), silently changing how Datadog aggregated every submitted series. The spec enum is 0 unspecified, 1 count, 2 rate, 3 gauge; an unrecognized type is now omitted so Datadog infers it. Also stops stamping an invented `resources: [{name:'host'}]` default and now forwards `interval`, which Datadog requires for count and rate metrics. - update_slo replaced the whole SLO with only the fields the caller filled in, so editing one field erased description, tags, query, monitor_ids, groups, thresholds, and timeframe. PUT /api/v1/slo/{slo_id} is a full replacement, so the stored SLO is now read first and the supplied edits are overlaid onto it, with the read-only fields stripped. - update_incident admitted empty strings, so a blank input could blank a stored incident title or fail as an invalid date-time. - query_timeseries reported a failed query as success: Datadog returns 200 with a non-ok `status` and the reason in `error`. - create_monitor swallowed malformed options JSON and created a monitor with no thresholds. - send_logs rebuilt each entry from a fixed field list, discarding the custom attributes Datadog accepts as additionalProperties, and padded absent optional fields with empty strings. Removed: - mute_monitor. /api/v1/monitor/{monitor_id}/mute is absent from the v1 spec entirely, there is no unmute counterpart to reverse it, and downtimes are the supported mechanism. Contract accuracy: - Security signal search advertised relative times ("now-1h"); the spec types filter.from/to as format: date-time. Descriptions, placeholders, and wand prompts now produce ISO-8601. - list_incidents advertised an `include` value ("integrations") that is not in the spec enum, and neither incident tool trimmed the comma-separated list, so "users, attachments" 400d. - Invalid "ok" group state dropped from both monitor descriptions. - time_slice removed from SLO create input, which cannot build one without an SLI specification. - DatadogSite gains ap2, uk1, and us2.ddog-gov.com. Pagination and errors: - list_downtimes silently truncated at Datadog's default 30 with no way to page; adds page[limit]/page[offset] and surfaces totalCount. - query_logs returned a cursor it had no way to accept back. - Error extraction consolidated onto datadogErrorMessage, which now also reads the dictionary-shaped errors of the SLO delete conflict. Ten tools were reading `.detail` off plain strings or the raw entry off objects, degrading every failure to a bare status line. - Debug logging removed from list_monitors. Adds 29 regression tests, each verified to fail when its fix is reverted. * fix(datadog): add SEV-0, document page-size caps, drop unsourced output defaults - The severity dropdown omitted SEV-0, which IncidentSeverity allows and both incident tool descriptions already advertised. - Page-size descriptions now state Datadog's documented default of 10 and cap of 100 instead of an arbitrary example, so an agent does not request an out-of-range page. - trigger_synthetics_tests emitted an explicit null for a string-typed optional output, and update_synthetics_status reported 'live' on the error path regardless of what the caller actually requested. * fix(datadog): keep mute_monitor and add the missing unmute counterpart Reverses the removal in the previous commit. Absence from the datadog-api-client-go generator spec showed the endpoint is unpublished there, not that it is retired: Datadog's official Python client still implements it on master as `Monitor.mute(id, scope=, end=)` and `Monitor.unmute(id, scope=, all_scopes=)` (datadogpy datadog/api/monitors.py), which `_trigger_class_action` resolves to `POST /api/v1/monitor/{id}/mute` and `/unmute` with exactly those body fields. mute_monitor has also been in the block since #2175 in December, so dropping it would have broken existing workflows for an endpoint that two independent sources agree is live. The genuine defect was that muting was a one-way trapdoor: Sim could mute a monitor but had no way to reverse it. Adds datadog_unmute_monitor, sharing the monitor ID and scope inputs with mute, so the operation is recoverable from the same block. Also: mute no longer discards the response body (it now reports the monitor id, name, and state), routes errors through datadogErrorMessage, encodes the monitor ID in the path, and stops dropping an explicit `end` of 0. * fix(datadog): make downtime targeting explicit and reach downtime pagination from the block Addresses the review findings on the previous round. - create_downtime accepted both a monitor ID and monitor tags but `monitor_identifier` is a oneOf, so it silently kept the ID and dropped the tags, muting a different set of monitors than the caller asked for. It now rejects the ambiguous combination. - create_downtime ran Number.parseInt on the monitor ID with no validation, so a non-numeric value became NaN and serialized as null inside monitor_identifier. It now uses the same parseMonitorIds guard the SLO path already had, naming the offending value. - list_downtimes gained limit/offset in the tool but the block exposed neither, so no block-driven call could page past Datadog's default. Adds the two sub-blocks and wires them through the params mapper. - The block did not declare the totalCount the tool now returns, so nothing downstream could bind to it. * fix(datadog): tolerate non-string list inputs and keep the shipped mute subblock ids Both defects were introduced by this branch. - splitCommaList called .split on its argument, so routing create_downtime's monitorId through it turned a legitimate numeric input into a TypeError before the request was built. A <Block.output> reference to get_monitor or list_monitors resolves to a number, and an LLM tool call can pass a number or an array, so the helper now normalizes all three shapes. The previous Number.parseInt path had accepted a number by coercion. - Adding the unmute operation renamed the mute subblock ids scope/end to muteScope/muteEnd. Workflow state is persisted by subblock id, so every existing Mute Monitor block would have kept the old keys and silently lost its scope and end time. Restored the shipped ids; both are still unique block-wide and no operation reads another operation's value. * fix(datadog): compare downtime targets after parsing, not before A whitespace-only Monitor ID is truthy as a raw string but parses to no monitor, so the oneOf conflict guard rejected a valid tag-targeted downtime whenever the untouched Monitor ID field carried blank text. Both sides are now compared after parsing.
… R2, Workers, and Tunnels (#6740) * feat(cloudflare): add WAF rulesets, rate limiting, Zero Trust Access, R2, Workers, and Tunnels Extends the Cloudflare integration past DNS/zones/cache with the security and Zero Trust surface: - Rulesets engine (zone-scoped): list rulesets, get a ruleset, read a phase entry point, and create/update/delete rules. WAF managed-rule overrides are surfaced through the http_request_firewall_managed entry point, since Cloudflare has no dedicated overrides endpoint. - Rate limiting (zone-scoped) via the current Rulesets-based http_ratelimit phase, not the deprecated rate_limits endpoint. - Cloudflare Access (account-scoped): applications, application policies, groups, identity providers, and service tokens. - R2 buckets, Workers scripts/routes, and cloudflared Tunnels. Destructive operations (delete application, delete policy, revoke service token, delete rule, delete bucket) spell out their blast radius, and every tool branches on the envelope's success flag rather than the HTTP status. Security events are intentionally omitted: Cloudflare exposes them only through the GraphQL firewallEventsAdaptive dataset, whose field list is not documented outside schema introspection. * fix(cloudflare): correct docs drift and remove any from the tool layer Validation pass over all 47 Cloudflare tools against developers.cloudflare.com. - Two tool descriptions still escaped a quote as \'. That reaches the model verbatim and truncates the generated MDX cell — the get_zone_settings `value` output row was missing from the published docs entirely. Both are now template literals, and the row is back. - list_rulesets ignored pagination. The endpoint pages by cursor via result_info.cursors.after (not page/per_page), so a zone with many rulesets silently truncated with no way to page. Expose per_page + cursor and return the next cursor. - The managed-ruleset override description claimed action and enabled were the overridable properties. They are the ones the Rulesets engine documents at every level, but individual managed rulesets add more: an OWASP Core Ruleset rule override also takes score_threshold. Corrected in both the tool output description and the block's action-parameters wand prompt. (sensitivity_level is a DDoS override, not a WAF one — deliberately absent.) - list_tunnels/get_tunnel dropped the documented `metadata` field. - list_r2_buckets appended order=name whenever any filter was set. `order` only qualifies `direction`, and `name` is its sole documented value. - Path-interpolated IDs are trimmed, so a pasted ID with trailing whitespace no longer 404s. - Replaced every `any` in the integration with checked types: a shared CloudflareEnvelope plus per-resource raw payload interfaces, read through readCloudflareResponse. The mappers in utils.ts were the widest hole — typing them caught four real output-shape mismatches (identity provider read_only, service token enabled, DNS record meta/priority, certificate geo_restrictions) that `any` had been hiding. - BlockMeta only described DNS and zone work. Added templates and skills for the WAF, rate limiting, and Zero Trust Access surfaces the block now has. Confirmed against the docs and left unchanged: rulesets/rate limiting are zone-scoped and Access/R2/Workers scripts/Tunnels are account-scoped while Workers routes are zone-scoped; tunnels live under /accounts/{id}/cfd_tunnel; the ratelimit object is a sibling of action/expression, not nested in action_parameters; every rate limiting period and mitigation_timeout option matches the documented set; R2 delete returns an empty result so echoing the requested bucket name is correct; app-nested Access policy endpoints are current, not deprecated; and every tool fails on a 200 carrying success:false. * fix(cloudflare): stop per-operation subblock defaults colliding on a shared id Subblock initial values are seeded into block state keyed by subblock id — both stores/workflows/utils.ts and lib/workflows/defaults.ts assign `subBlocks[subBlock.id]` in a plain forEach — so two controls sharing an id leave one stored value and the last definition in file order wins. Four ids were duplicated with differing defaults: - `type` was defined four times. The Access "Application Type" control is last, so every new block seeded `type = 'self_hosted'` and the three DNS record controls inherited it — Create DNS Record sent a Zero Trust application type as its record type. The subblock added on this branch broke a default on tools that shipped long before it. - `status` was defined three times. The empty tunnel filter is last, so List Certificates lost its `all` default. - `proxied` was defined three times. An empty filter is last, so Create DNS Record lost its explicit `false`. - `action` was defined twice. The rate limiting dropdown is last, so the ruleset-rule action input was seeded `block`, quietly making "block live traffic" the default for a WAF custom rule the user never configured. Give the colliding controls their own ids and map them back to the tool params per operation, ahead of the coercions that read them, so each operation keeps its own default. The other 17 duplicated ids agree on their value and are left shared. Adds tests covering each separated default plus a sweep asserting no id carries two different seeded values, so a future duplicate goes red. * fix(cloudflare): generate array include rules and allow bootstrapping a phase ruleset The Access policy include wand asked for a JSON object while the tool parses the field with parseJsonArrayParam, so generated rules failed validation. Switch it to json-array, whose prompt reinforcement omits the object braces. Rate limiting and WAF custom rules could only be appended to an existing ruleset, but a zone that has never had a rule in a phase has no entry point ruleset and returns 404, leaving no way to add the first rule. Add cloudflare_create_ruleset for the documented POST /zones/{id}/rulesets bootstrap, seeded with optional initial rules. * fix(cloudflare): correct verified API defects and stop filters leaking into writes Independent re-validation of all 48 tools against developers.cloudflare.com turned up defects that the shipped tools would have hit on their happy path. Delete DNS record reported every success as a failure. That endpoint is the one Cloudflare v4 response with no envelope — its documented body is `{"result":{"id":...}}` with no `success` — so `!data.success` was always true. Branch on an explicit `=== false` instead. The two replace-semantics PATCH endpoints could silently destroy live config. Update rate limit rule defaulted a missing action to `block`, converting an existing `log` or challenge rule into a hard block on real traffic; update ruleset rule left action and expression optional and had no `ratelimit` or `logging` passthrough, so updating a rate limiting rule stopped it rate limiting. Both now require the fields the replacement needs, and the ruleset rule carries the two nested objects through. Access applications were unbuildable for most types: `domain` was required, but it does not exist on the saas, app_launcher, warp, biso, dash_sso, infrastructure, mcp, mcp_portal, or proxy_endpoint request variants. The application type enum was also six values behind. Access group `is_default` is an array of rule objects, not a boolean. Purge cache merged every supplied target into one body, but the purge body is a one-of over the five target kinds; it now names the conflict instead. The remaining fixes are documentation drift: the priority field is MX and URI only (an SRV record carries priority inside its content), the certificate status filter documents only "all", the Worker tag filter takes tag:allowed pairs, and the managed-rule override list conflated the DDoS-only sensitivity_level with the WAF rule-level set. Separately, controls that share a subBlock id share one stored value, and `shouldSerializeSubBlock` short-circuits on `mode: 'advanced'` before it evaluates `condition` — so a hidden list filter was reaching a write. A `list_dns_records` content filter could overwrite a record's content, cache tags could be written onto a DNS record, and the zone status enum could reach the tunnel list, whose enum is disjoint. Filters that differ from the value they collided with now carry their own id, remapped through one table before any coercion. Sharings that mean the same thing everywhere are unchanged. Aliases are cleared by explicit assignment rather than destructuring, because the executor merges the mapper's output over the raw inputs and a merely omitted key survives as its raw subBlock string. The tests assert on that merged result, and three mechanical invariants now go red on a new collision: no id spans a read filter and a written value, no dropdown id carries two option sets, and no hidden advanced control feeds an operation that cannot render it. That last one found the name filter reaching three list operations. * docs(cloudflare): point self_hosted_domains at its replacement Cloudflare deprecated the field in favour of destinations, which the tools already surface. The output stays — Cloudflare still returns it — but the description now says which one to read. * refactor(cloudflare): drop a dead exception from the empty-type guard create_dns_record now takes its record type from the recordType control, whose dropdown has no empty option, so the operation can never reach this guard with an empty type. Clear it unconditionally. * fix(cloudflare): point the canvas sentences at the renamed filter controls The list filters that were split off their write-side twin kept their old ids in canvasPresentation, so seven clauses referenced a control that is no longer visible for that operation — check:canvas-sentences catches exactly this, and a broken clause fails silently on the card rather than throwing. * fix(cloudflare): stop the rate limiting action defaulting on a replacing update Making action required on update_rate_limit_rule was only half the fix: the Action dropdown still seeded block for the update operation too, so an update that edited only the threshold kept sending block and converted a live log or challenge rule into a hard block — exactly the harm the required flag was meant to prevent. The update now has its own control with no seeded value, so the action is something the caller states rather than inherits. The certificate status filter also still offered Active and Pending, which Cloudflare does not document for that endpoint; the only documented value is all, and omitting it returns active packs. * fix(cloudflare): stop the Access replacements seeding a type and a decision Same class as the rate limiting action: both Access updates are full replacements, and the shared controls seeded self_hosted and allow for the update operations too. Editing only a policy's include rules would silently convert a live deny, bypass, or non_identity policy to allow — widening who gets in — and editing an application would rewrite what it IS. Each update now has its own required control with no seeded value, so the type and the decision are stated rather than inherited. Regression tests cover both, and the canvas sentence follows the renamed decision control.
#6761) * fix(sandbox): undefine the raw fetch host bridge before user code runs * test(sandbox): scope the hardening assertions to each execution path * fix(sandbox): preserve the fetch global's property attributes
* feat(docs): fail CI when generated integration docs are stale * fix(docs): don't flag delete-then-recreated trigger pages in check mode * docs(skills): require docs:check in the integration authoring skills * chore(skills): migrate agent commands to native skills * fix(skills): clean orphaned Claude projections
* feat(mssql): add Microsoft SQL Server integration
Add a Microsoft SQL Server block backed by six tools (query, execute,
insert, update, delete, introspect), mirroring the existing PostgreSQL
and MySQL integrations.
Connections go through the `mssql` (Tedious) driver: `connectionTimeout`
is top-level while `encrypt`, `trustServerCertificate`, and
`instanceName` live under `options`, and `port` is omitted when a named
instance is used. Values are bound as `@paramN` via `request.input()`;
no user value is interpolated into SQL. Identifiers are bracket-quoted
after validation and WHERE clauses run through the shared injection
guard.
Introspection reads INFORMATION_SCHEMA plus the `sys.indexes` catalog
views for tables, columns, primary keys, foreign keys, and indexes.
The icon is a placeholder database cylinder drawn with `currentColor`
until the real brand mark lands.
Requires `bun install` for the new `mssql` / `@types/mssql` deps.
* feat(mssql): use the SQL Server brand mark on a white tile
* fix(mssql): pin the validated IP and correct the introspection catalog reads
Tedious exposes `options.connector`, a hook that replaces its own
resolve-and-connect path, so the connection can be pinned to the address
`validateDatabaseHost` already approved instead of re-resolving the
hostname. `server` stays the hostname because tedious derives the TLS
`servername` from it independently of the connector, so SNI and
certificate validation survive the pin. This brings MSSQL in line with
the PostgreSQL and MySQL tools.
Named instances are dropped: tedious resolves them with a UDP SQL Server
Browser lookup issued outside the connector, and node-mssql deletes
`port` whenever `instanceName` is set, so no configuration leaves a
named instance pinned. A named instance is reachable through its static
TCP port.
Introspection fixes:
- index key columns now filter on `key_ordinal > 0`; INCLUDEd columns
and partitioning columns both report `0` and were being returned as
key columns, ordered ahead of the real ones
- foreign keys resolve through `sys.foreign_keys` /
`sys.foreign_key_columns` rather than
`INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS`, whose join to
`TABLE_CONSTRAINTS` has no row when a key references a unique index
and so dropped the key entirely
- `is_unique` is a `bit`, which tedious maps to a boolean, so it is
coerced rather than compared
- schemas come from `sys.schemas`, which needs only `public` and carries
no metadata-visibility caveat
The WHERE-clause guard also covers `WAITFOR TIME`, `OPENQUERY`,
`OPENXML`, the legacy `master..sys*` compatibility views, and extended
and OLE-automation procedures beyond `xp_cmdshell`.
Regenerates the docs and catalog artifacts the icon change left stale.
* chore(mssql): commit the lockfile entries for mssql and its tedious dependency tree
* fix(mssql): make the Query operation genuinely read-only
The block label, tool description, and docs all present Query as SELECT-only
while the route ran whatever T-SQL it was given, so an agent picking
mssql_query because "it is only a SELECT" could delete rows. Screen the
statement for mutating keywords with string literals stripped, which also
catches the WITH ... DELETE form that a leading-token check would miss.
Also switch the tool barrel to absolute imports per the repo convention.
* fix(mssql): compose the shared WHERE guard and close the semicolon-less batch gap
The local validateWhereClause re-derived an older copy of the shared patterns
and scanned raw text, so it missed a bare 1=1 and false-positived on prose in a
quoted value. Delegate to validateSqlWhereClause, which masks string literals
first, and keep only the SQL Server surfaces it has no reason to know about.
T-SQL needs no statement terminator, so every semicolon-anchored stacked-query
check reads straight past `id = 1 DROP TABLE dbo.users`. Screen for a bare
statement-introducing keyword to close that; word boundaries leave ordinary
column names like updated_at and deleted_at untouched.
Export maskSqlStringLiterals so the dialect layer masks the same way the shared
guard does rather than carrying a weaker single-quote-only copy.
* fix(mssql): screen administrative T-SQL and reject batches in the read-only path
The previous round left two keyword lists maintained separately, and both were
short: DBCC, KILL, CHECKPOINT, USE, and DENY were in neither, so
`SELECT 1; DBCC SHRINKDATABASE(...)` and `id = 1 DBCC SHRINKDATABASE(...)`
both got through. Collapse them into one MSSQL_STATEMENT_KEYWORDS shared by the
query and WHERE screens so a keyword cannot be covered in one place and missed
in the other, and add the administrative commands.
Also reject any second statement after a semicolon in the Query path outright.
That closes SELECT 1; <anything> structurally instead of by naming the anything,
so the blacklist no longer has to be exhaustive to hold.
* fix(mssql): reject SQL comments in the read-only query path
A block comment placed inside a keyword splits it as far as a lexical scan is
concerned, so keyword coverage cannot settle whether the server rejoins the
halves. Refuse comments in the Query path instead of modelling the tokenizer.
A SELECT sent through this operation has no need for one, and Execute Raw SQL
still accepts them. Masking leaves comment markers intact, so a literal
containing -- still passes.
* fix(mssql): close the masker-desync bypasses and correct the catalog reads
Every T-SQL screen runs over the shared literal masker, which was written for
the MySQL dialect. Three ways to desynchronise it let real SQL hide inside what
the masker believes is a string, two of which survived the existing even-quote
check:
- a backslash before a quote. T-SQL has no backslash escape, so the server
closes the literal where the masker swallowed the quote and runs the rest as
code. `a='x\' DELETE FROM dbo.t WHERE b='y'` holds four quotes and masks the
DELETE out of the keyword screen entirely, so the read-only Query operation
would run it.
- a double quote inside a bracketed identifier, which the bracket rule missed
because it only looked for single quotes.
- any unbalanced double quote or backtick, which the parity check did not cover.
All three now fail closed. Introspection also filters hypothetical and disabled
indexes, which were reported as if they were live, and resolves the referenced
side of a foreign key through sys.schemas so a cross-schema reference is no
longer an ambiguous bare table name. Values bound through request.input are
serialized when they are nested JSON, which the driver otherwise rejects with a
bare "Invalid string.".
* test(mssql): cover the block param merge and the operation-to-tool map
Asserts on the merged `{ ...inputs, ...buildParams(inputs) }` the generic tool
handler forwards rather than the mapper's return, since a key the mapper omits
keeps its raw subBlock value through that merge. Pins the TLS toggles to their
string form end to end — a switch subBlock would serialize `'false'`, which is
truthy, and the route contract would coerce the user's off into on — and checks
that duplicate subBlock ids agree on their seeded default.
* fix(mssql): release a pool whose connect failed, and allow a keyword with no trailing space
Only a pool handed back to the route reaches its `finally`, so a pool whose
connect rejected leaked its tarn resources — one per attempt when a bad
credential is retried. It now closes itself, and a failure to close cannot mask
the connect error the caller needs.
The read-only screen also anchored on `\s` after the opening keyword, which
refused valid reads like `SELECT*FROM dbo.users` and `SELECT(1)`. A word
boundary accepts those while still refusing `SELECTX`, and cannot loosen the
screen — the keyword and batch checks run over the whole statement regardless.
* chore(mssql): regenerate tool metadata and the integration catalog after rebase
Artifacts rebuilt with the generators rather than hand-merged, so they carry
both the mssql entries and the tools that landed on staging in parallel.
* fix(mssql): reject a parenthesised or negated constant tautology in a WHERE clause
The shared guard recognises `OR 1` but not `OR (1)`, `OR ((1))`, `OR NOT 0`, or
`OR NOT (FALSE)` — a parenthesis or a NOT between the operator and the constant
hides it. Both patterns require the constant to be the whole parenthesised term,
so a real disjunct such as `OR (1 = priority)` is untouched.
This narrows the gap rather than closing it, and is not meant to close it: an
always-true expression is not lexically decidable in general, which is why the
WHERE screen stays documented as defense-in-depth rather than a boundary.
* chore(mssql): regenerate tool metadata after rebase onto staging
Rebuilt with the generators so the artifacts carry the servicenow and
crowdstrike tools that landed on staging alongside the mssql entries.
* fix(mssql): screen trigger and state statements, and drop the space anchor on Execute
DISABLE and ENABLE were missing from the shared statement list, so
`SELECT 1 DISABLE TRIGGER dbo.audit ON dbo.users` passed the read-only screen as
a semicolon-less batch and turned auditing off. SET, BEGIN, COMMIT, and ROLLBACK
are added with them, since session and transaction state are reachable the same
way. FETCH is deliberately left out: OFFSET ... FETCH NEXT is the standard paging
clause, and screening it would reject the ordinary paged SELECT.
Execute Raw SQL anchored its allowlist on `\s`, which refused `EXEC(@SQL)` —
the ordinary form of dynamic SQL, on the one operation meant to run it. It now
uses `\b`, matching the read-only screen.
) * docs(cli): add a CLI section, generated from the command tree The `sim` CLI shipped with no coverage in the docs site. Adds a fourth top-level tab for it, and moves Academy last. The command reference is generated. `sim` exposes 147 leaf commands across 33 groups, most of them derived at runtime from the v2 route contracts, so a hand-written reference would be wrong the week after it was written. The generator walks the command tree `buildProgram()` hands to commander — the same tree the terminal parses — rather than re-deriving it from the contract, which would be a second implementation free to describe commands nobody can invoke. `check:cli-docs` is a zero-arg `check:*` script, so the existing audit runner picks it up and stale pages fail CI. Generating against the real tree surfaced a collision it had been hiding: `bulkUpdateKnowledgeDocuments` and `updateKnowledgeDocument` both derived to `sim knowledge documents update`. Commander resolves a duplicate to the first match, so the bulk form shadowed the single-document one and its flags were unreachable while still appearing in `--help`. The bulk form is now `batch-update`, matching how `tables rows batch-delete`/`batch-update` already handle the same REST overload, and the generator fails on any duplicate path so the next one cannot land silently. Five hand-written guides cover install, auth, configuration, output formats, and scripting. Also corrects two commands in the package README that do not exist as documented (`tables columns <tableId>`, and `--sort score:desc`, which is JSON). * docs(cli): document every flag from the contracts, add troubleshooting and a single-page reference The command reference was structurally complete but said almost nothing: 223 of 377 flags rendered as "Set sort by" because the CLI only ever read flag help from its own contract overrides, and fell back to restating the flag name. The prose already existed. The v2 route contracts carry 931 `.describe()` calls and the OpenAPI specs publish all of them — 327 parameters and 282 body properties, 100% coverage — but the generated operation table dropped every one, carrying only a per-operation summary. It now carries the field descriptions, the path-parameter descriptions, and positional help, so `--help` and the docs explain a flag the same way the API reference does. Placeholder descriptions are now zero, and 147/147 commands, 377/377 flags and 130/130 arguments are documented. `check:cli-docs` fails on a request field with no `.describe()` rather than letting it render as documentation that says nothing. Also in this pass: - Commands are root-level sidebar entries under a Commands heading rather than a folder, and headings are the command's description, so the table of contents distinguishes entries at the first word instead of repeating "sim knowledge documents …" fourteen times. A guard fails the build if two descriptions on a page collide, since they would share an anchor. - A single-page `Complete reference` carrying all 147 commands, for in-page search and for agents fetching `/cli/reference.mdx`. It keys on exact command paths because descriptions are only unique within a group. - A troubleshooting page, with every message copied from the source. - Table columns are sized by a local component; the flag column was starved while descriptions kept most of the row empty. - The prerelease install channels are dropped from the docs and the package README, which is what npm renders. * fix(docs): match the CLI tab by path segment, and escape backslashes before pipes `pathname.includes('/cli')` also matches `/integrations/clickup` and `/integrations/clickhouse`, so both existing integration pages lit the CLI tab and unlit Documentation. Matching is now per path segment. Anchoring to the start would not work either — a non-default locale prefixes the path, as in `/ja/cli` — so the segment is matched wherever it sits. Table cells now double a backslash before escaping pipes. A value ending in one turned `a\` + `|` into `a\\|`, which the table parser reads as an escaped backslash followed by an unescaped pipe, splitting the cell early. Nothing in the command surface contains a backslash today, so this was latent rather than visible. The reference page's global options table is two-column and was being wrapped in `CommandTable`, which sizes the second column for the `Required` cell of the three-column tables and crushed the description into 5.5rem. It now matches the overview page, which leaves that table unsized.
…ations (#6742) * feat(microsoft_ad): licensing, security, audit, role, and device operations Deepens the Microsoft Entra ID block from 12 to 36 tools against the Microsoft Graph v1.0 reference: license assignment and tenant SKUs, password set/reset, sign-in session revocation, authentication methods, sign-in and directory audit logs, app role and directory role assignments, service principals, device reads, and conditional access policy reads. Device write (device-update, device-delete) is deliberately excluded. Both document Directory.AccessAsUser.All as their only delegated scope, with the higher-privileged read documented as unavailable, so supporting them would mean requesting tenant-wide act-as-the-user directory access for two operations that additionally require the caller to hold Intune Administrator. Also drops an undocumented ?$select= from create_user that was silently nulling department and accountEnabled in the response. * fix(microsoft_ad): resolve OData filter and search by owning operation The params mapper assigned result.filter from each filter subBlock in turn, so the last non-empty one won regardless of the selected operation. Because a subBlock keeps its value after the operation changes, a filter written for one endpoint was sent to every other collection operation — invalid OData against a different Graph resource, or a silently wrong page. Resolves the filter and search terms from an explicit operation-to-field map instead, so each operation reads only the field it owns. * fix(microsoft_ad): clear non-owning filter and search on the merged inputs The executor merges { ...inputs, ...transformedParams }, so declining to copy a stale filter is not enough — the serialized value survives the merge and still reaches the tool. Advanced-mode subBlocks are serialized on non-emptiness alone and never have their condition evaluated, so the value is present even when the field is hidden. Write filter and search on every operation, as undefined when the operation owns neither, so the merge clears them. * fix(microsoft_ad): clear the MFA flag and let paged user operations continue without a User ID The set_password MFA dropdown only wrote its key when non-empty, so the "No Change" empty string survived `{ ...inputs, ...transformedParams }` and reached Graph in place of a boolean. Assign it explicitly, including as `undefined`, the same way `filter` and `search` are handled. `list_user_app_role_assignments` and `list_user_devices` page by `@odata.nextLink`, and both tools already treat `userId` as optional once a continuation URL is supplied. Drop them from the required set when Next Page is filled in so pagination-only runs pass block validation. Also note on the reset_password output that a generated password reaches workflow outputs, run history, and the model, matching how other tools that return secrets document exposure. * fix(microsoft_ad): require the service principal ID only on the first page Every other single-resource ID field pairs its condition with a matching required rule; servicePrincipalId had none, so a first-page run could pass block validation with an empty ID and fail inside the tool instead. Require it unless a continuation URL is supplied, matching the paged per-user operations. * fix(microsoft_ad): reject a continuation URL from a different collection Every paged operation reads the one shared Next Page field, and a subBlock keeps its value after the operation changes. Paging /users and then switching the block to /devices short-circuited back to the user page, silently returning the previous collection instead of the selected one. Assert the continuation URL's terminal path segment against the collection the tool actually reads, which also rejects a nextLink pasted from an unrelated response.
…n the CLI guides (#6763) * docs(cli): use -g for the install, and cut the prose that was not pulling weight `--global` is valid but `-g` is what every comparable CLI documents, and the long form only came from the package README. Also drops the yarn tab: it read `yarn global add sim`, which works on Yarn 1 only — Yarn 2 removed global installs, so that command fails for anyone on a modern Yarn. Adds `npx sim` for running without installing. The guides had accumulated design rationale that belongs in code comments rather than user docs — why the filter grammar is JSON, why the config section naming is asymmetric, why an unexpected error keeps its stack trace. Surveying how gh, Vercel, Turborepo, Deno, Bun and Supabase write theirs, none carry that kind of justification, and callouts are reserved for content whose absence produces a wrong result rather than for general asides. So: 1016 lines to 763, and 12 callouts to 3. The three that remain are the pairing-code check, that `sim logout` does not revoke the key, and the `--limit 100` default on `batch-delete`/`batch-update`, which silently truncates a larger match. Troubleshooting drops the entries whose error message already contained its own fix and keeps the seven whose cause is not obvious. * fix(docs): render JSON-LD as native script tags so it reaches the HTML All four structured-data blocks — WebSite, TechArticle, BreadcrumbList, SoftwareApplication — were rendered with `next/script`, which never emitted a script tag. Measured on a production build, `/api-reference/getting-started` contained zero `<script type="application/ld+json">` elements; the payload existed only in the `__next_s` client-injection queue and the RSC flight data, so anything reading the served HTML saw no structured data at all. React was also logging "Encountered a script tag while rendering React component" on every page. `next/script` is for loading and executing JavaScript. JSON-LD is data, and Next's own guidance is a native `<script>` in the component. `serializeJsonLd` already escapes the `<` character to its unicode form, which is the sanitization that guidance calls for, so only the element changes. Same build, after: three valid tags per page with `WebSite` in `<head>`, and the injection queue gone entirely. * fix(docs): scope the flush-separator rule to a container's first separator `[data-separator]:not([data-separator] ~ [data-separator])` was meant to keep the first sidebar group flush against the top padding, but `~` only reaches siblings, so it also matched the first separator inside every expanded folder. Under Self-Hosting, "Install" lost its top margin and crowded the "Architecture" link above it — 25px of gap where "Configure" and "Operate" below it had 40px. `:first-child` expresses the intent directly. Only the four sidebar roots open with a separator; every nested folder starts with a page, so the intended case still goes flush and nothing else changes. * fix(docs): move the flush-separator rule onto the separator component Keeps the styling with the component that owns it, per the repo standard, and lets the global rule be deleted outright rather than corrected — `global.css` now only loses a rule in this PR. Tailwind's `first:` variant compiles to the same `:first-child` selector, so behavior is unchanged: the build emits `.first\:mt-0:first-child{margin-top:0}` and the prerendered HTML carries the class on the separator.
…eep (#6764) * fix(integrations): close regressions found in the final validation sweep An independent read-only audit of the eight integrations merged to staging today found defects in every one, most of them side effects of the surgery those PRs performed on already-shipped code. Data loss and destructive paths: - cloudflare: restore the shipped subBlock ids on read filters so existing workflows keep their DNS/zone/purge filters. Losing them made list_dns_records return the entire zone with success: true, which a downstream delete fan-out would then target. The colliding write controls are renamed instead, chosen by blast radius. - cloudflare: refuse an update_ruleset_rule that would tear down the rule it edits. PATCH is a replace, so an omitted action_parameters unbound the WAF managed ruleset and every override under it. - cloudflare: split the hidden `enabled` control so a value set while drafting can no longer disable a live WAF or rate-limiting rule. - cloudflare: stop `name` leaking into update_dns_record and renaming a live record. - okta: stop a blank name overwriting a stored group name via the LLM path. The block guard covered only the UI. Broken on the default path: - microsoft_ad: update_user sent accountEnabled: "" on its own default, so every call left at "No Change" failed. Same tri-state defect already fixed for forceChangePasswordNextSignInWithMfa; `visibility` fixed alongside it. - cloudflare: `domain` is required for self_hosted (the default app type), ssh, vnc and rdp; add saas_app/target_criteria and drop dash_sso, which has no request variant. Silent wrong results: - datadog: list_monitors inherited Create Monitor's tag filter and returned a filtered list as if complete. - servicenow: `fields` carried both a JSON body and a projection on the three legacy generic operations. The regression test for this fed already-JSON and could not fail; it now feeds a real projection. - splunk: cancel_search_job reported failure on success by parsing an XML body as JSON; readSplunkJson now tolerates it. - okta: sendEmail === true dropped a string 'true', silently skipping the deactivation email. Security: - mssql: add writetext/updatetext/readtext to the statement screen. \bupdate\b cannot match UPDATETEXT, so both were reachable through the read-only path. - crowdstrike: chunk repeated-query ids. At the published caps a single request built a ~68 KB query string, past typical proxy limits. Also: splunk count=0 unbounded read, splunk pagination totals, the `nobody` placeholder that reintroduced the namespace bug by copy-paste, okta cursor and activate controls split per operation, servicenow sysparm_having syntax and two required controls no longer pre-seeded with consequential values, datadog block outputs reconciled with tool outputs, and 16 escaped apostrophes that corrupted the published Entra docs. One scope removed from microsoft_ad (User.Read.All). Directory.Read.All and GroupMember.ReadWrite.All were proposed for removal and verified still required; a test now asserts they stay. * fix(integrations): surface corrupt Splunk bodies and partial CrowdStrike deletes Narrows readSplunkJson's non-JSON tolerance to XML. The dispatching and job-control endpoints answer in XML, but a body that is neither empty nor XML was meant to be JSON, so swallowing its parse failure handed get_search_results an empty envelope and reported a lost result set as a search with zero events. Annotates a batched CrowdStrike delete that fails partway with the IDs its earlier batches already removed. Falcon cannot roll those back, so a bare failure left the caller unable to tell what was gone and a blind retry re-targeted IDs that no longer existed. Registers the Cloudflare subblock-ID migration the registry-stability check requires. The suffixed read-filter IDs never shipped in a release and every block already materializes the restored IDs, so they are dropped rather than renamed onto values the collision guard would discard anyway. * fix(integrations): close the three defects Bugbot found in the sweep An execute rule sent an explicit empty action_parameters object past the new guard, because presence was checked rather than emptiness. `{}` is the same payload Cloudflare's schema default produces, so it unbound the managed ruleset the guard exists to protect. Datadog's new List Monitors pagination used a bare `Number()`, so a typo or an unresolved reference in either advanced field reached Datadog as a literal NaN — the pattern this same sweep fixed for Entra `top` and the Splunk numerics. Okta's block still marked the group name required on update, blocking a description-only update that the tool, its merge helper, and the API all accept. * fix(integrations): confine the Splunk XML tolerance and the CrowdStrike commit list Splitting the XML tolerance out of readSplunkJson into readSplunkDispatchJson puts it only on the three dispatching and job-control tools that need it. The results path can no longer read any non-JSON body as an empty envelope, so a 2xx HTML interstitial surfaces instead of reporting a search that matched nothing. The dispatch reader anchors on the one documented `<response>` root, so an interstitial fails there too. A batched delete now records the IDs Falcon echoed in `resources` rather than the IDs that were requested. A batch can answer 200 while reporting per-ID failures, and naming those as deleted told the caller to drop still-live indicators from the retry. * test(crowdstrike): pin batched partial-delete parity with an unbatched request A 2xx envelope carrying per-ID errors is a partial success, not a failure — failedWithoutResources fails the operation only when nothing came back at all. The batched path already reports it exactly as a single request does, with deletedIds naming what Falcon confirmed and errors naming what it refused. Pin that so the contract is not mistaken for a swallowed failure. * fix(splunk): read the dispatch XML envelope instead of discarding it A dispatch answering in the documented XML form was replaced with an empty object, so create_search_job and dispatch_saved_search threw a missing-sid error after the remote job had already been created — stranding a job the caller could no longer poll or cancel. The envelope is now projected onto the same `{ sid }` shape output_mode=json produces, so the search ID survives. Matching only the opening tag also accepted a body cut off mid-transfer, which on a cancellation reported a truncated response as a successful cancel. The pattern now spans the closing tag, so a truncated envelope falls through to JSON.parse and throws.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Too many files changed for review (879 files, 100 file limit). |
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 36185505 | Triggered | Generic Password | 257029a | apps/sim/tools/microsoft_ad/reset_password.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
PR SummaryLow Risk Overview Docs CI gate: Several skills ( Migration skill hardening: Command cleanup: Large slash-command files ( Reviewed by Cursor Bugbot for commit aeb5624. Configure here. |