mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
cc1a278d7307347dbadc9f14a06fc069b94ca794
6249
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cc1a278d73 |
fix(integrations): close defects found by an independent cold audit (#6767)
* fix(integrations): repair Update SLO and advanced OData filters
An independent audit — eight cold readers, one per integration, given no
prior findings — checked the eight integrations merged to staging today.
Two defects broke an operation outright; both are fixed here.
datadog: Update SLO rewrote every non-metric SLO to `metric`. The SLO Type
dropdown carried a `metric` default and its condition covered both create and
update, so an untouched control reached mergeSloUpdatePayload as an edit. A
metric SLO requires `query`, and the merged body carries monitor_ids or
sli_specification instead, so Datadog rejected it — Update SLO was unusable on
monitor-based and time-slice SLOs, with no way to express "keep the current
type". Update now has its own control defaulting to "Keep current".
microsoft_ad: list_users, list_groups and list_service_principals emitted
$count=true only alongside $search, so any $filter using an advanced operator
(ne, not, endsWith, startsWith on non-indexed properties) returned 400. Graph
requires $count=true with ConsistencyLevel: eventual for those. list_devices
already did this correctly; the other three now match it.
Also from the same audit: Datadog path IDs are trimmed before encoding in all
20 URL builders rather than 2, matching the existing get_monitor test.
* fix(integrations): cloudflare, crowdstrike, and mssql audit findings
From the independent cold audit. Cloudflare: DNS analytics no longer emits
fabricated min/max telemetry (Cloudflare documents both as always empty);
purge_everything defaults to specific-purge and errors when combined with
target lists; three unsourced description claims corrected; Array.isArray
guards on four older list transforms.
CrowdStrike: IOC sort placeholder corrected to the dot form (created_on.desc,
not the nonexistent created_timestamp); the 500-indicator cap relabelled as a
Sim bound rather than a CrowdStrike one; credential failures return 401 rather
than 500; prevent_no_ui noted as unenumerated.
MSSQL: introspect no longer lets the model choose the database; row and byte
caps on reads; introspection collapsed from 4N+2 to 6 fixed queries; WHERE and
identifier guards run before the connection opens so rejections are 400 not
500; SAVE TRANSACTION, OPEN/CLOSE key, DEALLOCATE and ADD SIGNATURE added to
the statement screen as two-token phrases; encrypt wording corrected to say
TDS 7.4 encryption is negotiated, not guaranteed.
* fix(splunk): publish the real output tables and read the errors Splunk sends
The docs generator parses tool source text and resolves a shared `outputs`
const only from the family's `types.ts`, so Splunk's helpers in `utils.ts` were
invisible to it: seven operations published the block's union of every output
instead of their own. Run Search and Get Search Results each shipped a ~50-row
table naming savedSearches, alerts, indexes, and apps they never return, Cancel
Search Job lost `messages`, and the four list tools lost `total`/`offset`.
Inline the four helpers into each consuming tool and delete them, since
relocating a shared const only moves the trap.
Also:
- Add a `splunk-errors` extractor for the documented `{messages: [{type, text}]}`
envelope and set it on all twelve tools. A rejected SPL string, the most common
failure, previously fell through to the status text and reported "Bad Request".
- Read `searchEarliestTime`/`searchLatestTime` with `asNumber`. The job entry
documents them as bare epoch numbers, so `asString` returned null for every
`output_mode=json` response.
- Project the `<messages>` block of the XML job-control response. It is the only
payload that endpoint returns, so `cancel_search_job.messages` was always empty.
- Mark the nullable job outputs optional, matching the transform.
- Default `run_search` to `max_count=1000`. A oneshot search has no paging escape
hatch and Splunk's own default is 10000 rows in one buffered response.
- Add suggested skills to `SplunkBlockMeta`, grounded in `tools.access`.
The regenerated tool metadata also picks up the Cloudflare and MSSQL output
changes from the previous commit, which were never synced.
* fix(okta,servicenow): apply integration audit findings
Cherry-picked from fix/okta-servicenow-audit-followups (6db0f54), whose base
predated the earlier audit round; the duplicate isOktaFlagEnabled that produced
is resolved in favour of the existing richer helper, which already accepts
'yes'/1/'on' as well as true/'true'.
okta: get_logs no longer advertises hasMore forever. A System Log query with no
'until' is a polling query, and Okta always returns a next link for one, even on
an empty page — so any loop driven by hasMore never terminated, including the
one our own shipped skill instructs the agent to run. errorCauses is now
surfaced, so a failed write reports the real reason instead of the useless
'Api validation failed: profile'. sendEmail routes through one coercion helper
across all four lifecycle tools. update_group's declarative fallback throws
rather than silently truncating an extensible group profile.
servicenow: attachmentLimit and limit no longer overwrite each other. Neither
assignment was scoped to an operation, so all 12 paginated operations could
silently return a row count the user never asked for — defeating the block's own
design, which gave attachmentLimit a unique id precisely to avoid this. All
seven approval states are published by ServiceNow and are now reachable from the
filter, with the space-vs-underscore punctuation documented. The five legacy
generic tools route through the shared response helpers, and the folder's only
'any' is gone. Block skills now name the semantic operations.
* chore(integrations): regenerate catalog and docs artifacts
* fix(integrations): disclose MSSQL truncation and keep Okta's poll cursor
Three defects the review round found in the audit fixes themselves.
MSSQL capped a recordset and then reported it as complete: `executeQuery`
computed `truncated`/`truncationReason` but all five statement routes returned
only `message`, `rows`, and `rowCount`, so a caller could not tell paging was
required. A shared `toRowsResponseBody` now folds the reason into `message`
for an agent reading the status line and exposes the two fields for a caller
that branches on them.
The byte ceiling also admitted a single oversized row as a lone exception, so
one `nvarchar(max)` value serialized an unbounded body — the ceiling bounded
everything except the case it exists for. A row is now admitted only when it
still fits, and the drop is disclosed rather than read as an empty table.
Okta's `get_logs` nulled `nextCursor` alongside `hasMore` on an empty polling
page. Terminating the loop is right, but the cursor is the resume handle Okta
tells callers to persist, so a scheduled workflow that hit one quiet interval
restarted from `since` and re-delivered events it had already processed. The
two answer different questions and now diverge.
Cloudflare's purge block no longer lets the invalid combination be built: the
four target fields are hidden once Purge Everything is selected, so the tool's
guard is a backstop rather than a reachable hard error.
* fix(okta,servicenow): stop sending requests the APIs reject
Okta documents `since` and `after` on the System Log as mutually
exclusive, so `get_logs` lets the cursor win rather than sending both —
the shape a scheduled poll that persists the cursor would otherwise send.
Seven boolean query params reached Okta interpolated raw, so an agent
tool call supplying `yes` was rejected. Each now routes through
`isOktaFlagEnabled`, keeping its existing send-or-omit behavior.
A cleared ServiceNow limit/offset/quantity stayed `''` through the block
mapper and was appended as a valueless `sysparm_limit=`. The mapper now
resolves a blank to undefined, and the tools skip a blank as well.
* fix(integrations): mssql guard gaps and Entra query, scope, and output findings
MSSQL read-only screen
- Screen RENAME, documented T-SQL DDL for Azure Synapse dedicated SQL pools and
Analytics Platform System, which are reachable over TDS with exactly the
connection fields this block exposes. `SELECT 1 RENAME OBJECT dbo.t TO t2`
was a schema change passing an operation advertised as read-only.
- Screen the Service Broker family: RECEIVE as a word, and END/MOVE/GET
CONVERSATION and SEND ON CONVERSATION as two-token phrases, since END closes
every CASE. RECEIVE is a destructive read and END CONVERSATION WITH CLEANUP
drops a conversation's messages.
MSSQL routes and block
- Build the insert statement before connecting, matching update and delete, so a
bad identifier answers 400 instead of burning a TLS+login and returning 500.
- Declare `truncated`/`truncationReason` on the block, which the tools declare
and the routes emit but the block left unreferenceable.
Microsoft Entra ID
- Pair `$count=true` with `ConsistencyLevel: eventual` conditionally. Graph
documents `hasMembersWithLicenseErrors`, `isLicenseReconciliationNeeded`, and
`identities/any(i:i/issuer)` as filterable only *without* advanced query
parameters, and documents advanced queries as unsupported in Azure AD B2C
tenants, so the unconditional pair broke filters that previously worked. When
continuing from a nextLink the pairing is read off the link itself.
- Request `LicenseAssignment.Read.All` instead of `Directory.Read.All`. The
latter was needed by `GET /subscribedSkus` alone, whose permission table names
the former as least privileged and does not list the ReadWrite scope we hold.
- Enumerate the block's real output keys instead of a single `response` object
no tool emits.
* fix(splunk,datadog): stop truncating searches and send mute/unmute as query params
Splunk run_search: revert the `max_count=1000` default added last pass. It was
wrong on both halves. Splunk documents the parameter as "the number of events
that can be accessible in any given status bucket. Also, in transforming mode,
the maximum number of results to store" — so for a non-transforming oneshot it
bounds status buckets, not the response, and for a transforming search (`| stats`,
`| timechart`, which is what the block's own skills generate) it capped results
at 1000 where Splunk would have stored 10000, silently. The block's `maxCount`
placeholder already read `10000`, contradicting the code. Send `max_count` only
when the caller sets it and restate the description in Splunk's own terms,
matching create_search_job. The real guidance — a oneshot buffers the whole
result set, so use Create Search Job + Get Search Results for anything large —
moves into the tool description and the search-splunk-logs skill.
Datadog mute/unmute: send `scope`, `end`, and `all_scopes` as query parameters.
`MuteMonitor` and `UnmuteMonitor` declare no `requestBody` in the authoritative
spec (docs.datadoghq.com/resources/json/full_spec_v1.json — the generated
datadog-api-client-go v1 schema omits both operations and is a subset, not the
authority); all three parameters are `in: query`. Sent as a JSON body they are
dropped, so a scoped, time-boxed mute becomes an indefinite mute across every
scope and unmute's "all scopes" never applies — answered with a 200 and the full
monitor object, so nothing surfaces.
Datadog list_monitors: imply `page=0` when a page size is set without a page.
Datadog "returns all monitors without a `page_size` limit" when `page` is absent,
so Page Size was inert from a control that reads as a bound. `page` is not
defaulted when neither is set — that would silently truncate a caller relying on
the documented return-everything behavior.
Also:
- Note in get_fired_alerts that `name=-` returns every saved search's fired
alerts and the endpoint documents "Request parameters: None", so there is no
count/offset to bound it.
- Fix the Splunk block's `messages` output blurb: `[{type, text}]` holds for the
search and job-control operations, but get_search_job returns an object.
- Generalize the Datadog block's numeric coercion (`datadogPageNumber` →
`datadogNumber`) over all 32 bare `Number()` mappings, so a typo or unresolved
reference is omitted rather than sent as `NaN`/`null`, and an explicit `0`
survives the old truthiness guard.
- Disclose create_event's documented 18-hour `date_happened` ceiling, and that
send_logs' `ddsource: "custom"` is a Sim default rather than a Datadog one.
* chore(integrations): regenerate tool metadata and docs
* fix(integrations): resolve confirmed findings from cold block audit
Cloudflare: clear purge_cache advanced targets across operations; send
action_parameters/ref/logging on rate-limit rule updates; migrate off the
deprecated batch zone-settings endpoint; correct MX/URI priority wording;
stop coercing blank numerics to 0.
CrowdStrike: seed includeHidden to match Falcon's documented default.
Microsoft Entra ID: resolve a UPN to an object ID for app role assignment;
wrap 21 array outputs in items.properties so nested paths resolve.
Okta: route assign_user_role's notification flag through isOktaFlagEnabled.
ServiceNow: drop the triage skill's claim of a default limit that does not exist.
Splunk: always assign coerced numerics so raw values cannot leak through the
executor's raw-input merge.
* fix(editor,credential-group): mask secrets outside short-input and stop a per-option abort from failing a shared query
config.password only reached the short-input renderer, so eight credential
fields rendered in plaintext: private keys on ssh/sftp/pi/kalshi, the
Secrets Manager payload, the STS web-identity and SAML assertions, and the
Browser Use variables table. long-input, code, and table now honor the flag.
Code fields mask through the highlighter because react-simple-code-editor
paints its textarea transparent; the table masks every column but the first
so key/value rows stay distinguishable. A registry-walking audit test fails
both on a password flag sitting on a type that cannot honor it and on any of
the eight fields losing its flag.
credential-group threaded a per-option AbortSignal into the fetch registered
under the workspace-wide credential group list key, so closing one option
panel rejected every co-observer with an AbortError that is not a React
Query cancellation. The shared fetch now runs on its own lifecycle signal.
* fix(mssql,editor): measure the response cap in UTF-8 and stop search from unmasking secrets
capRecordset sized rows with JSON.stringify(row).length, which counts UTF-16
code units while the emitted body carries raw UTF-8. CJK is the worst case at
3 bytes per unit, so a recordset admitted as 10 MB serialized to 28 MB. Rows
are now measured with Buffer.byteLength, serialized once each, with array
punctuation charged exactly and a reserve held back for the response envelope.
Workflow search revealed masked credentials without the user touching the
field: the search panel keeps focus in its own input and only scrolls the
match into view, so typing a guess painted a private key on screen. The index
is built client-side from values already in page memory, so this was never a
privilege boundary, but masking exists to prevent incidental display and a
screenshare-visible reveal defeats it. Focus is now the only reveal, applied
through one shared policy across all four renderers.
* test(editor): drop PEM-shaped fixtures from the masking tests
The masking fixtures carried a literal OPENSSH private key header, which
GitGuardian flags as a committed secret even though the body was only the
base64 of "openssh-key-v1". The fixtures now use an obvious marker string,
and the assertions derive their match text and dot counts from the fixture
instead of restating its bytes.
* refactor(editor): drop the dead isSearchHighlighted prop
No renderer consumed it. The editor computed it at two call sites and
sub-block passed a hardcoded false into renderLabel's slot for it, so even
the one function that declared a parameter never saw the real value. Its
only live effect was in the memo comparator, where an unconsumed value
changing forced a re-render for nothing.
The name stays in the masking audit's forbidden-inputs list, which guards
against a search signal being wired back into a masking decision.
|
||
|
|
fd828f80d0 |
fix(knowledge): stop capping the unpaged knowledge-base list (#6771)
#6770 routed GET /api/knowledge through the workspace read, which carried a 10,000-row cap. Staging crossed it, and the first list request after the deploy returned 500 with "Knowledge base list exceeds the 10000 row limit" — against data that had served fine for days. The cap could never have worked. Its throw was guarded by `limit === undefined`, so it fired only for callers that had NOT asked for a page: exactly the callers with no cursor to retry with and no way to ask for less. A paged caller never reached it. It also read one row PAST the cap before throwing, so it refused to serve rows it had already materialized — most of the memory was already spent. Soft-delete cleanup reclaims archived rows only past a retention window, and not at all where none is configured, so a workspace that archives faster than that window crosses any fixed count on its own. Remove it. An unpaged read is unbounded, matching the sibling internal lists (`listTables`, workspace files), and paged callers keep their page. That fixes the same latent 500 in the archived list, the catalog read, and the VFS name lookup, which are all unpaged too, rather than only the surface that failed. Two more of the same shape found while auditing for others: - `attachConnectorTypes` threw a bare Error above its own cap, on those same unpaged callers. Archiving a knowledge base archives its connectors, so the growth curve that broke staging could not reach it — but it is the identical construct, and the sibling `latestJobsForTables` has no equivalent. - The VFS path lookup read every knowledge base whose name merely CONTAINED the term and then exact-matched in JS, so a single-row lookup scaled with the workspace. It now queries the exact name and reads two rows, mirroring `findActiveTablesByExactName`. |
||
|
|
0844d4166b |
feat(jotform): add Jotform integration (#6772)
* feat(jotform): add Jotform integration
Adds 43 tools covering forms, questions, submissions, reports, webhooks,
labels, and account operations, plus the block, icon, and generated docs.
Request shapes are pinned against the API's own curl samples and the
official SDKs: PUT /form/{id}/properties and PUT /form/{id}/questions each
take a named envelope while PUT /form and the bulk-submission PUT take
their payload bare, and submission answers accept both the nested object
and the documented {qid}_{subfield} shorthand.
Skips the deprecated folder endpoints in favor of labels, and leaves out
endpoints whose response shape the docs do not publish.
* fix(jotform): harden the error envelope against quoted codes and non-JSON bodies
Jotform quotes `responseCode` on some endpoints and not others, so a
typeof-number test skipped the check on the quoted ones and turned an auth
failure into a successful tool result with empty output. Also caps the raw
body fallback, since an upstream gateway can answer with an HTML page
instead of the documented envelope.
* fix(jotform): stop duplicate question labels overwriting derived answers
Question labels are not unique — a form can carry two questions both
labelled "Email" — so keying the derived `values` map on the label alone
dropped all but the last and handed downstream workflows a confidently
wrong answer.
Every occurrence of a repeated label is now suffixed with its question ID,
rather than only the later ones, so the result does not depend on answer
order and a newly duplicated label reads as absent instead of as an
arbitrary winner. The id-keyed `answers` record was already complete and
is unchanged.
* fix(jotform): make the label-keyed answer map collision-proof
Question labels are free text, so the disambiguation key added in
|
||
|
|
11fe8481d0 |
fix(knowledge): list knowledge bases on the same authority that creates them (#6770)
* fix(knowledge): list a workspace's bases on the same authority that creates them GET /api/knowledge authorizes the session against the canonical workspace and then re-derives access inside the row query from a `permissions` join. Those two no longer agree: workspace `admin` can come from an organization role alone, with no workspace permission row behind it. Such a caller passes authorization, creates a knowledge base, and then sees an empty list forever — the row is filtered out by the join. Tables and files carry no equivalent join, which is why only knowledge is affected. Read the workspace's own rows through `getWorkspaceKnowledgeBases` once the operation is authorized. The caller-scoped query stays only on the path with no workspace to authorize against. * refactor(knowledge): stop re-deriving workspace access inside the list query The module resolves workspace authority one way everywhere — an explicit permission row OR an organization admin role — except in the listing query, which joined `permissions` and required a row. Both list surfaces authorized the caller and then contradicted that authorization: an org admin could create a knowledge base through /api/knowledge or /api/v1/knowledge and never see it listed. Tables and files carry no such join. Replace the caller-scoped query with `getLegacyPersonalKnowledgeBases`, which answers only for workspace-less bases whose creator IS their only authority, and have both surfaces read the workspace's own rows through `getWorkspaceKnowledgeBases` after authorizing. The legacy rows keep riding along so they stay reachable. The permissions join now appears nowhere in the module, and the duplicated connector projection collapses onto the shared helper that enforces the row cap. * refactor(knowledge): clean up the module's client layer and fix two state bugs Cleanup pass over the knowledge module — effects, state, memo, callback, React Query, url-state, emcn, and comments — keeping the fixes that change behavior for the better and leaving the ones that would change how the UI feels. Bugs found and fixed: - Opening a document flashed "Document not ready" for a frame. The chunk-row builder rendered the loading state as a status claim: with no document loaded yet it fell through to the branch that reports a missing processing status. - A partial upload failure skipped every cache invalidation, because the throw jumped past them, so the list stayed missing rows the server had already created. Admission failures create nothing and still skip the refetch. - The document and chunk context menus captured the row they opened on, so the Enable/Disable label went stale under the list's own polling. They hold an id and resolve against live data now. - The action bar's "Select all"/"Clear" links were painted with `--brand-primary`, which is defined nowhere: the links fell back to `currentColor` and were indistinguishable from the text beside them. Consistency and weight: - Mutations no longer invalidate `detail` non-exactly for writes that touch one document: that key is the parent of every documents page, chunk page, tag definition, and connector row cached for the base. - Dead hook surface removed (five exports with no consumer, a query instantiated only to reach a cache helper, a `goToPage` that only range-checked), unused parameters dropped, `getErrorMessage` replacing hand-rolled instanceof checks. - `page` joins the document list's param group, so a search resets pagination in the same debounced write instead of writing the URL on every keystroke. - Icons import from `@sim/emcn/icons`, the action bar composes `chipFilledFillTokens` instead of restating it three times, chunk cells use the canonical content-label chrome, and the icon-only buttons have accessible names. * refactor(knowledge): one row reader, one visible-list composition Follow-up from the quality pass. The two list queries had grown into near-copies of each other — same 14-column projection, same document join, same cap check, same row mapping — and the workspace-plus-legacy composition was pasted into both the internal use case and the v1 route, one of which is a surface adapter that should not be composing domain reads at all. Both queries now read through one private projection, so a column added to one list cannot go missing from the other half of the same rendered list, and `listWorkspaceAndLegacyKnowledgeBases` owns the composition both surfaces call. That merge also projects connector types once over the merged set instead of once per source, and skips the copy-and-sort entirely when there are no legacy rows — the common case. Also from the review: the chunk-row memo depends on the two primitives it reads rather than the whole polled document object, the selected chunk resolves in one scan instead of two, an aborted chunk-search pagination throws instead of caching a truncated result as complete, upload cache reconciliation no longer delays the rejected promise, the key-hierarchy rule is stated once on the key factory rather than six times at its call sites, and `TagDefinition` has one declaration. * fix(knowledge): refresh the document list pages after a document write Review caught a regression in the invalidation narrowing: `documents` (the list pages) and `document` (one row) are SIBLINGS under `detail`, not parent and child, so scoping a write to the row key left every list rendering the filename, status, tags, `tokenCount`, and `chunkCount` it had just changed. The key factory now exposes a `documentLists` prefix and all six document-scoped mutations invalidate it alongside the row — the chunk mutations included, since every chunk write moves the parent document's `tokenCount`. `detail` stays `exact: true` where only the base's own totals move. Also repoints the shared list-convention test at `getWorkspaceKnowledgeBases`; it exercised the caller-scoped query this branch removed. |
||
|
|
ad83796b69 |
fix(forks): stop sync demanding config the source never had (#6766)
* fix(forks): stop sync demanding config the source never had
Fork sync manufactured required re-pick rows the source never asked for, disabling
Sync and suppressing the "Fully mapped" badge. Two distinct causes landed on the
same line in `collectForkDependentReconfigs`.
Cause A - a block-level `required` applied to a nested tool param. The collector
runs one helper over both a block's own subblocks and the params of each tool in a
`tool-input`, reading the raw block config for the nested pass. A Jira Get Issue
tool inside an Agent block therefore inherited Jira's `issueKey` required-condition
even though the tool param is `visibility: 'user-or-llm'` - the agent fills it at
runtime, and `createLLMToolSchema` keeps an empty one in the model's schema
precisely so it can. The tool-row editor already strips `required` for anything not
`user-only`; the fork collector was the only surface that did not.
Cause B - demanding a value the source never had. `projectId` hangs off the
credential anchor, so a Jira block on `write` with a blank project emitted a
required row. The proof it is a collector bug: only members carrying a
`selectorKey` are emitted, so the basic selector gated while its advanced twin
(identical `required`, no `selectorKey`) did not - the same empty config blocked or
not purely on a display preference. That is also why toggling to manual mode
"fixed" it.
Neither fix subsumes the other: an emptiness guard alone leaves a *populated*
`user-or-llm` param gating after a parent swap (`effectiveDependentValue` blanks it
when the parent changed), and the visibility rule alone never touches top-level
subblocks.
Both invariants now meet in one predicate, and the tool-row rule is extracted so
the editor and the sync gate cannot drift. Rows are still emitted either way - only
the gating flag changes - so no stored dependent value is orphaned.
Also fixes the placeholder stutter in the re-pick selector ("Select select issue"),
which composed a verb onto titles that already read as instructions.
Verified end-to-end against a forked workspace: before, 3 of 7 rows were required
(including one with a populated source value); after, only a top-level populated
required field gates, Sync enables, and the badge reads "Fully mapped".
* fix(forks): ask emptiness of the raw dependent value, not the coerced one
Pre-landing review caught a silent un-gating in the new predicate: it asked
`isNonEmptyValue` about `rawSourceValue`, which flattens every non-string to `''`
for the wire contract. A multi-select dependent selector stores an array, so a
populated one reported blank and stopped gating the sync. `isNonEmptyValue`
handles arrays and non-strings deliberately - give it the raw value.
Reachable today via zoho-desk `departmentIds`, the one multi-select dependent
selector with a `selectorKey` + `dependsOn`.
Also from review:
- the canonical id is an alias, so it no longer clobbers a param that owns that
key as its own `paramId` (first write wins)
- drop a redundant conjunct that implied a third state the code cannot reach
- document the present-but-undefined visibility case, which the resolver's
`buildToolInputSearchConfig` branch produces routinely
- extract the placeholder-noun transform so a bare "Select" title falls back to
the whole title instead of rendering "Select " / "No found", and test it
Tests: non-string and empty-array source values, both verified to fail without
the fix; the basic/advanced parity case now pins both shapes rather than calling
`.every()` on an empty array; the indexer mock is reset per test so the new cases
are not order-dependent.
* fix(forks): make the post-sync collector honor tool param visibility too
Greptile caught a real asymmetry, and corrected a wrong assumption in the first
commit: `needsConfiguration` is NOT warning-only. `promote.ts:1035` skips the
target's redeploy for any workflow in that list, and `:792` also withholds its
chat-deployment carry-over.
So with only the pre-sync collector fixed, an Agent block whose Jira `issueKey`
was populated in the target and cleared by a credential remap would let the sync
through (the modal correctly treats a model-supplied param as non-blocking) and
then silently decline to redeploy that workflow - leaving the fork running its
previous deployed version with no gate and no error.
Both collectors now resolve `required` through one shared
`resolveToolParamRequired`, so the pre-sync gate and the promote path cannot
disagree about what a nested tool param means. The lookup (sub-block id, then
canonical param id, then fail closed to the block-level rule) lives in one place
instead of being duplicated.
The `@/tools/params` mock in remap-references.test.ts is now overridable so a
test can opt into an authoritative resolution; its defaults are unchanged and
the other 74 tests pass untouched. The new case is verified to fail without the
fix.
* style(forks): use TSDoc for the new test declaration comments
CLAUDE.md requires TSDoc for documentation and no non-TSDoc comments. The
vi.mock boundary note, the resolve helper, and the beforeEach mock-reset
rationale all document declarations, so doc tooling could not associate them.
In-body step comments are left as-is - they explain a flow, not a declaration.
|
||
|
|
aeb5624cc8 |
fix(integrations): close regressions found in the final validation sweep (#6764)
* fix(integrations): close regressions found in the final validation sweep
An independent read-only audit of the eight integrations merged to staging
today found defects in every one, most of them side effects of the surgery
those PRs performed on already-shipped code.
Data loss and destructive paths:
- cloudflare: restore the shipped subBlock ids on read filters so existing
workflows keep their DNS/zone/purge filters. Losing them made
list_dns_records return the entire zone with success: true, which a
downstream delete fan-out would then target. The colliding write controls
are renamed instead, chosen by blast radius.
- cloudflare: refuse an update_ruleset_rule that would tear down the rule it
edits. PATCH is a replace, so an omitted action_parameters unbound the WAF
managed ruleset and every override under it.
- cloudflare: split the hidden `enabled` control so a value set while drafting
can no longer disable a live WAF or rate-limiting rule.
- cloudflare: stop `name` leaking into update_dns_record and renaming a live record.
- okta: stop a blank name overwriting a stored group name via the LLM path.
The block guard covered only the UI.
Broken on the default path:
- microsoft_ad: update_user sent accountEnabled: "" on its own default, so
every call left at "No Change" failed. Same tri-state defect already fixed
for forceChangePasswordNextSignInWithMfa; `visibility` fixed alongside it.
- cloudflare: `domain` is required for self_hosted (the default app type),
ssh, vnc and rdp; add saas_app/target_criteria and drop dash_sso, which has
no request variant.
Silent wrong results:
- datadog: list_monitors inherited Create Monitor's tag filter and returned a
filtered list as if complete.
- servicenow: `fields` carried both a JSON body and a projection on the three
legacy generic operations. The regression test for this fed already-JSON and
could not fail; it now feeds a real projection.
- splunk: cancel_search_job reported failure on success by parsing an XML body
as JSON; readSplunkJson now tolerates it.
- okta: sendEmail === true dropped a string 'true', silently skipping the
deactivation email.
Security:
- mssql: add writetext/updatetext/readtext to the statement screen. \bupdate\b
cannot match UPDATETEXT, so both were reachable through the read-only path.
- crowdstrike: chunk repeated-query ids. At the published caps a single request
built a ~68 KB query string, past typical proxy limits.
Also: splunk count=0 unbounded read, splunk pagination totals, the `nobody`
placeholder that reintroduced the namespace bug by copy-paste, okta cursor and
activate controls split per operation, servicenow sysparm_having syntax and two
required controls no longer pre-seeded with consequential values, datadog block
outputs reconciled with tool outputs, and 16 escaped apostrophes that corrupted
the published Entra docs.
One scope removed from microsoft_ad (User.Read.All). Directory.Read.All and
GroupMember.ReadWrite.All were proposed for removal and verified still required;
a test now asserts they stay.
* fix(integrations): surface corrupt Splunk bodies and partial CrowdStrike deletes
Narrows readSplunkJson's non-JSON tolerance to XML. The dispatching and
job-control endpoints answer in XML, but a body that is neither empty nor XML
was meant to be JSON, so swallowing its parse failure handed get_search_results
an empty envelope and reported a lost result set as a search with zero events.
Annotates a batched CrowdStrike delete that fails partway with the IDs its
earlier batches already removed. Falcon cannot roll those back, so a bare
failure left the caller unable to tell what was gone and a blind retry
re-targeted IDs that no longer existed.
Registers the Cloudflare subblock-ID migration the registry-stability check
requires. The suffixed read-filter IDs never shipped in a release and every
block already materializes the restored IDs, so they are dropped rather than
renamed onto values the collision guard would discard anyway.
* fix(integrations): close the three defects Bugbot found in the sweep
An execute rule sent an explicit empty action_parameters object past the new
guard, because presence was checked rather than emptiness. `{}` is the same
payload Cloudflare's schema default produces, so it unbound the managed ruleset
the guard exists to protect.
Datadog's new List Monitors pagination used a bare `Number()`, so a typo or an
unresolved reference in either advanced field reached Datadog as a literal NaN
— the pattern this same sweep fixed for Entra `top` and the Splunk numerics.
Okta's block still marked the group name required on update, blocking a
description-only update that the tool, its merge helper, and the API all accept.
* fix(integrations): confine the Splunk XML tolerance and the CrowdStrike commit list
Splitting the XML tolerance out of readSplunkJson into readSplunkDispatchJson
puts it only on the three dispatching and job-control tools that need it. The
results path can no longer read any non-JSON body as an empty envelope, so a 2xx
HTML interstitial surfaces instead of reporting a search that matched nothing.
The dispatch reader anchors on the one documented `<response>` root, so an
interstitial fails there too.
A batched delete now records the IDs Falcon echoed in `resources` rather than
the IDs that were requested. A batch can answer 200 while reporting per-ID
failures, and naming those as deleted told the caller to drop still-live
indicators from the retry.
* test(crowdstrike): pin batched partial-delete parity with an unbatched request
A 2xx envelope carrying per-ID errors is a partial success, not a failure —
failedWithoutResources fails the operation only when nothing came back at all.
The batched path already reports it exactly as a single request does, with
deletedIds naming what Falcon confirmed and errors naming what it refused. Pin
that so the contract is not mistaken for a swallowed failure.
* fix(splunk): read the dispatch XML envelope instead of discarding it
A dispatch answering in the documented XML form was replaced with an empty
object, so create_search_job and dispatch_saved_search threw a missing-sid error
after the remote job had already been created — stranding a job the caller could
no longer poll or cancel. The envelope is now projected onto the same `{ sid }`
shape output_mode=json produces, so the search ID survives.
Matching only the opening tag also accepted a body cut off mid-transfer, which
on a cancellation reported a truncated response as a successful cancel. The
pattern now spans the closing tag, so a truncated envelope falls through to
JSON.parse and throws.
|
||
|
|
025ea4d2bd |
fix(docs): serve JSON-LD in the HTML, fix sidebar spacing, and tighten the CLI guides (#6763)
* docs(cli): use -g for the install, and cut the prose that was not pulling weight
`--global` is valid but `-g` is what every comparable CLI documents, and the
long form only came from the package README. Also drops the yarn tab: it read
`yarn global add sim`, which works on Yarn 1 only — Yarn 2 removed global
installs, so that command fails for anyone on a modern Yarn. Adds `npx sim` for
running without installing.
The guides had accumulated design rationale that belongs in code comments rather
than user docs — why the filter grammar is JSON, why the config section naming
is asymmetric, why an unexpected error keeps its stack trace. Surveying how gh,
Vercel, Turborepo, Deno, Bun and Supabase write theirs, none carry that kind of
justification, and callouts are reserved for content whose absence produces a
wrong result rather than for general asides.
So: 1016 lines to 763, and 12 callouts to 3. The three that remain are the
pairing-code check, that `sim logout` does not revoke the key, and the
`--limit 100` default on `batch-delete`/`batch-update`, which silently truncates
a larger match. Troubleshooting drops the entries whose error message already
contained its own fix and keeps the seven whose cause is not obvious.
* fix(docs): render JSON-LD as native script tags so it reaches the HTML
All four structured-data blocks — WebSite, TechArticle, BreadcrumbList,
SoftwareApplication — were rendered with `next/script`, which never emitted a
script tag. Measured on a production build, `/api-reference/getting-started`
contained zero `<script type="application/ld+json">` elements; the payload
existed only in the `__next_s` client-injection queue and the RSC flight data,
so anything reading the served HTML saw no structured data at all. React was
also logging "Encountered a script tag while rendering React component" on every
page.
`next/script` is for loading and executing JavaScript. JSON-LD is data, and
Next's own guidance is a native `<script>` in the component. `serializeJsonLd`
already escapes the `<` character to its unicode form, which is the
sanitization that guidance calls for, so only the element changes.
Same build, after: three valid tags per page with `WebSite` in `<head>`, and the
injection queue gone entirely.
* fix(docs): scope the flush-separator rule to a container's first separator
`[data-separator]:not([data-separator] ~ [data-separator])` was meant to keep the
first sidebar group flush against the top padding, but `~` only reaches siblings,
so it also matched the first separator inside every expanded folder. Under
Self-Hosting, "Install" lost its top margin and crowded the "Architecture" link
above it — 25px of gap where "Configure" and "Operate" below it had 40px.
`:first-child` expresses the intent directly. Only the four sidebar roots open
with a separator; every nested folder starts with a page, so the intended case
still goes flush and nothing else changes.
* fix(docs): move the flush-separator rule onto the separator component
Keeps the styling with the component that owns it, per the repo standard, and
lets the global rule be deleted outright rather than corrected — `global.css`
now only loses a rule in this PR. Tailwind's `first:` variant compiles to the
same `:first-child` selector, so behavior is unchanged: the build emits
`.first\:mt-0:first-child{margin-top:0}` and the prerendered HTML carries the
class on the separator.
|
||
|
|
257029a60c |
feat(microsoft_ad): licensing, security, audit, role, and device operations (#6742)
* feat(microsoft_ad): licensing, security, audit, role, and device operations
Deepens the Microsoft Entra ID block from 12 to 36 tools against the Microsoft
Graph v1.0 reference: license assignment and tenant SKUs, password set/reset,
sign-in session revocation, authentication methods, sign-in and directory audit
logs, app role and directory role assignments, service principals, device reads,
and conditional access policy reads.
Device write (device-update, device-delete) is deliberately excluded. Both
document Directory.AccessAsUser.All as their only delegated scope, with the
higher-privileged read documented as unavailable, so supporting them would mean
requesting tenant-wide act-as-the-user directory access for two operations that
additionally require the caller to hold Intune Administrator.
Also drops an undocumented ?$select= from create_user that was silently nulling
department and accountEnabled in the response.
* fix(microsoft_ad): resolve OData filter and search by owning operation
The params mapper assigned result.filter from each filter subBlock in turn, so
the last non-empty one won regardless of the selected operation. Because a
subBlock keeps its value after the operation changes, a filter written for one
endpoint was sent to every other collection operation — invalid OData against a
different Graph resource, or a silently wrong page.
Resolves the filter and search terms from an explicit operation-to-field map
instead, so each operation reads only the field it owns.
* fix(microsoft_ad): clear non-owning filter and search on the merged inputs
The executor merges { ...inputs, ...transformedParams }, so declining to copy a
stale filter is not enough — the serialized value survives the merge and still
reaches the tool. Advanced-mode subBlocks are serialized on non-emptiness alone
and never have their condition evaluated, so the value is present even when the
field is hidden.
Write filter and search on every operation, as undefined when the operation owns
neither, so the merge clears them.
* fix(microsoft_ad): clear the MFA flag and let paged user operations continue without a User ID
The set_password MFA dropdown only wrote its key when non-empty, so the "No Change"
empty string survived `{ ...inputs, ...transformedParams }` and reached Graph in place
of a boolean. Assign it explicitly, including as `undefined`, the same way `filter` and
`search` are handled.
`list_user_app_role_assignments` and `list_user_devices` page by `@odata.nextLink`, and
both tools already treat `userId` as optional once a continuation URL is supplied. Drop
them from the required set when Next Page is filled in so pagination-only runs pass block
validation.
Also note on the reset_password output that a generated password reaches workflow outputs,
run history, and the model, matching how other tools that return secrets document exposure.
* fix(microsoft_ad): require the service principal ID only on the first page
Every other single-resource ID field pairs its condition with a matching required
rule; servicePrincipalId had none, so a first-page run could pass block validation
with an empty ID and fail inside the tool instead. Require it unless a continuation
URL is supplied, matching the paged per-user operations.
* fix(microsoft_ad): reject a continuation URL from a different collection
Every paged operation reads the one shared Next Page field, and a subBlock keeps
its value after the operation changes. Paging /users and then switching the block
to /devices short-circuited back to the user page, silently returning the previous
collection instead of the selected one.
Assert the continuation URL's terminal path segment against the collection the tool
actually reads, which also rejects a nextLink pasted from an unrelated response.
|
||
|
|
fed891f69d |
docs(cli): add a CLI docs section generated from the command tree (#6762)
* docs(cli): add a CLI section, generated from the command tree
The `sim` CLI shipped with no coverage in the docs site. Adds a fourth
top-level tab for it, and moves Academy last.
The command reference is generated. `sim` exposes 147 leaf commands across
33 groups, most of them derived at runtime from the v2 route contracts, so a
hand-written reference would be wrong the week after it was written. The
generator walks the command tree `buildProgram()` hands to commander — the
same tree the terminal parses — rather than re-deriving it from the contract,
which would be a second implementation free to describe commands nobody can
invoke. `check:cli-docs` is a zero-arg `check:*` script, so the existing audit
runner picks it up and stale pages fail CI.
Generating against the real tree surfaced a collision it had been hiding:
`bulkUpdateKnowledgeDocuments` and `updateKnowledgeDocument` both derived to
`sim knowledge documents update`. Commander resolves a duplicate to the first
match, so the bulk form shadowed the single-document one and its flags were
unreachable while still appearing in `--help`. The bulk form is now
`batch-update`, matching how `tables rows batch-delete`/`batch-update` already
handle the same REST overload, and the generator fails on any duplicate path
so the next one cannot land silently.
Five hand-written guides cover install, auth, configuration, output formats,
and scripting. Also corrects two commands in the package README that do not
exist as documented (`tables columns <tableId>`, and `--sort score:desc`,
which is JSON).
* docs(cli): document every flag from the contracts, add troubleshooting and a single-page reference
The command reference was structurally complete but said almost nothing: 223 of
377 flags rendered as "Set sort by" because the CLI only ever read flag help
from its own contract overrides, and fell back to restating the flag name.
The prose already existed. The v2 route contracts carry 931 `.describe()` calls
and the OpenAPI specs publish all of them — 327 parameters and 282 body
properties, 100% coverage — but the generated operation table dropped every one,
carrying only a per-operation summary. It now carries the field descriptions,
the path-parameter descriptions, and positional help, so `--help` and the docs
explain a flag the same way the API reference does. Placeholder descriptions are
now zero, and 147/147 commands, 377/377 flags and 130/130 arguments are
documented.
`check:cli-docs` fails on a request field with no `.describe()` rather than
letting it render as documentation that says nothing.
Also in this pass:
- Commands are root-level sidebar entries under a Commands heading rather than
a folder, and headings are the command's description, so the table of
contents distinguishes entries at the first word instead of repeating
"sim knowledge documents …" fourteen times. A guard fails the build if two
descriptions on a page collide, since they would share an anchor.
- A single-page `Complete reference` carrying all 147 commands, for in-page
search and for agents fetching `/cli/reference.mdx`. It keys on exact command
paths because descriptions are only unique within a group.
- A troubleshooting page, with every message copied from the source.
- Table columns are sized by a local component; the flag column was starved
while descriptions kept most of the row empty.
- The prerelease install channels are dropped from the docs and the package
README, which is what npm renders.
* fix(docs): match the CLI tab by path segment, and escape backslashes before pipes
`pathname.includes('/cli')` also matches `/integrations/clickup` and
`/integrations/clickhouse`, so both existing integration pages lit the CLI tab
and unlit Documentation. Matching is now per path segment. Anchoring to the
start would not work either — a non-default locale prefixes the path, as in
`/ja/cli` — so the segment is matched wherever it sits.
Table cells now double a backslash before escaping pipes. A value ending in one
turned `a\` + `|` into `a\\|`, which the table parser reads as an escaped
backslash followed by an unescaped pipe, splitting the cell early. Nothing in
the command surface contains a backslash today, so this was latent rather than
visible.
The reference page's global options table is two-column and was being wrapped in
`CommandTable`, which sizes the second column for the `Required` cell of the
three-column tables and crushed the description into 5.5rem. It now matches the
overview page, which leaves that table unsized.
|
||
|
|
6a29a9e2f4 |
feat(mssql): add Microsoft SQL Server integration (#6739)
* feat(mssql): add Microsoft SQL Server integration
Add a Microsoft SQL Server block backed by six tools (query, execute,
insert, update, delete, introspect), mirroring the existing PostgreSQL
and MySQL integrations.
Connections go through the `mssql` (Tedious) driver: `connectionTimeout`
is top-level while `encrypt`, `trustServerCertificate`, and
`instanceName` live under `options`, and `port` is omitted when a named
instance is used. Values are bound as `@paramN` via `request.input()`;
no user value is interpolated into SQL. Identifiers are bracket-quoted
after validation and WHERE clauses run through the shared injection
guard.
Introspection reads INFORMATION_SCHEMA plus the `sys.indexes` catalog
views for tables, columns, primary keys, foreign keys, and indexes.
The icon is a placeholder database cylinder drawn with `currentColor`
until the real brand mark lands.
Requires `bun install` for the new `mssql` / `@types/mssql` deps.
* feat(mssql): use the SQL Server brand mark on a white tile
* fix(mssql): pin the validated IP and correct the introspection catalog reads
Tedious exposes `options.connector`, a hook that replaces its own
resolve-and-connect path, so the connection can be pinned to the address
`validateDatabaseHost` already approved instead of re-resolving the
hostname. `server` stays the hostname because tedious derives the TLS
`servername` from it independently of the connector, so SNI and
certificate validation survive the pin. This brings MSSQL in line with
the PostgreSQL and MySQL tools.
Named instances are dropped: tedious resolves them with a UDP SQL Server
Browser lookup issued outside the connector, and node-mssql deletes
`port` whenever `instanceName` is set, so no configuration leaves a
named instance pinned. A named instance is reachable through its static
TCP port.
Introspection fixes:
- index key columns now filter on `key_ordinal > 0`; INCLUDEd columns
and partitioning columns both report `0` and were being returned as
key columns, ordered ahead of the real ones
- foreign keys resolve through `sys.foreign_keys` /
`sys.foreign_key_columns` rather than
`INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS`, whose join to
`TABLE_CONSTRAINTS` has no row when a key references a unique index
and so dropped the key entirely
- `is_unique` is a `bit`, which tedious maps to a boolean, so it is
coerced rather than compared
- schemas come from `sys.schemas`, which needs only `public` and carries
no metadata-visibility caveat
The WHERE-clause guard also covers `WAITFOR TIME`, `OPENQUERY`,
`OPENXML`, the legacy `master..sys*` compatibility views, and extended
and OLE-automation procedures beyond `xp_cmdshell`.
Regenerates the docs and catalog artifacts the icon change left stale.
* chore(mssql): commit the lockfile entries for mssql and its tedious dependency tree
* fix(mssql): make the Query operation genuinely read-only
The block label, tool description, and docs all present Query as SELECT-only
while the route ran whatever T-SQL it was given, so an agent picking
mssql_query because "it is only a SELECT" could delete rows. Screen the
statement for mutating keywords with string literals stripped, which also
catches the WITH ... DELETE form that a leading-token check would miss.
Also switch the tool barrel to absolute imports per the repo convention.
* fix(mssql): compose the shared WHERE guard and close the semicolon-less batch gap
The local validateWhereClause re-derived an older copy of the shared patterns
and scanned raw text, so it missed a bare 1=1 and false-positived on prose in a
quoted value. Delegate to validateSqlWhereClause, which masks string literals
first, and keep only the SQL Server surfaces it has no reason to know about.
T-SQL needs no statement terminator, so every semicolon-anchored stacked-query
check reads straight past `id = 1 DROP TABLE dbo.users`. Screen for a bare
statement-introducing keyword to close that; word boundaries leave ordinary
column names like updated_at and deleted_at untouched.
Export maskSqlStringLiterals so the dialect layer masks the same way the shared
guard does rather than carrying a weaker single-quote-only copy.
* fix(mssql): screen administrative T-SQL and reject batches in the read-only path
The previous round left two keyword lists maintained separately, and both were
short: DBCC, KILL, CHECKPOINT, USE, and DENY were in neither, so
`SELECT 1; DBCC SHRINKDATABASE(...)` and `id = 1 DBCC SHRINKDATABASE(...)`
both got through. Collapse them into one MSSQL_STATEMENT_KEYWORDS shared by the
query and WHERE screens so a keyword cannot be covered in one place and missed
in the other, and add the administrative commands.
Also reject any second statement after a semicolon in the Query path outright.
That closes SELECT 1; <anything> structurally instead of by naming the anything,
so the blacklist no longer has to be exhaustive to hold.
* fix(mssql): reject SQL comments in the read-only query path
A block comment placed inside a keyword splits it as far as a lexical scan is
concerned, so keyword coverage cannot settle whether the server rejoins the
halves. Refuse comments in the Query path instead of modelling the tokenizer.
A SELECT sent through this operation has no need for one, and Execute Raw SQL
still accepts them. Masking leaves comment markers intact, so a literal
containing -- still passes.
* fix(mssql): close the masker-desync bypasses and correct the catalog reads
Every T-SQL screen runs over the shared literal masker, which was written for
the MySQL dialect. Three ways to desynchronise it let real SQL hide inside what
the masker believes is a string, two of which survived the existing even-quote
check:
- a backslash before a quote. T-SQL has no backslash escape, so the server
closes the literal where the masker swallowed the quote and runs the rest as
code. `a='x\' DELETE FROM dbo.t WHERE b='y'` holds four quotes and masks the
DELETE out of the keyword screen entirely, so the read-only Query operation
would run it.
- a double quote inside a bracketed identifier, which the bracket rule missed
because it only looked for single quotes.
- any unbalanced double quote or backtick, which the parity check did not cover.
All three now fail closed. Introspection also filters hypothetical and disabled
indexes, which were reported as if they were live, and resolves the referenced
side of a foreign key through sys.schemas so a cross-schema reference is no
longer an ambiguous bare table name. Values bound through request.input are
serialized when they are nested JSON, which the driver otherwise rejects with a
bare "Invalid string.".
* test(mssql): cover the block param merge and the operation-to-tool map
Asserts on the merged `{ ...inputs, ...buildParams(inputs) }` the generic tool
handler forwards rather than the mapper's return, since a key the mapper omits
keeps its raw subBlock value through that merge. Pins the TLS toggles to their
string form end to end — a switch subBlock would serialize `'false'`, which is
truthy, and the route contract would coerce the user's off into on — and checks
that duplicate subBlock ids agree on their seeded default.
* fix(mssql): release a pool whose connect failed, and allow a keyword with no trailing space
Only a pool handed back to the route reaches its `finally`, so a pool whose
connect rejected leaked its tarn resources — one per attempt when a bad
credential is retried. It now closes itself, and a failure to close cannot mask
the connect error the caller needs.
The read-only screen also anchored on `\s` after the opening keyword, which
refused valid reads like `SELECT*FROM dbo.users` and `SELECT(1)`. A word
boundary accepts those while still refusing `SELECTX`, and cannot loosen the
screen — the keyword and batch checks run over the whole statement regardless.
* chore(mssql): regenerate tool metadata and the integration catalog after rebase
Artifacts rebuilt with the generators rather than hand-merged, so they carry
both the mssql entries and the tools that landed on staging in parallel.
* fix(mssql): reject a parenthesised or negated constant tautology in a WHERE clause
The shared guard recognises `OR 1` but not `OR (1)`, `OR ((1))`, `OR NOT 0`, or
`OR NOT (FALSE)` — a parenthesis or a NOT between the operator and the constant
hides it. Both patterns require the constant to be the whole parenthesised term,
so a real disjunct such as `OR (1 = priority)` is untouched.
This narrows the gap rather than closing it, and is not meant to close it: an
always-true expression is not lexically decidable in general, which is why the
WHERE screen stays documented as defense-in-depth rather than a boundary.
* chore(mssql): regenerate tool metadata after rebase onto staging
Rebuilt with the generators so the artifacts carry the servicenow and
crowdstrike tools that landed on staging alongside the mssql entries.
* fix(mssql): screen trigger and state statements, and drop the space anchor on Execute
DISABLE and ENABLE were missing from the shared statement list, so
`SELECT 1 DISABLE TRIGGER dbo.audit ON dbo.users` passed the read-only screen as
a semicolon-less batch and turned auditing off. SET, BEGIN, COMMIT, and ROLLBACK
are added with them, since session and transaction state are reachable the same
way. FETCH is deliberately left out: OFFSET ... FETCH NEXT is the standard paging
clause, and screening it would reject the ordinary paged SELECT.
Execute Raw SQL anchored its allowlist on `\s`, which refused `EXEC(@sql)` —
the ordinary form of dynamic SQL, on the one operation meant to run it. It now
uses `\b`, matching the read-only screen.
|
||
|
|
7f936dc02a |
feat(tooling): enforce docs freshness and modernize agent skills (#6756)
* feat(docs): fail CI when generated integration docs are stale * fix(docs): don't flag delete-then-recreated trigger pages in check mode * docs(skills): require docs:check in the integration authoring skills * chore(skills): migrate agent commands to native skills * fix(skills): clean orphaned Claude projections |
||
|
|
6e5c337527 |
fix(sandbox): undefine the raw fetch host bridge before user code runs (#6761)
* fix(sandbox): undefine the raw fetch host bridge before user code runs * test(sandbox): scope the hardening assertions to each execution path * fix(sandbox): preserve the fetch global's property attributes |
||
|
|
8a44621382 |
feat(cloudflare): add WAF rulesets, rate limiting, Zero Trust Access, R2, Workers, and Tunnels (#6740)
* feat(cloudflare): add WAF rulesets, rate limiting, Zero Trust Access, R2, Workers, and Tunnels
Extends the Cloudflare integration past DNS/zones/cache with the security and
Zero Trust surface:
- Rulesets engine (zone-scoped): list rulesets, get a ruleset, read a phase
entry point, and create/update/delete rules. WAF managed-rule overrides are
surfaced through the http_request_firewall_managed entry point, since
Cloudflare has no dedicated overrides endpoint.
- Rate limiting (zone-scoped) via the current Rulesets-based http_ratelimit
phase, not the deprecated rate_limits endpoint.
- Cloudflare Access (account-scoped): applications, application policies,
groups, identity providers, and service tokens.
- R2 buckets, Workers scripts/routes, and cloudflared Tunnels.
Destructive operations (delete application, delete policy, revoke service
token, delete rule, delete bucket) spell out their blast radius, and every
tool branches on the envelope's success flag rather than the HTTP status.
Security events are intentionally omitted: Cloudflare exposes them only
through the GraphQL firewallEventsAdaptive dataset, whose field list is not
documented outside schema introspection.
* fix(cloudflare): correct docs drift and remove any from the tool layer
Validation pass over all 47 Cloudflare tools against developers.cloudflare.com.
- Two tool descriptions still escaped a quote as \'. That reaches the model
verbatim and truncates the generated MDX cell — the get_zone_settings
`value` output row was missing from the published docs entirely. Both are
now template literals, and the row is back.
- list_rulesets ignored pagination. The endpoint pages by cursor via
result_info.cursors.after (not page/per_page), so a zone with many rulesets
silently truncated with no way to page. Expose per_page + cursor and return
the next cursor.
- The managed-ruleset override description claimed action and enabled were
the overridable properties. They are the ones the Rulesets engine documents
at every level, but individual managed rulesets add more: an OWASP Core
Ruleset rule override also takes score_threshold. Corrected in both the
tool output description and the block's action-parameters wand prompt.
(sensitivity_level is a DDoS override, not a WAF one — deliberately absent.)
- list_tunnels/get_tunnel dropped the documented `metadata` field.
- list_r2_buckets appended order=name whenever any filter was set. `order`
only qualifies `direction`, and `name` is its sole documented value.
- Path-interpolated IDs are trimmed, so a pasted ID with trailing whitespace
no longer 404s.
- Replaced every `any` in the integration with checked types: a shared
CloudflareEnvelope plus per-resource raw payload interfaces, read through
readCloudflareResponse. The mappers in utils.ts were the widest hole —
typing them caught four real output-shape mismatches (identity provider
read_only, service token enabled, DNS record meta/priority, certificate
geo_restrictions) that `any` had been hiding.
- BlockMeta only described DNS and zone work. Added templates and skills for
the WAF, rate limiting, and Zero Trust Access surfaces the block now has.
Confirmed against the docs and left unchanged: rulesets/rate limiting are
zone-scoped and Access/R2/Workers scripts/Tunnels are account-scoped while
Workers routes are zone-scoped; tunnels live under /accounts/{id}/cfd_tunnel;
the ratelimit object is a sibling of action/expression, not nested in
action_parameters; every rate limiting period and mitigation_timeout option
matches the documented set; R2 delete returns an empty result so echoing the
requested bucket name is correct; app-nested Access policy endpoints are
current, not deprecated; and every tool fails on a 200 carrying success:false.
* fix(cloudflare): stop per-operation subblock defaults colliding on a shared id
Subblock initial values are seeded into block state keyed by subblock id —
both stores/workflows/utils.ts and lib/workflows/defaults.ts assign
`subBlocks[subBlock.id]` in a plain forEach — so two controls sharing an id
leave one stored value and the last definition in file order wins. Four ids
were duplicated with differing defaults:
- `type` was defined four times. The Access "Application Type" control is
last, so every new block seeded `type = 'self_hosted'` and the three DNS
record controls inherited it — Create DNS Record sent a Zero Trust
application type as its record type. The subblock added on this branch
broke a default on tools that shipped long before it.
- `status` was defined three times. The empty tunnel filter is last, so
List Certificates lost its `all` default.
- `proxied` was defined three times. An empty filter is last, so Create DNS
Record lost its explicit `false`.
- `action` was defined twice. The rate limiting dropdown is last, so the
ruleset-rule action input was seeded `block`, quietly making "block live
traffic" the default for a WAF custom rule the user never configured.
Give the colliding controls their own ids and map them back to the tool
params per operation, ahead of the coercions that read them, so each
operation keeps its own default. The other 17 duplicated ids agree on their
value and are left shared.
Adds tests covering each separated default plus a sweep asserting no id
carries two different seeded values, so a future duplicate goes red.
* fix(cloudflare): generate array include rules and allow bootstrapping a phase ruleset
The Access policy include wand asked for a JSON object while the tool parses
the field with parseJsonArrayParam, so generated rules failed validation.
Switch it to json-array, whose prompt reinforcement omits the object braces.
Rate limiting and WAF custom rules could only be appended to an existing
ruleset, but a zone that has never had a rule in a phase has no entry point
ruleset and returns 404, leaving no way to add the first rule. Add
cloudflare_create_ruleset for the documented POST /zones/{id}/rulesets
bootstrap, seeded with optional initial rules.
* fix(cloudflare): correct verified API defects and stop filters leaking into writes
Independent re-validation of all 48 tools against developers.cloudflare.com
turned up defects that the shipped tools would have hit on their happy path.
Delete DNS record reported every success as a failure. That endpoint is the
one Cloudflare v4 response with no envelope — its documented body is
`{"result":{"id":...}}` with no `success` — so `!data.success` was always
true. Branch on an explicit `=== false` instead.
The two replace-semantics PATCH endpoints could silently destroy live config.
Update rate limit rule defaulted a missing action to `block`, converting an
existing `log` or challenge rule into a hard block on real traffic; update
ruleset rule left action and expression optional and had no `ratelimit` or
`logging` passthrough, so updating a rate limiting rule stopped it rate
limiting. Both now require the fields the replacement needs, and the ruleset
rule carries the two nested objects through.
Access applications were unbuildable for most types: `domain` was required,
but it does not exist on the saas, app_launcher, warp, biso, dash_sso,
infrastructure, mcp, mcp_portal, or proxy_endpoint request variants. The
application type enum was also six values behind. Access group `is_default`
is an array of rule objects, not a boolean.
Purge cache merged every supplied target into one body, but the purge body is
a one-of over the five target kinds; it now names the conflict instead.
The remaining fixes are documentation drift: the priority field is MX and URI
only (an SRV record carries priority inside its content), the certificate
status filter documents only "all", the Worker tag filter takes tag:allowed
pairs, and the managed-rule override list conflated the DDoS-only
sensitivity_level with the WAF rule-level set.
Separately, controls that share a subBlock id share one stored value, and
`shouldSerializeSubBlock` short-circuits on `mode: 'advanced'` before it
evaluates `condition` — so a hidden list filter was reaching a write. A
`list_dns_records` content filter could overwrite a record's content, cache
tags could be written onto a DNS record, and the zone status enum could reach
the tunnel list, whose enum is disjoint. Filters that differ from the value
they collided with now carry their own id, remapped through one table before
any coercion. Sharings that mean the same thing everywhere are unchanged.
Aliases are cleared by explicit assignment rather than destructuring, because
the executor merges the mapper's output over the raw inputs and a merely
omitted key survives as its raw subBlock string. The tests assert on that
merged result, and three mechanical invariants now go red on a new collision:
no id spans a read filter and a written value, no dropdown id carries two
option sets, and no hidden advanced control feeds an operation that cannot
render it. That last one found the name filter reaching three list operations.
* docs(cloudflare): point self_hosted_domains at its replacement
Cloudflare deprecated the field in favour of destinations, which the tools
already surface. The output stays — Cloudflare still returns it — but the
description now says which one to read.
* refactor(cloudflare): drop a dead exception from the empty-type guard
create_dns_record now takes its record type from the recordType control,
whose dropdown has no empty option, so the operation can never reach this
guard with an empty type. Clear it unconditionally.
* fix(cloudflare): point the canvas sentences at the renamed filter controls
The list filters that were split off their write-side twin kept their old ids
in canvasPresentation, so seven clauses referenced a control that is no longer
visible for that operation — check:canvas-sentences catches exactly this, and
a broken clause fails silently on the card rather than throwing.
* fix(cloudflare): stop the rate limiting action defaulting on a replacing update
Making action required on update_rate_limit_rule was only half the fix: the
Action dropdown still seeded block for the update operation too, so an update
that edited only the threshold kept sending block and converted a live log or
challenge rule into a hard block — exactly the harm the required flag was
meant to prevent. The update now has its own control with no seeded value, so
the action is something the caller states rather than inherits.
The certificate status filter also still offered Active and Pending, which
Cloudflare does not document for that endpoint; the only documented value is
all, and omitting it returns active packs.
* fix(cloudflare): stop the Access replacements seeding a type and a decision
Same class as the rate limiting action: both Access updates are full
replacements, and the shared controls seeded self_hosted and allow for the
update operations too. Editing only a policy's include rules would silently
convert a live deny, bypass, or non_identity policy to allow — widening who
gets in — and editing an application would rewrite what it IS.
Each update now has its own required control with no seeded value, so the
type and the decision are stated rather than inherited. Regression tests
cover both, and the canvas sentence follows the renamed decision control.
|
||
|
|
cabd2e2fc1 |
feat(datadog): extend to 40 tools and align every operation with the published OpenAPI specs (#6745)
* feat(datadog): add incidents, SLOs, dashboards, synthetics, Cloud SIEM, and APM tools
Extends the Datadog block from 12 to 39 operations, all verified against
Datadog's published OpenAPI specs:
- Incidents (v2, public beta): list, get, create, update, add todo
- SLOs (v1): list, get, create, update, delete, history
- Dashboards (v1): list, get, create, delete
- Synthetics (v1): list tests, get test, latest results, trigger, pause/resume
- Cloud SIEM (v2): search signals, get signal, update triage state, assign,
list detection rules
- APM: search spans (v2), list Service Catalog definitions (v2)
Adds tools/datadog/utils.ts so every tool builds its URL from the configured
site/region and shares the JSON:API-aware error extraction, and handles the
v1 flat vs v2 envelope shapes and cursor pagination per endpoint.
* fix(datadog): align every operation with the published OpenAPI specs
Validated all 39 shipped operations (plus the 12 pre-existing ones that had
never been spec-checked) against the DataDog v1 and v2 OpenAPI schemas.
- `POST /api/v2/downtime` requires `monitor_identifier`, so a downtime created
without a monitor id was rejected. Default to the `*` monitor tag.
- A one-time downtime schedule declares `additionalProperties: false` and
accepts only `start`/`end`; the timezone moves to `display_timezone`.
- `GET /api/v2/downtime` has no `monitor_id` filter, and the response carries
no `disabled` attribute. Downtime ids are UUID strings, not numbers.
- Drop scaffold types for operations that do not exist (metric metadata, event
query, monitor update/delete/unmute, host listing) along with their fields.
- Note that monitor mute is no longer published in the v1 specification.
- Add browser Synthetic test results, which the browser-specific endpoint
returns with its own camelCase step-count shape.
- Replace every `any` with a spec-derived interface, keeping the polymorphic
service-definition schema opaque.
* fix(datadog): remove remaining any types and declare every returned output field
Replace the six surviving `Record<string, any>` request-body and response-cast
sites with concrete spec-derived shapes, and declare the output fields that
transformResponse already returned but outputs omitted:
- create_downtime / list_downtimes: timezone, created, modified
- create_monitor / get_monitor: options, creator
- list_monitors: message, priority, options, created, modified, creator
- query_logs: content.attributes, content.tags
- update_security_signal_state / _assignee: type; assignee also gained the
archiveReason/archiveComment pair its sibling already declared
- query_timeseries: series gained the items shape it never described
* fix(datadog): stop dropping downtime targeting inputs in the block mapping
create_downtime accepts monitorTags, timezone and muteFirstRecoveryNotification,
but the block exposed no inputs for them and never forwarded them. Monitor-tag
targeting silently fell back to the `*` tag, so a downtime meant for one team's
monitors muted every monitor in scope. Adds the three advanced sub-blocks and
wires them through.
Also routes list_downtimes' currentOnly through toSwitchBoolean. A switch yields
the strings 'true'/'false', and 'false' is truthy, so turning the toggle off
still sent current_only=true. Every other switch in the block already used the
helper; this was the last raw one.
* fix(datadog): correct metric type codes, stop SLO update data loss, drop unpublished mute
Independent re-validation of all 39 operations against the DataDog/datadog-api-client-go
generator specs (v1 and v2 openapi.yaml) rather than the client-rendered docs site.
Correctness:
- submit_metrics sent inverted MetricIntakeType codes (gauge as 0/unspecified, rate as 1/count,
count as 2/rate), silently changing how Datadog aggregated every submitted series. The spec
enum is 0 unspecified, 1 count, 2 rate, 3 gauge; an unrecognized type is now omitted so
Datadog infers it. Also stops stamping an invented `resources: [{name:'host'}]` default and
now forwards `interval`, which Datadog requires for count and rate metrics.
- update_slo replaced the whole SLO with only the fields the caller filled in, so editing one
field erased description, tags, query, monitor_ids, groups, thresholds, and timeframe.
PUT /api/v1/slo/{slo_id} is a full replacement, so the stored SLO is now read first and the
supplied edits are overlaid onto it, with the read-only fields stripped.
- update_incident admitted empty strings, so a blank input could blank a stored incident title
or fail as an invalid date-time.
- query_timeseries reported a failed query as success: Datadog returns 200 with a non-ok
`status` and the reason in `error`.
- create_monitor swallowed malformed options JSON and created a monitor with no thresholds.
- send_logs rebuilt each entry from a fixed field list, discarding the custom attributes
Datadog accepts as additionalProperties, and padded absent optional fields with empty strings.
Removed:
- mute_monitor. /api/v1/monitor/{monitor_id}/mute is absent from the v1 spec entirely, there is
no unmute counterpart to reverse it, and downtimes are the supported mechanism.
Contract accuracy:
- Security signal search advertised relative times ("now-1h"); the spec types filter.from/to as
format: date-time. Descriptions, placeholders, and wand prompts now produce ISO-8601.
- list_incidents advertised an `include` value ("integrations") that is not in the spec enum,
and neither incident tool trimmed the comma-separated list, so "users, attachments" 400d.
- Invalid "ok" group state dropped from both monitor descriptions.
- time_slice removed from SLO create input, which cannot build one without an SLI specification.
- DatadogSite gains ap2, uk1, and us2.ddog-gov.com.
Pagination and errors:
- list_downtimes silently truncated at Datadog's default 30 with no way to page; adds
page[limit]/page[offset] and surfaces totalCount.
- query_logs returned a cursor it had no way to accept back.
- Error extraction consolidated onto datadogErrorMessage, which now also reads the
dictionary-shaped errors of the SLO delete conflict. Ten tools were reading `.detail` off
plain strings or the raw entry off objects, degrading every failure to a bare status line.
- Debug logging removed from list_monitors.
Adds 29 regression tests, each verified to fail when its fix is reverted.
* fix(datadog): add SEV-0, document page-size caps, drop unsourced output defaults
- The severity dropdown omitted SEV-0, which IncidentSeverity allows and both incident
tool descriptions already advertised.
- Page-size descriptions now state Datadog's documented default of 10 and cap of 100
instead of an arbitrary example, so an agent does not request an out-of-range page.
- trigger_synthetics_tests emitted an explicit null for a string-typed optional output,
and update_synthetics_status reported 'live' on the error path regardless of what the
caller actually requested.
* fix(datadog): keep mute_monitor and add the missing unmute counterpart
Reverses the removal in the previous commit. Absence from the datadog-api-client-go
generator spec showed the endpoint is unpublished there, not that it is retired:
Datadog's official Python client still implements it on master as
`Monitor.mute(id, scope=, end=)` and `Monitor.unmute(id, scope=, all_scopes=)`
(datadogpy datadog/api/monitors.py), which `_trigger_class_action` resolves to
`POST /api/v1/monitor/{id}/mute` and `/unmute` with exactly those body fields.
mute_monitor has also been in the block since #2175 in December, so dropping it would
have broken existing workflows for an endpoint that two independent sources agree is live.
The genuine defect was that muting was a one-way trapdoor: Sim could mute a monitor but
had no way to reverse it. Adds datadog_unmute_monitor, sharing the monitor ID and scope
inputs with mute, so the operation is recoverable from the same block.
Also: mute no longer discards the response body (it now reports the monitor id, name, and
state), routes errors through datadogErrorMessage, encodes the monitor ID in the path, and
stops dropping an explicit `end` of 0.
* fix(datadog): make downtime targeting explicit and reach downtime pagination from the block
Addresses the review findings on the previous round.
- create_downtime accepted both a monitor ID and monitor tags but `monitor_identifier` is a
oneOf, so it silently kept the ID and dropped the tags, muting a different set of monitors
than the caller asked for. It now rejects the ambiguous combination.
- create_downtime ran Number.parseInt on the monitor ID with no validation, so a non-numeric
value became NaN and serialized as null inside monitor_identifier. It now uses the same
parseMonitorIds guard the SLO path already had, naming the offending value.
- list_downtimes gained limit/offset in the tool but the block exposed neither, so no
block-driven call could page past Datadog's default. Adds the two sub-blocks and wires them
through the params mapper.
- The block did not declare the totalCount the tool now returns, so nothing downstream could
bind to it.
* fix(datadog): tolerate non-string list inputs and keep the shipped mute subblock ids
Both defects were introduced by this branch.
- splitCommaList called .split on its argument, so routing create_downtime's monitorId
through it turned a legitimate numeric input into a TypeError before the request was
built. A <Block.output> reference to get_monitor or list_monitors resolves to a number,
and an LLM tool call can pass a number or an array, so the helper now normalizes all
three shapes. The previous Number.parseInt path had accepted a number by coercion.
- Adding the unmute operation renamed the mute subblock ids scope/end to muteScope/muteEnd.
Workflow state is persisted by subblock id, so every existing Mute Monitor block would
have kept the old keys and silently lost its scope and end time. Restored the shipped ids;
both are still unique block-wide and no operation reads another operation's value.
* fix(datadog): compare downtime targets after parsing, not before
A whitespace-only Monitor ID is truthy as a raw string but parses to no monitor, so
the oneOf conflict guard rejected a valid tag-targeted downtime whenever the untouched
Monitor ID field carried blank text. Both sides are now compared after parsing.
|
||
|
|
be20df9257 |
fix(forking): hide satisfied dependent configuration (#6723)
* fix(forking): hide satisfied dependent configuration * fix(forking): keep dependent chains configurable * fix(forking): invalidate stale dependent selectors |
||
|
|
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. |