mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
9e67655b23942e16f0a0ec44a937bf6ba987aab1
6234
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9e67655b23 |
feat(servicenow): semantic incident, change, catalog, approval, CMDB, 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.
|
||
|
|
57611bda18 |
fix(workflow): derive the webhook URL only where a sub-block shows one (#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. |
||
|
|
4bc89c9256 |
feat(crowdstrike): add alerts, host response, IOC, Spotlight, RTR, and 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. |
||
|
|
76318e42df |
fix(fork): preserve folder structure across a fork edge for files, tables, 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. |
||
|
|
0077f6fae1 |
fix(tables): re-check the find match at the reveal, not just before paging (#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 |
||
|
|
4f722c6439 |
fix(deploy): resolve the error-output flag from edges on both sides of 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 |
||
|
|
623c30f05c |
fix(credentials): restore reliable OAuth connections (#6753)
* fix(credentials): restore reliable OAuth connections * fix(credentials): cover OAuth handoff edge cases * fix(credentials): preserve reauthorization outcomes |
||
|
|
cbbcca970c |
fix(okta): stop partial updates erasing stored profile data (#6751)
* 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 |
||
|
|
852906ec91 |
feat(splunk): add Splunk Enterprise and Cloud integration (#6743)
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.
|
||
|
|
611df8b8a1 |
improvement(tables): make Cmd+F search as you type and clear on close (#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 |
||
|
|
d45dad7e8b |
feat(okta): add System Log, MFA, sessions, apps, roles, and group rules (#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. |
||
|
|
4fc0fb4bad |
refactor(resources): converge Files onto the shared drag hook and batch 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. |
||
|
|
77f520ce0a |
fix(auth): scope SSO account linking to the verified domain and fence 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. |
||
|
|
b96c053d15 |
fix(media): prevent drawtext filtergraph injection in add_text (#6734)
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. |
||
|
|
03ba95ad5d |
fix(credentials): let workspace admins disconnect a teammate's OAuth 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.
|
||
|
|
e3b428e502 | fix(docker): prune the app package by manifest name (#6736) | ||
|
|
f0fd48c2a4 |
fix(knowledge): bound chunking separators so one config can't stall processing (#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. |
||
|
|
0cd87bad2f |
feat(resources): multiselect on tables and knowledge, spring-loaded folders (#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 |
||
|
|
c269e883bf | fix(credentials): conceal inaccessible credential reads (#6730) | ||
|
|
337a53f12c |
feat(cli): Sim CLI with AWS-style profiles and a platform key exchange (#6147)
* improvement(api): pull in the v2 external endpoint surface Cherry-picks improvement/v2-endpoints ( |
||
|
|
cfb99e19a2 |
fix(tables): auto-scroll during column drag (#6722)
* 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 |
||
|
|
b198080356 |
feat(credential-groups): complete managed account enrollment (#6729)
* 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 |
||
|
|
6006870f02 |
feat(credentials): add v2 credential lifecycle APIs (#6664)
* 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 |
||
|
|
0ce8ded1e7 |
improvement(credential-groups): align settings surface with the shared 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. |
||
|
|
a9688d01b9 |
feat(library): Automation Anywhere Alternative: AI Agents vs. RPA for Real Reasoning (#6728)
Co-authored-by: Sim Pi Agent <pi@sim.ai> |
||
|
|
96f7b2395a |
fix(access-control): default every operation and model picker to one the permission group allows (#6720)
* fix(access-control): default every operation picker to one the group allows
The block editor's operation dropdown already hid operations whose tool the
caller's permission group denies, but it seeded its default without waiting for
that config. `usePermissionConfig` resolves as "nothing denied" while its query
is in flight, so a freshly dropped block persisted the static first operation —
and nothing revisits a field that already holds a value, so the correction that
arrived with the config never applied. A user whose group denies `slack_message`
still got a Slack block sitting on Send Message. The model combobox already had
this guard; the dropdown did not.
Consolidates the rule behind `lib/permission-groups/operation-access` and
`useOperationAccess`, which resolves an operation to its tool without guessing
(an unresolvable one stays visible; the server gate stays authoritative) and
withholds a default until the config has loaded, so seeding on a defined value
is the whole guard.
Applies it to every surface that offers or seeds an operation:
- block editor dropdown — default now waits for the config
- agent block tool list — operations were not gated at all; the picker now hides
denied ones, drops blocks whose every operation is denied, and defaults to the
first allowed
- canvas search / connection picker — the tool-operation index was filtered only
by the block allowlist, so denied operations were still offered as one-click
block drops
- block creation — a declared default operation the group denies is replaced
with the first allowed one, and a denied preset operation is discarded
* fix(access-control): gate the seeded model too, and collapse the seed rule
Block creation seeds `model` the same way it seeds `operation`: `agent`,
`router` and `evaluator` all declare `defaultValue: 'claude-sonnet-5'`, and
`prepareBlockState` wrote it unconditionally. The model combobox only fills a
field that is empty, so a group denying that model (or the Anthropic provider)
got an Agent block pre-filled with a model it cannot run — the same bug as the
operation one, on the other axis the permission group governs.
Rather than a second bespoke gate, `prepareBlockState` now takes one veto,
`isSeededValueAllowed(subBlockId, value)`, and seeds nothing when a declared
default is denied. Nothing substitutes a replacement there any more: the
editor's own permission-aware pickers already resolve the right one and only
fill an empty field, and substituting in the store would drift from
`getDefaultBlockName`, which names a block after its *declared* default. That
also deletes `firstAllowedOperation` and its copy of subblock-option
enumeration.
Review follow-ups:
- `usePermissionConfig` gains `isModelUsable` (denylist AND provider allowlist);
the combobox's two hand-rolled copies of that pair now call it
- `isToolAllowed`/`isModelAllowed` index their denylists — the gate calls them
once per option of every block offered, so a linear scan made a check's cost
scale with denylist length (measured 3.2ms -> 0.30ms per search-index build
at 500 denied tools)
- `useOperationAccess` had three members with three different loading
semantics, one documented as unsafe alone; it now exposes one withholding
`resolveOperationGate`
- the agent tool picker derived its option list twice with the empty-id filter
on only one path; both callers now share one `{ options, denied }` result
- `OPERATION_SUBBLOCK_ID` was a verbatim copy of the private constant in
`canvas-sentence.ts`, doc comment included; that file now imports it
* chore: trim restating comments and a dead coalesce
Cleanup-pass findings on this branch's own lines: comments that restated the
code they sat on, a four-line note whose sibling said the same in one, and a
`?? undefined` on a non-nullable value. The comment on the tool-picker filter
now explains the clause that actually needed it (an empty option list is not a
denied one) instead of narrating the filter.
Left alone as pre-existing and out of scope: the inline `staleTime` literal in
`useAllowedIntegrationsFromEnv`, and the Operation selector's raw label /
plain `Combobox` — both byte-identical to staging and matching the convention
of every sibling field in that panel.
* fix(access-control): seed nothing restricted when the config is unknown
Block creation is one-shot, so the withholding pattern the editor's pickers use
does not transfer: withholding the predicate there meant `prepareBlockState`
seeded the declared `operation`/`model` defaults unchecked, and nothing
revisits a field that already holds a value — so a block added before the
permission config resolved kept a model the group may deny.
Both restricted fields now seed empty until the config is known; the pickers
fill them the moment it resolves. A preset operation is still honoured in that
window: unlike a declared default it is the user's explicit pick, and the
server gates the run.
* fix(access-control): make the loading rule structural, not a convention
Two review bots found the same class of bug in two more places, which is the
real finding: "never persist from a predicate that reads as unrestricted while
the config loads" was a rule each callsite re-implemented, and the rule had
already been forgotten twice.
Closes both reported instances and moves the rule somewhere it cannot be
forgotten again:
- `useOperationAccess.resolveSeedGate` now owns the creation-time veto for both
restricted fields, so `workflow.tsx` states no policy of its own — it asks for
a gate and passes it on. Previously the model half of the invariant was
carried by an operation-shaped object that merely happened to be absent
during the same window.
- The agent tool picker and both operation selectors close while the config is
unknown. Every list they offer — blocks, operations, MCP and custom tools —
reads as unrestricted for that beat, and each pick is a one-shot write.
- A preset operation goes through the same gate as a declared default. It comes
from the search index, which is itself unfiltered while loading, so it is not
the informed pick it looks like.
- `isPermissionLoading` is exposed from one hook, so all four surfaces read the
same symbol instead of four spellings of the same condition.
Also from the review passes: dropped `isModelAllowed`/`isProviderAllowed` from
the public interface (consolidating onto `isModelUsable` left them with no
external consumer), un-exported `resolveOperationToolId` (no non-test caller),
and corrected the `isSeededValueAllowed` TSDoc, which still described the
contract the previous commit replaced.
Tests: replaced a case that asserted its own fixture rather than the code with
coverage of the two guard branches that were genuinely untested — an empty-string
and a non-string declared default must bypass the gate, since both mean "nothing
was declared" rather than a value to authorize.
* fix(access-control): only gate a model field the provider allowlist is about
The seed gate ran `isModelUsable` on every subblock named `model`, but
`getProviderFromModel` resolves chat models and falls back to `ollama` for
everything else. 28 of the 44 seeded model defaults in the registry are
embedding, speech, image, video or search ids — so for any group with a
provider allowlist that omits Ollama, those blocks were created with an empty
model.
Adds `findProviderFromModel`, the non-guessing half of `getProviderFromModel`,
which returns `null` where the registry declares nothing. `isModelUsable` now
treats an unresolved id as not-a-provider-choice and leaves it alone, matching
the rule the operation gate already follows: never guess, and let the server
stay authoritative. `getProviderFromModel` delegates to it, so there is one
resolution path and its ollama fallback is unchanged.
This also repairs the same misjudgement where it predates the branch: the model
combobox filtered its options through the identical provider check, so those 28
defaults were already being hidden from their own pickers for allowlisted
groups.
The dead `try/catch` around the old call went with it — `getProviderFromModel`
returns a fallback rather than throwing for an unknown id.
|
||
|
|
8fb571ac54 |
feat(credentials): add managed credential groups (#6697)
* feat(credentials): add managed credential groups * fix(audit): sync credential group mock * fix(credentials): serialize enrollment revocation * fix(credentials): isolate managed delegation * fix(credentials): serialize invitation lifecycle * fix(credentials): preserve enrollment lifecycle * refactor(credentials): migrate groups to application boundary * fix(credentials): serialize enrollment readiness * fix(credentials): preserve completed reconnect state * fix(credentials): revalidate policy before grant persistence * fix(credentials): prioritize expired invitations * fix(credentials): redirect unavailable oauth starts * fix(credentials): clarify managed oauth boundaries * fix(settings): complete feature flag test mocks * fix(credentials): clarify enrollment actions and entitlement errors * fix(credentials): preserve entitlement failure reasons * fix(credentials): refine managed oauth flow * fix(lint): use optional chain for pagination |
||
|
|
2a7abfdd17 |
feat(tables): filter by cell value from the cell menu, sort from the column menu (#6719)
* feat(tables): filter by cell value from the cell menu, sort from the column menu * fix(tables): drop nested same-column conditions when filtering by cell value * fix(tables): refuse cell-value filters on json columns before the array branch |
||
|
|
ee1fc379a4 |
fix(tables): allow unbounded v1 row queries (#6713)
* fix(tables): allow unbounded v1 row queries * fix(tables): drain under-budget queries fully * fix(tables): bound expanded query metadata * fix(tables): always return query totals |
||
|
|
daff02249d |
fix(webhooks): read every Ashby error shape when webhook registration fails (#6711)
* fix(webhooks): read every Ashby error shape when webhook registration fails
The provider read errorInfo.message and a top-level message, but not the
`errors` array. Ashby uses three shapes in practice, confirmed live: objects
`[{ message, parameter }]`, plain strings `['webhook_not_found']`, and
`errorInfo`. A missing apiKeysWrite permission arrives in the array form, so
the user saw 'Unknown Ashby API error' instead of the cause.
The duplicate-webhook branch made it worse: it only fires when the message was
extracted, so an unparsed error also cost the user the one actionable
instruction for fixing it - delete the duplicate under Settings > API/Webhooks.
Uses the shared ashbyErrorMessage extractor rather than a second partial copy,
matching how other providers already import from @/tools. The delete path now
reports why it failed instead of only the HTTP status.
* style(webhooks): format the Ashby provider test to biome's apps/sim config
CI runs `biome check .` from apps/sim; I had run biome ad hoc from the repo
root, which resolves a different config and left this hunk unformatted.
* fix(webhooks): keep the Ashby error extractor local to the provider
Importing the shared extractor from @/tools/ashby/utils failed
check:tool-registry-boundary. Two separate reasons, both real:
An import edge from lib/webhooks/providers into @/tools/** grows the workspace
page graphs that reach the providers, because @/tools/types statically reaches
@/lib/oauth, the rate limiter and the executor.
And carving the helper into its own file did not help either: the knowledge page
graph already sits exactly at the +42 ceiling the audit allows, so one more
module anywhere it can reach is one too many.
So the logic is duplicated across the subsystem boundary rather than shared
across it, with a comment on both sides saying why. Both copies derive from the
same three documented Ashby error shapes and are covered independently.
* fix(webhooks): fail an Ashby webhook delete that returns success:false
Ashby returns what would be a 4XX elsewhere as HTTP 200 with
`success: false` — its own docs state this explicitly. `deleteSubscription`
branched on `ashbyResponse.ok`, so every rejected delete logged
"Successfully deleted Ashby webhook subscription <id>" and never threw in
strict mode. Sim then dropped its own row while the subscription stayed
live in Ashby, and since there is no `webhook.list` endpoint the orphan
cannot be enumerated afterwards.
Check `success` the way `createSubscription` already does, and treat
`webhook_not_found` as already-removed rather than an error — that is the
shape an unknown id comes back in, not a 404.
An absent `success` field stays a success here, unlike on create: teardown
runs on the undeploy path, and failing closed on an undocumented response
shape would wedge cleanup.
Also corrects two trigger-surface details against the API reference: the
setup text said the webhook is created when you save the trigger (it is
created on deploy), and the jobCreate `employmentType` description omitted
the documented `Temporary` value.
* fix(webhooks): match Ashby's real not-found envelope on repeat delete
The already-removed branch tested `/webhook_not_found/` against the
extracted message, but `ashbyErrorMessage` returns `errorInfo.message`
first and that reads "Webhook not found" — Ashby carries the machine code
on `errorInfo.code` and in the deprecated `errors` array, both of which
lose to the message. So the one envelope this branch exists for, a repeat
delete of an id Ashby has already dropped, fell through to the failure
path: a spurious warn today and a strict-mode throw on the undeploy
cleanup path.
Read the codes directly and keep a prose fallback for the message-only
form. Caught by Cursor Bugbot.
---------
Co-authored-by: Waleed Latif <walif6@gmail.com>
|
||
|
|
2223b63851 |
chore(utils): consolidate record guards and remove dead code (#6715)
* refactor: consolidate local isRecord guards onto shared isRecordLike Nineteen files had re-declared a local `isRecord` guard rather than using the shared one from `@sim/utils/object`, drift that reappeared after #5061 first consolidated them. Two more imported the shared guard under an `isRecordLike as isRecord` alias. The copies were not interchangeable. Nine matched `isRecordLike` exactly. The rest omitted the array exclusion (`typeof x === 'object' && x !== null`, or `Boolean(x) && typeof x === 'object'`), so arrays passed the guard. Each of those call sites was reviewed individually: in every case the guard is followed by string/number field checks that an array fails anyway, so the outcome is unchanged. The one exception is `isOptionsTagData`, where `Object.values` on an array of option items really did make an array-form `<options>` tag render. It now accepts arrays explicitly rather than by accident. `executor/handlers/pi/search/extension-source.ts` keeps its own copy: it is source text written into an E2B/Daytona sandbox at runtime and cannot import. * refactor: replace inline record guards with isRecordLike 103 inline `typeof x === 'object' && x !== null && !Array.isArray(x)` guards (and the `x &&` / `Boolean(x)` spellings of the same conjunction) now call the shared guard. Inside a conjunction that already asserts `typeof x === 'object'`, `x &&` and `x !== null` are interchangeable, so all three orderings are the same predicate at runtime. Only exactly-equivalent conjunctions were converted. Matching required the three clauses to be one adjacent conjunction over the same operand, so a nearby but unrelated clause cannot be absorbed — generic-handler.ts, where the array case is handled inside the block rather than excluded by the guard, is correctly left alone. Two sites were reverted after type-check rejected them: instagram/server-utils.ts and workflows/[id]/log/route.ts both cast straight to a specific interface, which is legal from `object` but not from `Record<string, unknown>`. Their narrowing is genuinely not identical, so they keep the inline form rather than acquiring a double cast. Left as-is: `packages/ts-sdk` (published with no runtime dependencies) and the two sandbox sources written into E2B/Daytona as text, which cannot import. * refactor: consolidate duplicate record coercion helpers onto @sim/utils A second sweep found 28 more local record helpers hiding under names the previous `isRecord` grep never matched — `asRecord`, `toRecord`, `toRecordOrNull`, `asObject`, `isJsonObject`. rabbitmq defined the same `asRecord` twice within one service; dynatrace had three variants in one file. Fourteen of them were re-deriving the same two shapes, so those shapes now live in `@sim/utils/object` beside the guards they wrap: toRecord(value) // isRecordLike(value) ? value : {} toRecordOrNull(value) // isRecordLike(value) ? value : null Both preserve identity on a hit, so no call site starts copying. Eighteen local definitions are gone. `tools/instantly/utils.ts` keeps its exported `asRecord` because its return type is the local `JsonRecord` alias, but its body now delegates. `app/api/mcp/serve/[serverId]/route.ts` had an `isJsonObject` with zero call sites — deleted outright. Six helpers were deliberately left alone because they are NOT equivalent: `pagerduty`/`zendesk`/`gitlab` do `(value as Record) || {}`, which type-checks nothing at all, and `copilot/resources/extraction.ts`, `edit-workflow/validation.ts`, `pi/core/events.ts` omit the array exclusion. Those sit on webhook ingress and copilot paths where tightening is a behavior change, not a cleanup; they are audited separately. Two guards were removed rather than substituted, each proven dominated by an earlier check: the bedrock streaming `toolUse.input` guard was unreachable (`parseToolInput` already throws on non-objects before the loop builds `assembledToolUses`), and four `driver.ts` re-narrows follow an `if (!isRecordLike(x) || ...) throw` that dominates the later use. * fix(webhooks): guard non-string GitLab ref, and consolidate the last record helpers Two audits covered the six helpers held back from the previous commit for not being equivalent to isRecordLike. Five are now migrated; one is deliberately not. `gitlab.ts` carried a real crash path, independent of the guard work: const ref = (b.ref as string) || '' const branch = ref.replace('refs/heads/', '') The cast is unchecked and `|| ''` only catches falsy values, so a truthy non-string body field — `{"ref": 12345}` — reaches `.replace` and throws `TypeError: ref.replace is not a function` inside `formatInput`. That runs in the background worker after the webhook is already 200-ACKed, and GitLab does not auto-retry, so the delivery is lost silently. Now checks `typeof`. `pagerduty`/`zendesk`/`gitlab` each defined `asRecord` as `(value as Record<string, unknown>) || {}`, which type-checks nothing — a string or array passed through and was then spread into the workflow trigger payload (`gitlab.ts:114`) as character- or index-keyed garbage. All three now use the shared `toRecord`. These sit behind `verifyProviderAuth`, so reaching them requires the shared secret; this is robustness, not authorization. `copilot/resources/extraction.ts` and `pi/core/events.ts` were array-permissive but provably inert — extraction.ts has no key enumeration or spread anywhere, and events.ts only diverges by returning `null` instead of `{type:'other'}` for an array, which every consumer already no-ops on. Pinned with a test. `edit-workflow/validation.ts` is left permissive ON PURPOSE. Its `Object.entries` walk mirrors the unguarded walk in `operations.ts:86,191`, so an array-shaped `nestedNodes` from the model is currently visited by both. Tightening only the validation side would stop `collectHostedApiKeyInput` from stripping platform-managed API keys while the apply side still creates those child blocks. Both paths have to change together, with tests, in their own PR. * refactor: delete 31 unreachable module-local functions Removes 815 lines of provably dead code: module-local (non-exported) declarations whose identifier appears exactly once in their own file — the declaration itself. A non-exported symbol cannot be reached by an import, a barrel, a dynamic import, or a framework convention, so "unreferenced in its own file" is a complete proof of deadness rather than a heuristic. Notable removals include whole abandoned code paths: `findWebhookAndWorkflow` (78 lines), `calculateBillingProjection` and `initializeUserUsageLimit` (80 lines), `removeCredits` + `deductFromCredits` (54), `sendBatchSMS`, `executeToolBatch`, and four unused `async-runs/repository.ts` queries. Spans come from the TypeScript AST, not a regex. A regex cannot find a declaration's extent — a first-brace scan cuts inside a return-type annotation such as `Promise<{ canCreate: boolean }>` and silently corrupts the file. The AST pass also re-derives deadness from identifier nodes, which caught two wealthbox helpers that a regex export-check had wrongly reported as local. Note the runtime TypeScript API here is `@typescript/typescript6`; the bare `typescript` specifier resolves to the native compiler, which exposes no `createSourceFile`. * refactor: delete 434 stranded exported symbols Removes ~5,930 lines of unreachable code across 166 files: exported symbols that no other file in the repo mentions and that are unused inside their own file. Whole abandoned surfaces go with them — unused React Query hooks (useOrganizations, useOrganizationMembers, useUpgradeSubscription, ...), unused admin route contracts, dead executor constants and reference builders, and the landing-page StageWorkflow/LandingPreviewMount components. Three files left with no remaining code were removed outright. Candidates came from an AST pass; each was then verified individually against an UNFILTERED repo-wide search plus the reachability paths a name search misses: string-keyed tool/block registries, dynamic imports, `export *` barrels, and the docs generator's source-text parsing of tool files. 29 candidates were verified LIVE and kept. Those exposed a flaw in the candidate generator: it indexed only .ts/.tsx, while apps/docs/content/**/*.mdx imports React components directly — ActionImage appears in ~190 MDX pages and ActionVideo in ~115, and both scanned as dead. `app/global-error.tsx` was likewise kept, since Next.js reaches it by filename and its default export can never have a name reference. All 434 deleted names were afterwards cross-checked against every .mdx/.md/.json/.yaml in the repo: no hits. Verified with turbo type-check (23 workspaces), the full apps/sim suite (25,219 tests), all 26 audits, and a production `next build` — the last of these being what actually exercises route- and component-level reachability. |
||
|
|
638f389a77 | fix(knowledge): remove caution triangle overlay from connector icon (#6717) | ||
|
|
3848f97b4c |
fix(grafana): validate against the API docs, add data source querying and contact-point CRUD (#6712)
* fix(azure-data-explorer): correct the tags ingestion-property example
The example rendered as tags="[''daily'']" — doubled single quotes from an
escaping slip, which is not valid Kusto. The reference writes a tags list
as tags='["TagA","TagB"]': single outer quotes with the JSON array's own
double quotes inside.
The clause builder already handled that form; only the example text was
wrong. A template literal avoids the escaping entirely, since the metadata
generator reads the source verbatim and would otherwise carry the
backslashes into the description the model sees.
Adds a test asserting the reference's exact multi-property clause
round-trips, including the comma inside the quoted array.
* fix(grafana): correct response contracts, required alert fields, and outbound request hardening
Validated against Grafana's HTTP API reference and, where the docs
contradict themselves, against the Go wire structs.
Response shapes the tools got wrong:
- update_annotation declared an `id` that was always 0; a patch returns only
a message, so the request's annotation id is echoed and labelled as such
- delete_folder discarded the numeric id Grafana returns and presented an
input-echoed uid as if it came from the API
- delete_dashboard fabricated `id: 0` / `title: ''` via `||` on absent fields
- the contact-point `provenance` description was inverted: "api" means
API-managed, empty means it stayed UI-editable
Requests that could not succeed:
- create_alert_rule left noDataState and execErrState unset and invisible to
the model, but Grafana's validator rejects an empty value outright, so every
model-driven create failed. Both are now sent with Grafana's own defaults,
and skipped for recording rules, which take a different validator
- get_data_source routed a numeric input at /api/datasources/:id, which exists
only behind an off-by-default feature toggle. UID only now
- list_annotations did not trim the dashboard UID, so a padded value matched
nothing
Outbound hardening on the three proxy routes:
- the service-account token was re-sent to redirect targets; the shared fetch
only drops it when asked, so stripAuthOnRedirect is now set
- no timeout was passed, leaving two sequential hops at the 5-minute default
- upstream error bodies were interpolated whole into the tool result, putting
up to 10MB of HTML into logs and traces; now truncated
- UID path segments are URL-encoded so they cannot re-target the request
- update_folder sent both `version` and `overwrite: true`, which Grafana treats
as alternatives, making the freshly fetched version decorative and silently
clobbering a concurrent rename
- replaced the `any` casts with narrowed types
Block surface:
- 25 outputs the tools emit were undeclared and so unreferenceable downstream;
get_data_source had 13 of its 18 unreachable
- `version` was typed string though the dashboard, folder, and data-source
producers all emit a number
- the dashboard title field was shown only for create, so a dashboard could
never be renamed through Update Dashboard
- six list outputs were typed json rather than array
* fix(grafana): let the health check report ill-health, and disambiguate block outputs
The data source health check could only ever report health. Grafana answers an
unhealthy source with HTTP 400 carrying the same {status, message} payload as a
healthy one, and the tool framework converts any non-2xx into an opaque tool
error — so the diagnostic the caller actually wants was unreachable. The check
now goes through an internal route that reads the verdict off either status and
reports it as a successful check, while a failure carrying no verdict (missing
data source, bad token, plugin with no health endpoint) stays a real error. The
plugin's `details` payload is surfaced too.
Also on that route, matching the other three: an outbound timeout, redirect
auth stripping, a truncated upstream error, and a URL-encoded UID.
Block output descriptions: ten keys are emitted by several tools with different
meanings and were described for only one producer — `database` meant both a
data source name and a health status, `annotations` both an annotation list and
an alert rule's summary map. Eleven `json` outputs were opaque although the
tools already document their inner fields. All rewritten to name every producer.
Smaller alignment fixes:
- the same EmbeddedContactPoint.settings field was typed `object` in list and
`json` in create
- list_contact_points mapped non-nullable uid/name/type through `?? null`;
Grafana returns an empty string, which is what create already assumed
- create_alert_rule sent `orgID`, which Grafana overwrites from the
authenticated context, and `Number()` on a non-numeric value put NaN -> null
in the body
- the three update routes declared `output` as required though the auth
short-circuit omits it, and did not declare the `details` they emit on a
validation error
* feat(grafana): complete contact-point CRUD, and add folder move and rule-group read
Four operations the integration was missing, taking it to 29.
update_contact_point / delete_contact_point close a real gap: contact points
could be listed and created but never corrected or removed. Two things worth
recording, because the published docs get both wrong:
- both verbs answer 202 with only a message, not the object. The rendered docs
claim delete returns 204; the current spec and handler both say 202. So the
UID is echoed from the request, the way delete_folder and update_annotation
already do
- update is a full replace with no PATCH counterpart, so name, type, and
settings are all required and the description says so. Omitting
disableResolveMessage resets it
X-Disable-Provenance is exposed on update only. Its polarity is the opposite of
the alert-rule case: omitting it always succeeds, while sending it against an
API-provisioned contact point is rejected — with 403, not the 409 rules use. It
is not exposed on delete at all, because that handler never reads stored
provenance and the endpoint takes no such parameter.
move_folder reuses get_folder's mapping verbatim — same DTO. It always sends
the parentUid key, since Grafana reads an empty value as "move to the root",
which a conditionally-omitted field could not express.
get_alert_rule_group surfaces the group evaluation interval, the one alerting
knob the per-rule operations cannot reach. It reuses the shared mapAlertRule for
the nested rules, and the interval is documented as an integer of seconds.
* feat(grafana): add data source querying, and ground the skill and templates in real tools
query_data_source closes the largest gap in the integration: 29 tools could
read dashboards, folders, and alert configuration, but none could read a metric
value. It posts to /api/ds/query and returns both the raw response and the
frames flattened into rows.
The flattening is derived from the documented layout rather than any data
source's field names: a frame carries schema.fields[] alongside data.values[],
where values[i] is the whole column for fields[i], so zipping them by position
works for Prometheus, SQL, or anything else with a backend.
A failed query is a 400 by Grafana's own status table, so it stays a tool
error — unlike the health check, where the failure status carries the answer.
That also lets four templates and the review-firing-alerts skill stop promising
things the integration could not do. Three templates assumed a metric-query
tool, which now exists. The fourth, and the skill, assumed live alert instance
state, which the provisioning API never returns — they now derive firing rules
from alert-state annotations, which are documented to carry newState and
prevState, and say so explicitly rather than implying a live snapshot.
Deliberately not added: a tool over /api/prometheus/grafana/api/v1/rules for
live instance state. That endpoint appears on no Grafana HTTP API doc page, its
response is only readable from Go internals and test assertions, and the
instance-level state casing differs from the rule level with no documented
contract. Not something to build an output schema on.
* fix(grafana): declare the two block outputs the earlier fixes introduced
Renaming update_annotation's phantom `id` to `annotationId` and adding
`details` to the health check both created outputs the block never declared, so
neither was referenceable downstream. Caught by re-running the output-coverage
check over both integrations; the block now covers all 64 keys the 30 tools emit.
* fix(grafana): make Update Contact Point actually usable from the block
The new replace operation could never succeed. contactPointType and
contactPointSettings were widened to cover it, but contactPointNameNew was
left create-only — and the update maps `name` from that field, so the required
parameter was never supplied.
disableResolveMessage had the same gap, and it matters more than it looks:
the update is a full replace, so a block-driven update was silently clearing
resolve suppression on every contact point it touched. Both fields are now
shown, and required where the API requires them.
Also states a reason on each intentionally-unconstrained response field —
Zod issue objects, alert query stages, notification settings, recording-rule
config, and data-source health detail are all genuinely opaque, but that was
left implicit.
|
||
|
|
41923b8e95 | fix(settings): redirect unavailable tabs to general (#6710) | ||
|
|
d5701d5b2b |
fix(icons): align table block icon and 12-unit icon stroke with the emcn family (#6708)
The table/table_v2 blocks and the table trigger used a local lucide-shaped TableIcon (stroke 2.0, full-bleed 24 viewBox, 3x3 grid) while every other table surface used emcn's Table. Consolidate onto the emcn icon and drop the local copy. Nested tool-call rows in Chat applied no color class, so a non-brand block icon inherited body text instead of --text-icon. redo/undo/zoom-in/zoom-out draw 0.85 stroke on a 12-unit viewBox, rendering 0.992px at a 14px box against the family's 0.904px. 0.775 restores parity. |
||
|
|
9664e7ec17 |
Revert "fix(integrations): read every service mark from one registry (#6682)" (#6707)
This reverts commit
|
||
|
|
b7a5f22abd |
fix(integrations): read every service mark from one registry (#6682)
* fix(integrations): read every service mark from one registry A service looked like itself on the canvas and like nothing in particular everywhere it was connected. `OAUTH_PROVIDERS` registers 93 icons and no colour at all, so the surfaces built on it — the connect dialog above all — drew a flat grey mark for a block whose config already carries its brand icon and `bgColor`. Bridges the two: `resolveIntegrationBlockTypeForOAuth` maps any OAuth id (a service id, a provider id, an extra authorization server) to the catalog block behind it, so a credential surface holding only an OAuth identity can still reach the registry. The connect dialog now wears the block's tile, and `ChipModalHeader` takes a rendered mark so a tile can carry its own chrome instead of being tinted with the header's grey. Folds in the copies that had grown around the gap: `IntegrationTile` resolved its fill from the registry but took its icon from whatever the caller passed — one tile, two sources — and now defaults to the registry, with an override kept for the family service-account marks that genuinely are not the block's. The letter fallback it grew alongside was reading the catalog's `bgColor` while the tile beside it read the registry's; both are the tile now. Two `getProviderIcon` implementations for the same job (one tinted, one not) become one `ProviderIcon`, the connector tile duplicated verbatim across two knowledge-base surfaces becomes one `ConnectorTile`, and the permission rows that hardcoded `text-white` — which renders white-on-white on a pale brand tile — go through `BlockTile`. Public pages keep their generated catalog: importing the registry there would ship 282 block configs to a marketing page, and `integrations.json` is generated from the same `bgColor`, so the two cannot drift. * fix(blocks): drop the dead copies of a block's colour Sweeping the surfaces above turned up colour data nothing reads and colour data two surfaces disagreed on. Dead: `BLOCK_COLORS.DEFAULT/LOOP/PARALLEL` in the tag dropdown (only `VARIABLE` was ever referenced), `BlockIconInfo.color` on table columns — whose consumer documents that it deliberately ignores the colour, so the `#2F55FF` behind it could never render — and the `bgColor` threaded into the add-resource dropdown, whose row renders a bare tinted icon. Disagreeing: the Variables tile is `#2F8BFF` in the tag dropdown and `#8B5CF6` in the preview panel, for the same "V" on the same concept. Both now read `VARIABLE_TILE_COLOR`, and the preview panel's two hand-rolled squares become `BlockTile` like every other tile. Four spellings of the neutral fallback (`#6B7280`, `#6b7280`, `#666666`, and a `cancelled` status that happened to equal it) now point at `DEFAULT_BLOCK_TILE_COLOR`. The terminal and logs resolvers stay. They look like duplicates of `accent.ts` but carry behaviour it does not have — status fills for synthesized error/validation/cancelled rows, near-black contrast correction, MCP tool-id parsing, and a model-provider branch — so folding them in is a behavioural change, not a deletion. * chore(copilot): delete the mention machinery nothing calls The workflow panel's copilot tab renders `MothershipChat`, and that component brings its own input — so `panel/components/copilot` no longer holds a component at all, only the hook library the old input used. Five of those hooks have no caller anywhere: `useMentionData`, `useMentionKeyboard`, `useCaretViewport`, `useMentionInsertHandlers`, `useTextareaAutoResize`. They are not all of it. `home/components/user-input` still imports `useFileAttachments`, `useMentionMenu`, `useMentionTokens`, `useContextManagement`, and `useIntegrationAutoMention` from this directory, so it survives as a shared hook library rather than dead weight — which is why this removes the uncalled five rather than the folder. What they alone reached goes with them: `getFolderData` / `getFolderLoading` / `getFolderEnsureLoaded` and the `FOLDER_CONFIGS` table describing every mention folder, `buildMentionHighlightNodes`, the `MentionFolderNav` type, and the slash-command tables. Of the 266-line constants file only `SCROLL_TOLERANCE` had a live reader left. * fix(integrations): never answer with a sibling's tile Two integrations can share one OAuth id: Google Slides is authenticated by Drive's `google-drive` service and Jira Service Management by Jira's `jira`. Indexing first-write-wins made those ids resolve to whichever sorted first, so the dialog connecting Slides could wear Drive's brand. An id claimed by more than one block type now resolves to neither, and the caller keeps the service-specific mark it already had. A wrong brand is worse than no tile. |
||
|
|
5a88ce22d1 |
feat(ashby): incremental job sync, custom field writes, and application lifecycle ops (#6703)
* feat(tools): add incremental job sync and draft postings to Ashby reads
list_jobs accepts Ashby's syncToken and returns it as nextSyncCursor, so a
scheduled sync costs O(changed reqs) instead of rescanning every req. Ashby only
returns the token once the last page is drained, which the param description
states.
The output is named as a cursor deliberately. It is an opaque resumption marker,
not a credential, so it belongs with nextCursor - and a field literally named
syncToken matches the /^.*token$/i deny-list in redaction and renders as
[REDACTED], which makes an incremental sync unusable since the operator cannot
read the value the next run needs. The wire name stays syncToken.
list_job_postings gains includeUnpublishedJobPostings, plus the posting status
field - without status a caller cannot tell a returned draft from a published
posting, which makes the flag useless.
Also widens the custom field valueLabel type, which MultiValueSelect returns as
an array, for the write operations that follow.
* fix(tools): render Ashby object-shaped API errors readably
Ashby documents two error shapes and uses both. The `errors` array form carries
`{ message, parameter }` objects, which stringified to '[object Object]' and hid
the real cause - including the 403 a key gets when it lacks a module permission.
Also adds the shared pieces the new write operations need: one definition of the
custom field value shape for the read and write paths to agree on, and a
normalizer for Ashby's case-sensitive objectType enum so a model emitting
'candidate' fails here with the allowed values rather than at the API.
* feat(tools): add Ashby custom field writes, delete, source, and anonymize
customField.setValue/setValues are the only way to annotate a job or req, since
Ashby has no job notes and no job tags. Writing null clears a value, so the
annotation is reversible.
Because null clears, every one of these operations requires explicit intent
before it can destroy data. The block's required markers do not cover the agent
path - a model calls the tool directly, so tools.config.params never runs and
validateRequiredParametersAfterMerge skips a param marked not-required:
- set_custom_field_value rejects an absent or blank fieldValue; an explicit null
still clears
- change_application_source requires unsetSource to clear, and rejects a source
id and an unset request together, since preferring either one silently
discards the other. Ashby has no 'leave unchanged' mode, so setting and
clearing are the only two intents and exactly one must be expressed
- set_custom_field_values rejects an empty array locally rather than relying on
Ashby to reject it
application.delete needs candidatesDelete, a module permission separate from
candidatesWrite. candidate.anonymize strips PII but leaves the record; Ashby
exposes no candidate deletion endpoint.
* test(tools): cover the new Ashby request and response shapes
Includes a gated live harness (ASHBY_LIVE=1) alongside the mocked tests.
vitest.setup.ts stubs global fetch for every file in the app, so the live file
restores the real implementation and asserts the restore worked - without that
guard the whole suite silently passes against a mock.
* feat(blocks): expose the new Ashby operations in the block
fieldValue is polymorphic (boolean, number, string, array, object, null), so it
decodes structured input and otherwise passes text through. The decoding is
deliberately narrow rather than a blanket JSON.parse, which corrupts real text:
1e999 becomes Infinity and serializes back out as null, which CLEARS the field;
a long numeric id loses precision past 2^53; and prose starting with { turns into
an object. Only the literal keywords, {, [ or " prefixes, and exactly
round-tripping numbers decode.
fieldValue carries no wand generationType: json-object forces braces and
json-array forces brackets, and both would wrap a value that must stay bare.
fieldValues, whose contract really is an array, uses json-array.
Setting and clearing an application source are mutually exclusive, so the Source
ID field is conditioned off while the clear switch is on and the params mapping
sends only the intent the switch selects. A value typed before the switch was
flipped cannot reach the tool and surface as an error with no visible cause.
* docs(ashby): document the new operations, permissions, and limitations
Ashby scopes permissions per module and they fail at runtime, not build time, so
the block docs now carry the permission table. Also records the hard API limits
worth designing around: no note or tag on a job, no pagination on
jobPosting.list, and no delete for jobs, candidates, or custom field definitions.
* fix(blocks): stop a stale create-path source id leaking into a source change
The executor merges { ...inputs, ...transformedParams }, so any key the params
mapping leaves unset inherits whatever inputs held. The shared create-path
sourceId subblock reaches inputs even on change_application_source: it is mode
'advanced', and the serializer includes an advanced subblock whenever its value
is non-empty without ever evaluating its condition (serializer/index.ts).
So a source id typed while on Create Application survived into a source change.
With both fields blank it silently attributed a source nobody asked for, and
with the clear switch on it collided with the unset request and failed with no
visible cause, because the field producing it is hidden in that state.
sourceId is now always assigned for this operation rather than conditionally,
so it can never inherit. The regression test asserts the merged result rather
than the mapping alone, since the gap between them is where the bug lived.
|
||
|
|
3d4e3d26dd | fix(settings): preserve tab when switching workspaces (#6704) | ||
|
|
af076a7a34 |
fix(logger, blocks): log nested errors and stop spurious model-selection warnings (#6706)
* fix(logger, blocks): log nested errors and stop spurious model-selection warnings
Two production defects found in the prod logs, neither release-related.
logger: mergeArgs copied object arguments verbatim, so an Error held under a
key stayed an Error instance and JSON.stringify rendered it {} — message and
stack are non-enumerable on Error.prototype. 399 call sites use the
logger.x('msg', { error }) shape and every one logged error: {}, which is why
BlockOutputs failures (10k/week) were undiagnosable. The colorized path had
the same hole via formatObject.
blocks: router, evaluator and agent resolved config.tool through
getBaseModelProviders(), which deliberately excludes gateway providers
(OpenRouter, vLLM, LiteLLM, Ollama, ...). A valid openrouter/* model therefore
threw "Invalid model selected", as did a model still holding an unresolved
<variable.x> at serialization time — ~140 warnings/day. The value is cosmetic
(every handler re-derives the provider from the resolved model), so the throw
bought nothing.
- unwrap keyed Errors on both the structured and colorized log paths
- keep `error` a plain message string so log queries can group on it
- resolve serialized provider ids through getProviderFromModel, the same
resolver the executor uses, via one shared helper for all four call sites
- drop the unreachable `if (!model)` checks behind `params.model || default`
* fix(blocks): move the fallback rationale into TSDoc
The repo forbids non-TSDoc comments; the explanation for why recovery returns
a constant instead of resolving again belongs on the declaration anyway.
|
||
|
|
5bb59f08ee |
feat(connectors): add 9 knowledge base connectors (#6699)
* feat(connectors): add 9 knowledge base connectors
Box, Zoho Desk, PagerDuty, Trello, Microsoft Excel, Google Slides, Google
Vault, Mintlify, and SFTP. Selected by intersecting the published connector
catalogs of Glean, Onyx, Dust, Vectara, Writer, Guru, Elastic, Microsoft 365
Copilot, Notion AI, Unstructured, and Airbyte against services that already
ship a Sim block, so OAuth providers, credentials, and icons are reused. Box
was the largest gap, appearing in 7-8 of ~10 catalogs.
Every connector was validated against live provider documentation twice, the
second pass treating the first pass's conclusions as unproven. Notable
correctness work that came out of that:
Listing truncation. The sync engine hard-deletes documents past a cap that is
not flagged with `listingCapped`, and five connectors had a path there — an
empty Mintlify discovery, Zoho Desk's exact-multiple default caps, Trello's
archived lists and 1000-card ceiling, a Google Vault cursor bailout, and a
PagerDuty stalled page. The engine also gained a backstop: an empty or
collapsed listing blocks deletion reconciliation until the same observation
repeats on a consecutive sync, reconstructed from existing sync-log counters
so no migration is needed.
API alignment. `desk.zoho.ca` does not resolve (Canada is
`desk.zohocloud.ca`, and Singapore and UAE were missing); `modifiedTime` is
absent from Zoho's ticket list projection, so every ticket re-embedded on
every sync; Trello's `dateLastActivity` is documented to miss some edits;
PagerDuty's 10,000-record ceiling bounds `offset + limit`, not offset; Excel
indexed dates as raw serial numbers while Google Sheets renders them; Google
Vault truncated at roughly 249 matters.
Security. SFTP followed symlinks in `getDocument` and composed unchecked
server-supplied filenames into paths; it now also supports optional host-key
fingerprint verification, which runs during key exchange before any password
is sent. Trello interpolated user-supplied board ids into URL paths. Google
Vault is narrowed to `ediscovery.readonly`. `getDataverseBaseUrl` accepted
any host while attaching a bearer token, and is pinned to Microsoft's
Dataverse domains — pre-existing shipped code, fixed here.
Also adds `ConnectorAuthConfig.optional` so a public source can be configured
without inventing an API key, and teaches the scope check that a granted
read-write scope satisfies a required `.readonly` sibling.
Microsoft Dataverse was built and then removed: its OAuth cannot complete
consent. Dataverse requires a per-environment resource URI, the provider
declares a static `https://dynamics.microsoft.com/user_impersonation` that is
not an Entra Application ID URI, and the environment URL is only collected
after the credential exists. That predates this change and also affects the
12 shipped Dataverse tools.
* fix(dataverse): strip the bearer token when a request redirects
The host allowlist added alongside the connector work only constrains the
initial destination. `secureFetchWithPinnedIP` follows redirects and keeps the
`Authorization` header unless a tool opts out, so a redirect away from an
allowed Dataverse origin would forward the caller's OAuth token to whatever
host answers. Dataverse redirects in normal operation — file downloads hand
back a signed storage URL, and environment hosts move between regional
origins — so this is reachable without a compromised environment URL.
Sets `stripAuthOnRedirect` on all 18 Dataverse tools, matching the existing
GitHub job-logs and Windchill precedent.
* fix(connectors): address review findings on listing and hashing
- microsoft-excel: `fetchWorksheets` read only the first Graph page and never
followed `@odata.nextLink`. A workbook with more sheets than fit in one page
dropped the remainder from the listing without setting `listingCapped`, so
the sync engine reconciled those documents away as deleted. The walk now
pages, bounded by MAX_WORKSHEETS, and only follows a nextLink that stays on
the Graph origin, since the link is server-supplied and carries the token.
- google-slides: the listing `contentHash` covered only the file id and
modified time, so toggling the speaker-notes option left every stored hash
matching and no presentation was ever re-hydrated with the new scope. The
setting is now part of the hash, in the single shared stub builder so the
list and hydrate paths stay identical.
- mintlify: `pathPrefix` filtered with a bare `startsWith`, so a prefix of
`/guides` also matched a sibling like `/guides-archive`. It now shares the
`/`-boundary rule `withinBasePath` already used, extracted as `isUnderPath`.
* fix(connectors): list newest first in zoho desk, accept a trailing slash prefix
- zoho-desk: `sortBy: 'createdTime'` is ascending — Zoho denotes descending
with a `-` prefix — so the default 500-record caps kept the oldest tickets
and articles and recent ones were never listed. Because the cap sets
listingCapped, that stale tail could not reconcile away either. Now sorts
`-createdTime`. Still ordering on createdTime rather than modifiedTime, so
rows do not reshuffle mid-walk.
- mintlify: `resolvePathPrefix` kept a trailing slash while `isUnderPath`
accepts an exact match or `prefix + '/'`, so `/guides/` matched neither
`/guides` nor `/guides/intro` and the source synced nothing. A regression
from the previous round, which replaced a bare `startsWith`. The prefix is
now normalized before comparison.
* fix(dataverse): strip the bearer token on the upload route's own redirect
`upload_file` posts to an internal route rather than calling Dataverse
directly, so the tool-level `stripAuthOnRedirect` added in
|
||
|
|
cf78946529 |
fix(v2): close seven correctness and honesty gaps found sweeping the API (#6702)
* fix(v2): close seven correctness and honesty gaps found sweeping the API A ten-slice sweep of the live v2 surface turned up no regression from the recent cancellation work, but did surface a set of pre-existing defects where an endpoint either lost data, hid a failure, or reported something that was not true. Each is fixed at the layer that owns the behavior. Terminal execution logs. The two force-fail boundaries wrote `status: 'failed'` without `ended_at` or `total_duration_ms`, so a force-failed run dropped out of every duration-filtered log query — the same defect class already closed for cancellation, still open on its sibling. The cancellation payload factory is generalized to take the status; the cancellation call sites are untouched and still emit a byte-identical row. Custom tools. One malformed row failed the whole page, and because the list is keyset-paginated that row made every page containing it permanently unreachable. The projection now validates against the same contract schema the route builder applies, repairing only what can be repaired without inventing information — a stringified schema, and a missing `type` whose contract admits exactly one value — and omitting with a warning what cannot. Both rows observed in production are recovered rather than discarded. Table filters. `eq`/`ne`/`in`/`nin` compiled a wrongly-typed operand into a containment test that silently matched nothing, so a filter written against the value the write path had stored returned an empty page instead of its rows. The operand is now read through the same column-type registry the write used, and rejected only where that registry refuses it. Range operators already behaved this way; `null` and the cleared-cell sentinel still pass through untouched. Error messages. A custom `error` on a string schema also replaced the wrong-type wording, so supplying a number for a name reported that the name was missing. Messages now distinguish an omitted field from a mistyped one, `topK` names its own bounds, the knowledge search refine reports against a field rather than the whole body, and a workspace id is bounded before it reaches a lookup. Archived file metadata. A soft-deleted file was listed but unreadable, leaving no way to check share state before restoring it. The read takes the same `scope` selector the list already exposes; the default is unchanged, and the parameter relaxes only the `deleted_at` predicate, never the authorization. Cancellation reporting. Cancelling an already-terminal run reported a durable write that never happened. The service now distinguishes the no-op and names the state it observed, and both surfaces present one vocabulary instead of the internal route deriving its own. No claim predicate or write changed. Protocol. A 401 carries a challenge naming the header the API actually reads, and a body that failed to parse is reported as an unsupported media type only when the caller positively declared a non-JSON one — after the read has already failed, so nothing that succeeds today can begin to fail. * fix(v2): correct three regressions this branch introduced, and harden its tests Adversarial review of the previous commit found that three of its "behavior preserving" claims were wrong. Each is corrected here at the layer that owns it. Table filters no longer coerce a `date` operand, and no longer throw. `date` is the one column type whose registry `coerce` is not idempotent — it drops sub-second precision — and the leaf that compiles a filter also builds the unique-constraint and upsert-conflict probes, so re-reading an already-coerced operand could stop it matching the row it was written from and admit a duplicate inside the write transaction with no error. Throwing was the second mistake: the v2 predicate grammar type-checks structure but not operand values, so a rejected operand no longer failed at submission but inside the delete, update, dispatch and cancel runners, where a filter that cannot compile means the cells it started can no longer be cancelled. Coercion is now total — it rewrites what the registry accepts and passes everything else through unchanged, exactly as before. Reviving a force-failed run no longer inherits its terminal duration. Writing `ended_at` and `total_duration_ms` on the force-fail boundary was correct in isolation, but a partial resume flips that row back to `pending` and those columns survived. The preserved value is meant to be the pause checkpoint — the run's active time — and it had become wall clock measured at the failed resume, which the checkpoint rule then faithfully carried into the next terminal write. The revival clears them only for a row that was terminal, so an ordinary paused row keeps the checkpoint it is supposed to keep. Cancelling reports the terminal state it actually observed. Reclassification now requires that nothing else went wrong, so a genuine paused-reconciliation failure survives instead of being rewritten as an already-terminal no-op, and the claim's own row count — not a snapshot read before it — decides whether this cancel terminalized the run or lost a race to something else. The status the snapshot needed rides along on the ownership query that already reads the row, rather than the second read that query's own contract warns against. A custom tool that cannot be projected now answers the same way everywhere: the list omits it, and reading or patching it by id reports it as absent rather than as a server fault. Analytics stops reporting a cancellation for a request that cancelled nothing. The tests around all of this were audited by mutating each fix and checking the suite noticed. Where it did not, the assertion is stronger now: the absent content-type branch is genuinely exercised rather than relying on a header the client library supplies, the duration encoder is pinned to the column it must measure from, execution ownership is pinned to both ids it must match, and the archived-file concealment test proves it conceals the archived read specifically. Two tests that asserted a paused branch they could not observe are gone; the rendered-SQL test that can decide it already covers them. * fix(execution): report a workflow-group cancellation as the write it performed Cancelling a workflow-group run whose log had already been cancelled, but whose cell sidecar still needed reconciliation, durably cancelled that sidecar and then reported `already_cancelled` with `durablyRecorded: false` — because the terminal-status shortcut answered from the entry snapshot alone and never asked what this request had written. The analytics event, which now gates on that field, stopped firing for a cancellation that really happened. The outcome a cancel reports is the same question whichever path answers it, so there is now one vocabulary for it rather than one the direct claim tracked and one the group transition did not. Every group result maps to that outcome through a total map, so a new group result cannot compile without deciding what it wrote, and the reclassification leads with whether this request wrote at all. A group transition that reports itself already cancelled is deliberately mapped as unknown rather than as a no-op: it leaves the sidecar alone but still terminalizes a log that was active, and the result does not say which happened. That costs nothing today, because the only snapshot that would reclassify proves the log was already terminal. * fix(execution): have a workflow-group cancellation report the writes it made Three review findings landed on the same reporting logic, each a different face of one cause: the caller could not see what the group transaction had written, so it inferred. It inferred from an entry snapshot, then from the returned kind, and the remaining blind spot was the kind that covers two different transactions — a repair that terminalizes an active log, and a genuine no-op — which left a cancel that wrote nothing still claiming a durable write when it lost a race. The transaction now reports both writes it can make, each read from that statement's own returning row and recorded immediately before the throw that already depended on it, so the report cannot drift from the write. The caller derives its outcome from those rather than from the kind, and the kind is back to naming the situation instead of standing in for the work. The group path can now always answer whether it wrote. The only remaining unknown is the direct claim when its update throws or is never attempted, which genuinely has no row count to report. |
||
|
|
237f973a11 |
fix(condition): stop a secret value from breaking or forging a condition (#6705)
Condition expressions pasted every environment variable value into the
expression as source. Block references in the same expression go through a
proper escape and get quoted; env vars went through neither. That left three
defects:
- A bare string placeholder was a SyntaxError. `{{NAME}} === 'alice'` resolved
to `alice === 'alice'`, so the form the Function block docs recommend could
not be used here at all.
- Ordinary data broke the block. An apostrophe (`O'Brien`) or a newline in a
legitimate value produced unparseable source and failed the run.
- The quoted form was injectable. A value of `x' || true || '` turned
`'{{NAME}}' === 'bob'` into `'x' || true || '' === 'bob'`, forging a true
branch out of a comparison that should be false.
Inline only structurally inert literals — numbers, booleans, and null, with
optional space/tab padding. Every other value keeps its `{{NAME}}` placeholder
and is bound as a string by the execution-boundary compiler, the same one
Function blocks and Custom Tools already use.
Legacy outcomes are preserved. `{{COUNT}} === 3` and `{{ENABLED}} === true`
still compare as literals, and an embedded `"Bearer {{API_KEY}}"` still
compares equal — now via compiled concatenation rather than a pasted value.
Padding is admitted rather than trimmed so the inlined text stays
byte-identical to the stored value, which is what keeps a padded number
correct both bare and quoted.
A resolved secret also no longer travels to the execution boundary inside the
condition source.
The one deliberate behavior change: a value whose text is itself a quoted JS
literal (a secret stored as `'foo'`, a plausible workaround for the bare-string
SyntaxError) now compares as the 5-character string rather than as source.
That form is the injectable one, so it cannot be kept.
Docs: state the placeholder type contract, which was described mechanically but
never in terms of what a reader gets. `{{KEY}}` in Function and Custom Tool code
always evaluates to a string, so a bare `if ({{FLAG}})` is always true and a list
has to be stored as JSON. This is what a customer hit after the resolver lift in
#6247 moved Function blocks off source inlining.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a7115e87ee |
feat(integrations): add Azure Data Explorer (#6701)
* feat(integrations): add Azure Data Explorer Add a 14-operation Azure Data Explorer (Kusto) integration covering KQL queries, schema and metadata discovery, table management, inline and query-sourced ingestion, ingestion-failure triage, and arbitrary management commands. Authentication uses a Microsoft Entra service principal through an internal proxy route, since the Kusto token audience is per-cluster and cannot be expressed as a static-scope OAuth provider. * fix(azure-data-explorer): only read partial-failure status from the QueryStatus table Scanning every returned table for Severity and StatusDescription columns misread an ordinary query as a failed request whenever the user's own result selected columns of those names — a common shape for a log table. Failure detection now consults only the table the response's table of contents names as QueryStatus, and primary-result selection reuses the same index instead of re-reading it. * fix(azure-data-explorer): keep the Show Operations and Show Table Details cards from painting empty check:canvas-sentences flagged the Show Operations sentence: it anchored `core` on operationId, which is an advanced-mode optional field, so an untouched card resolved to nothing and painted empty. Show Table Details had the same shape in milder form — table is optional there, since omitting it describes every table, leaving a dangling preposition. Both now lead with literal copy and treat their field as an optional refinement. Also simplifies the primary-table condition to a single `!= null` check. * fix(azure-data-explorer): authenticate sovereign clusters against their own Entra authority The cluster allowlist accepted Azure China and US Government hosts, but every token request went to login.microsoftonline.com. Those clouds are isolated instances with their own Entra endpoints, so a sovereign cluster passed URI validation and then could never obtain a token. Each Kusto service domain is now declared alongside the authority that issues tokens for it, so the two cannot drift apart, and the authority is part of the token cache key. * improvement(azure-data-explorer): warn that ingest-from-query matches columns by position Kusto aligns an ingested query result to the target table on column type and order, never on column name, so a query projecting the right columns in the wrong order lands data in the wrong columns without erroring. Surfaces that in the tool description and param the model reads, in the wand prompt that generates the query, in the rollup skill's steps, and in the docs. Also verifies the target schema first rather than after. * chore(azure-data-explorer): drop the unsourced kustomfa host from the cluster allowlist Every other entry traces to a Microsoft reference — the Kusto connection-string doc, the national-cloud endpoint tables, and the Fabric KQL-database REST reference. kustomfa.windows.net does not, and the connection-string doc states the trust boundary as hostnames ending in kusto.windows.net. An allowlist should only hold hosts we can justify, so this drops it and records the sourcing standard for anything added later. * fix(azure-data-explorer): handle commas inside quoted properties and empty extent IDs Two defects in the shared command helpers: buildWithClause split the property list on every comma before validating, so a value that legally contains one — a docstring sentence, or a tags array with more than one entry — was torn in half and rejected. Splitting is now quote-aware, and an unterminated quote is rejected outright rather than swallowing the rest of the clause. transformColumnListResponse dropped empty strings, but `.ingest inline` reports "no data shards were generated" as a single record carrying an empty extent ID. A no-op load therefore looked like a missing column instead of an empty result. Only non-strings are skipped now. |
||
|
|
1d342722ad |
feat(rabbitmq): add RabbitMQ integration (#6700)
* feat(rabbitmq): add RabbitMQ integration * fix(rabbitmq): strip auth on redirect, require https, and bound the retrieval response * fix(rabbitmq): reserve message metadata in the retrieval response budget |
||
|
|
3051954396 |
fix(agent): show files in tool picker (#6666)
* fix(agent): show files in tool picker * chore(agent): remove redundant tool exclusions |
||
|
|
7f64d5e600 | perf(tables): stop a table write refetching every loaded page in the tab that made it (#6698) | ||
|
|
41819124d7 | fix(search): stop cmd+k boosts from lifting weaker matches over stronger ones (#6668) | ||
|
|
0650eab6e0 |
fix(desktop): let sites copy to the clipboard in the browser tab (#6696)
The agent partition denied every site permission, which included clipboard-sanitized-write — the permission Chromium routes navigator.clipboard.writeText through. Copy buttons rejected with NotAllowedError and did nothing at all: no error, no copied text. Sites still on document.execCommand('copy') kept working, which is why only some looked broken.
Granting it hands the page no reach it lacked. Chromium still requires the document to be focused and holding a transient user activation, and a sanitized write only places text the page already renders onto the clipboard. Reading stays denied, along with media, geolocation, and notifications.
|
||
|
|
0239db80c6 |
fix(desktop): report a stock Chrome user agent in the browser tab (#6695)
Electron's default user agent carries Sim/<version> and Electron/<version> tokens, and the detection libraries sites gate on test for Electron before Chrome — so the browser read as "Electron", which is on no site's supported list. Ashby warned "Ashby does not support this browser"; stricter sites refuse to render. Rebuild the string as the desktop form Chrome's user-agent reduction specifies — same platform token and Chromium major version, the rest zeroed, no application or Electron token — and apply it to both the browser partition session and each tab's WebContents. Service workers do not inherit a tab's user agent, so without the session-level call a worker's script request still announced Electron. Scoped to the browser partition: app.userAgentFallback is left alone so the Sim shell's own user agent is unchanged. |