mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-31 03:47:12 +08:00
002a3a3c8edea9d64c2157becc0b0bccd84405f7
6260
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
002a3a3c8e |
Merge remote-tracking branch 'origin/staging' into mship-fixes
# Conflicts: # apps/sim/app/workspace/[workspaceId]/integrations/components/connect-slack-bot-modal/connect-slack-bot-modal.tsx # apps/sim/lib/copilot/request/handlers/types.ts # apps/sim/lib/copilot/request/tools/files.ts # apps/sim/lib/copilot/resources/extraction.ts # apps/sim/lib/logs/log-views.ts # apps/sim/lib/table/application/views.ts # apps/sim/lib/table/views/service.test.ts # packages/logger/src/index.ts # scripts/check-api-validation-contracts.ts |
||
|
|
d1b9f1b12d | Harden browser panel and chat cleanup | ||
|
|
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. |
||
|
|
56128bec9b | Standardize tool environment references | ||
|
|
a9688d01b9 |
feat(library): Automation Anywhere Alternative: AI Agents vs. RPA for Real Reasoning (#6728)
Co-authored-by: Sim Pi Agent <pi@sim.ai> |
||
|
|
503b96faf5 | Harden VFS provenance and resource writes | ||
|
|
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 |
||
|
|
9c364ad9c6 | Harden chat stream transition handling | ||
|
|
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.
|
||
|
|
986829e776 |
Route in-band execution through the comprehensive tool dispatcher
The internal execute route used the bare server-tool router, which rejects VFS tools with 'Unknown server tool: read/glob/grep' — so nearly every background agent's first discovery call failed (102 in-band calls in one run, dozens rejected). It now uses the relay's executeTool dispatcher: registered handlers (VFS, function execute) with permission checks and param normalization, falling back to the app tool router — the same surface foreground execution gets. |
||
|
|
41923b8e95 | fix(settings): redirect unavailable tabs to general (#6710) | ||
|
|
d5701d5b2b |
fix(icons): align table block icon and 12-unit icon stroke with the emcn family (#6708)
The table/table_v2 blocks and the table trigger used a local lucide-shaped TableIcon (stroke 2.0, full-bleed 24 viewBox, 3x3 grid) while every other table surface used emcn's Table. Consolidate onto the emcn icon and drop the local copy. Nested tool-call rows in Chat applied no color class, so a non-brand block icon inherited body text instead of --text-icon. redo/undo/zoom-in/zoom-out draw 0.85 stroke on a 12-unit viewBox, rendering 0.992px at a 14px box against the family's 0.904px. 0.775 restores parity. |
||
|
|
9664e7ec17 |
Revert "fix(integrations): read every service mark from one registry (#6682)" (#6707)
This reverts commit
|
||
|
|
b7a5f22abd |
fix(integrations): read every service mark from one registry (#6682)
* fix(integrations): read every service mark from one registry A service looked like itself on the canvas and like nothing in particular everywhere it was connected. `OAUTH_PROVIDERS` registers 93 icons and no colour at all, so the surfaces built on it — the connect dialog above all — drew a flat grey mark for a block whose config already carries its brand icon and `bgColor`. Bridges the two: `resolveIntegrationBlockTypeForOAuth` maps any OAuth id (a service id, a provider id, an extra authorization server) to the catalog block behind it, so a credential surface holding only an OAuth identity can still reach the registry. The connect dialog now wears the block's tile, and `ChipModalHeader` takes a rendered mark so a tile can carry its own chrome instead of being tinted with the header's grey. Folds in the copies that had grown around the gap: `IntegrationTile` resolved its fill from the registry but took its icon from whatever the caller passed — one tile, two sources — and now defaults to the registry, with an override kept for the family service-account marks that genuinely are not the block's. The letter fallback it grew alongside was reading the catalog's `bgColor` while the tile beside it read the registry's; both are the tile now. Two `getProviderIcon` implementations for the same job (one tinted, one not) become one `ProviderIcon`, the connector tile duplicated verbatim across two knowledge-base surfaces becomes one `ConnectorTile`, and the permission rows that hardcoded `text-white` — which renders white-on-white on a pale brand tile — go through `BlockTile`. Public pages keep their generated catalog: importing the registry there would ship 282 block configs to a marketing page, and `integrations.json` is generated from the same `bgColor`, so the two cannot drift. * fix(blocks): drop the dead copies of a block's colour Sweeping the surfaces above turned up colour data nothing reads and colour data two surfaces disagreed on. Dead: `BLOCK_COLORS.DEFAULT/LOOP/PARALLEL` in the tag dropdown (only `VARIABLE` was ever referenced), `BlockIconInfo.color` on table columns — whose consumer documents that it deliberately ignores the colour, so the `#2F55FF` behind it could never render — and the `bgColor` threaded into the add-resource dropdown, whose row renders a bare tinted icon. Disagreeing: the Variables tile is `#2F8BFF` in the tag dropdown and `#8B5CF6` in the preview panel, for the same "V" on the same concept. Both now read `VARIABLE_TILE_COLOR`, and the preview panel's two hand-rolled squares become `BlockTile` like every other tile. Four spellings of the neutral fallback (`#6B7280`, `#6b7280`, `#666666`, and a `cancelled` status that happened to equal it) now point at `DEFAULT_BLOCK_TILE_COLOR`. The terminal and logs resolvers stay. They look like duplicates of `accent.ts` but carry behaviour it does not have — status fills for synthesized error/validation/cancelled rows, near-black contrast correction, MCP tool-id parsing, and a model-provider branch — so folding them in is a behavioural change, not a deletion. * chore(copilot): delete the mention machinery nothing calls The workflow panel's copilot tab renders `MothershipChat`, and that component brings its own input — so `panel/components/copilot` no longer holds a component at all, only the hook library the old input used. Five of those hooks have no caller anywhere: `useMentionData`, `useMentionKeyboard`, `useCaretViewport`, `useMentionInsertHandlers`, `useTextareaAutoResize`. They are not all of it. `home/components/user-input` still imports `useFileAttachments`, `useMentionMenu`, `useMentionTokens`, `useContextManagement`, and `useIntegrationAutoMention` from this directory, so it survives as a shared hook library rather than dead weight — which is why this removes the uncalled five rather than the folder. What they alone reached goes with them: `getFolderData` / `getFolderLoading` / `getFolderEnsureLoaded` and the `FOLDER_CONFIGS` table describing every mention folder, `buildMentionHighlightNodes`, the `MentionFolderNav` type, and the slash-command tables. Of the 266-line constants file only `SCROLL_TOLERANCE` had a live reader left. * fix(integrations): never answer with a sibling's tile Two integrations can share one OAuth id: Google Slides is authenticated by Drive's `google-drive` service and Jira Service Management by Jira's `jira`. Indexing first-write-wins made those ids resolve to whichever sorted first, so the dialog connecting Slides could wear Drive's brand. An id claimed by more than one block type now resolves to neither, and the caller keeps the service-specific mark it already had. A wrong brand is worse than no tile. |
||
|
|
5a88ce22d1 |
feat(ashby): incremental job sync, custom field writes, and application lifecycle ops (#6703)
* feat(tools): add incremental job sync and draft postings to Ashby reads
list_jobs accepts Ashby's syncToken and returns it as nextSyncCursor, so a
scheduled sync costs O(changed reqs) instead of rescanning every req. Ashby only
returns the token once the last page is drained, which the param description
states.
The output is named as a cursor deliberately. It is an opaque resumption marker,
not a credential, so it belongs with nextCursor - and a field literally named
syncToken matches the /^.*token$/i deny-list in redaction and renders as
[REDACTED], which makes an incremental sync unusable since the operator cannot
read the value the next run needs. The wire name stays syncToken.
list_job_postings gains includeUnpublishedJobPostings, plus the posting status
field - without status a caller cannot tell a returned draft from a published
posting, which makes the flag useless.
Also widens the custom field valueLabel type, which MultiValueSelect returns as
an array, for the write operations that follow.
* fix(tools): render Ashby object-shaped API errors readably
Ashby documents two error shapes and uses both. The `errors` array form carries
`{ message, parameter }` objects, which stringified to '[object Object]' and hid
the real cause - including the 403 a key gets when it lacks a module permission.
Also adds the shared pieces the new write operations need: one definition of the
custom field value shape for the read and write paths to agree on, and a
normalizer for Ashby's case-sensitive objectType enum so a model emitting
'candidate' fails here with the allowed values rather than at the API.
* feat(tools): add Ashby custom field writes, delete, source, and anonymize
customField.setValue/setValues are the only way to annotate a job or req, since
Ashby has no job notes and no job tags. Writing null clears a value, so the
annotation is reversible.
Because null clears, every one of these operations requires explicit intent
before it can destroy data. The block's required markers do not cover the agent
path - a model calls the tool directly, so tools.config.params never runs and
validateRequiredParametersAfterMerge skips a param marked not-required:
- set_custom_field_value rejects an absent or blank fieldValue; an explicit null
still clears
- change_application_source requires unsetSource to clear, and rejects a source
id and an unset request together, since preferring either one silently
discards the other. Ashby has no 'leave unchanged' mode, so setting and
clearing are the only two intents and exactly one must be expressed
- set_custom_field_values rejects an empty array locally rather than relying on
Ashby to reject it
application.delete needs candidatesDelete, a module permission separate from
candidatesWrite. candidate.anonymize strips PII but leaves the record; Ashby
exposes no candidate deletion endpoint.
* test(tools): cover the new Ashby request and response shapes
Includes a gated live harness (ASHBY_LIVE=1) alongside the mocked tests.
vitest.setup.ts stubs global fetch for every file in the app, so the live file
restores the real implementation and asserts the restore worked - without that
guard the whole suite silently passes against a mock.
* feat(blocks): expose the new Ashby operations in the block
fieldValue is polymorphic (boolean, number, string, array, object, null), so it
decodes structured input and otherwise passes text through. The decoding is
deliberately narrow rather than a blanket JSON.parse, which corrupts real text:
1e999 becomes Infinity and serializes back out as null, which CLEARS the field;
a long numeric id loses precision past 2^53; and prose starting with { turns into
an object. Only the literal keywords, {, [ or " prefixes, and exactly
round-tripping numbers decode.
fieldValue carries no wand generationType: json-object forces braces and
json-array forces brackets, and both would wrap a value that must stay bare.
fieldValues, whose contract really is an array, uses json-array.
Setting and clearing an application source are mutually exclusive, so the Source
ID field is conditioned off while the clear switch is on and the params mapping
sends only the intent the switch selects. A value typed before the switch was
flipped cannot reach the tool and surface as an error with no visible cause.
* docs(ashby): document the new operations, permissions, and limitations
Ashby scopes permissions per module and they fail at runtime, not build time, so
the block docs now carry the permission table. Also records the hard API limits
worth designing around: no note or tag on a job, no pagination on
jobPosting.list, and no delete for jobs, candidates, or custom field definitions.
* fix(blocks): stop a stale create-path source id leaking into a source change
The executor merges { ...inputs, ...transformedParams }, so any key the params
mapping leaves unset inherits whatever inputs held. The shared create-path
sourceId subblock reaches inputs even on change_application_source: it is mode
'advanced', and the serializer includes an advanced subblock whenever its value
is non-empty without ever evaluating its condition (serializer/index.ts).
So a source id typed while on Create Application survived into a source change.
With both fields blank it silently attributed a source nobody asked for, and
with the clear switch on it collided with the unset request and failed with no
visible cause, because the field producing it is hidden in that state.
sourceId is now always assigned for this operation rather than conditionally,
so it can never inherit. The regression test asserts the merged result rather
than the mapping alone, since the gap between them is where the bug lived.
|
||
|
|
e7a0d1c4d1 | Harden in-band tool execution and resources | ||
|
|
3d4e3d26dd | fix(settings): preserve tab when switching workspaces (#6704) | ||
|
|
af076a7a34 |
fix(logger, blocks): log nested errors and stop spurious model-selection warnings (#6706)
* fix(logger, blocks): log nested errors and stop spurious model-selection warnings
Two production defects found in the prod logs, neither release-related.
logger: mergeArgs copied object arguments verbatim, so an Error held under a
key stayed an Error instance and JSON.stringify rendered it {} — message and
stack are non-enumerable on Error.prototype. 399 call sites use the
logger.x('msg', { error }) shape and every one logged error: {}, which is why
BlockOutputs failures (10k/week) were undiagnosable. The colorized path had
the same hole via formatObject.
blocks: router, evaluator and agent resolved config.tool through
getBaseModelProviders(), which deliberately excludes gateway providers
(OpenRouter, vLLM, LiteLLM, Ollama, ...). A valid openrouter/* model therefore
threw "Invalid model selected", as did a model still holding an unresolved
<variable.x> at serialization time — ~140 warnings/day. The value is cosmetic
(every handler re-derives the provider from the resolved model), so the throw
bought nothing.
- unwrap keyed Errors on both the structured and colorized log paths
- keep `error` a plain message string so log queries can group on it
- resolve serialized provider ids through getProviderFromModel, the same
resolver the executor uses, via one shared helper for all four call sites
- drop the unreachable `if (!model)` checks behind `params.model || default`
* fix(blocks): move the fallback rationale into TSDoc
The repo forbids non-TSDoc comments; the explanation for why recovery returns
a constant instead of resolving again belongs on the declaration anyway.
|
||
|
|
5bb59f08ee |
feat(connectors): add 9 knowledge base connectors (#6699)
* feat(connectors): add 9 knowledge base connectors
Box, Zoho Desk, PagerDuty, Trello, Microsoft Excel, Google Slides, Google
Vault, Mintlify, and SFTP. Selected by intersecting the published connector
catalogs of Glean, Onyx, Dust, Vectara, Writer, Guru, Elastic, Microsoft 365
Copilot, Notion AI, Unstructured, and Airbyte against services that already
ship a Sim block, so OAuth providers, credentials, and icons are reused. Box
was the largest gap, appearing in 7-8 of ~10 catalogs.
Every connector was validated against live provider documentation twice, the
second pass treating the first pass's conclusions as unproven. Notable
correctness work that came out of that:
Listing truncation. The sync engine hard-deletes documents past a cap that is
not flagged with `listingCapped`, and five connectors had a path there — an
empty Mintlify discovery, Zoho Desk's exact-multiple default caps, Trello's
archived lists and 1000-card ceiling, a Google Vault cursor bailout, and a
PagerDuty stalled page. The engine also gained a backstop: an empty or
collapsed listing blocks deletion reconciliation until the same observation
repeats on a consecutive sync, reconstructed from existing sync-log counters
so no migration is needed.
API alignment. `desk.zoho.ca` does not resolve (Canada is
`desk.zohocloud.ca`, and Singapore and UAE were missing); `modifiedTime` is
absent from Zoho's ticket list projection, so every ticket re-embedded on
every sync; Trello's `dateLastActivity` is documented to miss some edits;
PagerDuty's 10,000-record ceiling bounds `offset + limit`, not offset; Excel
indexed dates as raw serial numbers while Google Sheets renders them; Google
Vault truncated at roughly 249 matters.
Security. SFTP followed symlinks in `getDocument` and composed unchecked
server-supplied filenames into paths; it now also supports optional host-key
fingerprint verification, which runs during key exchange before any password
is sent. Trello interpolated user-supplied board ids into URL paths. Google
Vault is narrowed to `ediscovery.readonly`. `getDataverseBaseUrl` accepted
any host while attaching a bearer token, and is pinned to Microsoft's
Dataverse domains — pre-existing shipped code, fixed here.
Also adds `ConnectorAuthConfig.optional` so a public source can be configured
without inventing an API key, and teaches the scope check that a granted
read-write scope satisfies a required `.readonly` sibling.
Microsoft Dataverse was built and then removed: its OAuth cannot complete
consent. Dataverse requires a per-environment resource URI, the provider
declares a static `https://dynamics.microsoft.com/user_impersonation` that is
not an Entra Application ID URI, and the environment URL is only collected
after the credential exists. That predates this change and also affects the
12 shipped Dataverse tools.
* fix(dataverse): strip the bearer token when a request redirects
The host allowlist added alongside the connector work only constrains the
initial destination. `secureFetchWithPinnedIP` follows redirects and keeps the
`Authorization` header unless a tool opts out, so a redirect away from an
allowed Dataverse origin would forward the caller's OAuth token to whatever
host answers. Dataverse redirects in normal operation — file downloads hand
back a signed storage URL, and environment hosts move between regional
origins — so this is reachable without a compromised environment URL.
Sets `stripAuthOnRedirect` on all 18 Dataverse tools, matching the existing
GitHub job-logs and Windchill precedent.
* fix(connectors): address review findings on listing and hashing
- microsoft-excel: `fetchWorksheets` read only the first Graph page and never
followed `@odata.nextLink`. A workbook with more sheets than fit in one page
dropped the remainder from the listing without setting `listingCapped`, so
the sync engine reconciled those documents away as deleted. The walk now
pages, bounded by MAX_WORKSHEETS, and only follows a nextLink that stays on
the Graph origin, since the link is server-supplied and carries the token.
- google-slides: the listing `contentHash` covered only the file id and
modified time, so toggling the speaker-notes option left every stored hash
matching and no presentation was ever re-hydrated with the new scope. The
setting is now part of the hash, in the single shared stub builder so the
list and hydrate paths stay identical.
- mintlify: `pathPrefix` filtered with a bare `startsWith`, so a prefix of
`/guides` also matched a sibling like `/guides-archive`. It now shares the
`/`-boundary rule `withinBasePath` already used, extracted as `isUnderPath`.
* fix(connectors): list newest first in zoho desk, accept a trailing slash prefix
- zoho-desk: `sortBy: 'createdTime'` is ascending — Zoho denotes descending
with a `-` prefix — so the default 500-record caps kept the oldest tickets
and articles and recent ones were never listed. Because the cap sets
listingCapped, that stale tail could not reconcile away either. Now sorts
`-createdTime`. Still ordering on createdTime rather than modifiedTime, so
rows do not reshuffle mid-walk.
- mintlify: `resolvePathPrefix` kept a trailing slash while `isUnderPath`
accepts an exact match or `prefix + '/'`, so `/guides/` matched neither
`/guides` nor `/guides/intro` and the source synced nothing. A regression
from the previous round, which replaced a bare `startsWith`. The prefix is
now normalized before comparison.
* fix(dataverse): strip the bearer token on the upload route's own redirect
`upload_file` posts to an internal route rather than calling Dataverse
directly, so the tool-level `stripAuthOnRedirect` added in
|
||
|
|
cf78946529 |
fix(v2): close seven correctness and honesty gaps found sweeping the API (#6702)
* fix(v2): close seven correctness and honesty gaps found sweeping the API A ten-slice sweep of the live v2 surface turned up no regression from the recent cancellation work, but did surface a set of pre-existing defects where an endpoint either lost data, hid a failure, or reported something that was not true. Each is fixed at the layer that owns the behavior. Terminal execution logs. The two force-fail boundaries wrote `status: 'failed'` without `ended_at` or `total_duration_ms`, so a force-failed run dropped out of every duration-filtered log query — the same defect class already closed for cancellation, still open on its sibling. The cancellation payload factory is generalized to take the status; the cancellation call sites are untouched and still emit a byte-identical row. Custom tools. One malformed row failed the whole page, and because the list is keyset-paginated that row made every page containing it permanently unreachable. The projection now validates against the same contract schema the route builder applies, repairing only what can be repaired without inventing information — a stringified schema, and a missing `type` whose contract admits exactly one value — and omitting with a warning what cannot. Both rows observed in production are recovered rather than discarded. Table filters. `eq`/`ne`/`in`/`nin` compiled a wrongly-typed operand into a containment test that silently matched nothing, so a filter written against the value the write path had stored returned an empty page instead of its rows. The operand is now read through the same column-type registry the write used, and rejected only where that registry refuses it. Range operators already behaved this way; `null` and the cleared-cell sentinel still pass through untouched. Error messages. A custom `error` on a string schema also replaced the wrong-type wording, so supplying a number for a name reported that the name was missing. Messages now distinguish an omitted field from a mistyped one, `topK` names its own bounds, the knowledge search refine reports against a field rather than the whole body, and a workspace id is bounded before it reaches a lookup. Archived file metadata. A soft-deleted file was listed but unreadable, leaving no way to check share state before restoring it. The read takes the same `scope` selector the list already exposes; the default is unchanged, and the parameter relaxes only the `deleted_at` predicate, never the authorization. Cancellation reporting. Cancelling an already-terminal run reported a durable write that never happened. The service now distinguishes the no-op and names the state it observed, and both surfaces present one vocabulary instead of the internal route deriving its own. No claim predicate or write changed. Protocol. A 401 carries a challenge naming the header the API actually reads, and a body that failed to parse is reported as an unsupported media type only when the caller positively declared a non-JSON one — after the read has already failed, so nothing that succeeds today can begin to fail. * fix(v2): correct three regressions this branch introduced, and harden its tests Adversarial review of the previous commit found that three of its "behavior preserving" claims were wrong. Each is corrected here at the layer that owns it. Table filters no longer coerce a `date` operand, and no longer throw. `date` is the one column type whose registry `coerce` is not idempotent — it drops sub-second precision — and the leaf that compiles a filter also builds the unique-constraint and upsert-conflict probes, so re-reading an already-coerced operand could stop it matching the row it was written from and admit a duplicate inside the write transaction with no error. Throwing was the second mistake: the v2 predicate grammar type-checks structure but not operand values, so a rejected operand no longer failed at submission but inside the delete, update, dispatch and cancel runners, where a filter that cannot compile means the cells it started can no longer be cancelled. Coercion is now total — it rewrites what the registry accepts and passes everything else through unchanged, exactly as before. Reviving a force-failed run no longer inherits its terminal duration. Writing `ended_at` and `total_duration_ms` on the force-fail boundary was correct in isolation, but a partial resume flips that row back to `pending` and those columns survived. The preserved value is meant to be the pause checkpoint — the run's active time — and it had become wall clock measured at the failed resume, which the checkpoint rule then faithfully carried into the next terminal write. The revival clears them only for a row that was terminal, so an ordinary paused row keeps the checkpoint it is supposed to keep. Cancelling reports the terminal state it actually observed. Reclassification now requires that nothing else went wrong, so a genuine paused-reconciliation failure survives instead of being rewritten as an already-terminal no-op, and the claim's own row count — not a snapshot read before it — decides whether this cancel terminalized the run or lost a race to something else. The status the snapshot needed rides along on the ownership query that already reads the row, rather than the second read that query's own contract warns against. A custom tool that cannot be projected now answers the same way everywhere: the list omits it, and reading or patching it by id reports it as absent rather than as a server fault. Analytics stops reporting a cancellation for a request that cancelled nothing. The tests around all of this were audited by mutating each fix and checking the suite noticed. Where it did not, the assertion is stronger now: the absent content-type branch is genuinely exercised rather than relying on a header the client library supplies, the duration encoder is pinned to the column it must measure from, execution ownership is pinned to both ids it must match, and the archived-file concealment test proves it conceals the archived read specifically. Two tests that asserted a paused branch they could not observe are gone; the rendered-SQL test that can decide it already covers them. * fix(execution): report a workflow-group cancellation as the write it performed Cancelling a workflow-group run whose log had already been cancelled, but whose cell sidecar still needed reconciliation, durably cancelled that sidecar and then reported `already_cancelled` with `durablyRecorded: false` — because the terminal-status shortcut answered from the entry snapshot alone and never asked what this request had written. The analytics event, which now gates on that field, stopped firing for a cancellation that really happened. The outcome a cancel reports is the same question whichever path answers it, so there is now one vocabulary for it rather than one the direct claim tracked and one the group transition did not. Every group result maps to that outcome through a total map, so a new group result cannot compile without deciding what it wrote, and the reclassification leads with whether this request wrote at all. A group transition that reports itself already cancelled is deliberately mapped as unknown rather than as a no-op: it leaves the sidecar alone but still terminalizes a log that was active, and the result does not say which happened. That costs nothing today, because the only snapshot that would reclassify proves the log was already terminal. * fix(execution): have a workflow-group cancellation report the writes it made Three review findings landed on the same reporting logic, each a different face of one cause: the caller could not see what the group transaction had written, so it inferred. It inferred from an entry snapshot, then from the returned kind, and the remaining blind spot was the kind that covers two different transactions — a repair that terminalizes an active log, and a genuine no-op — which left a cancel that wrote nothing still claiming a durable write when it lost a race. The transaction now reports both writes it can make, each read from that statement's own returning row and recorded immediately before the throw that already depended on it, so the report cannot drift from the write. The caller derives its outcome from those rather than from the kind, and the kind is back to naming the situation instead of standing in for the work. The group path can now always answer whether it wrote. The only remaining unknown is the direct claim when its update throws or is never attempted, which genuinely has no row count to report. |
||
|
|
237f973a11 |
fix(condition): stop a secret value from breaking or forging a condition (#6705)
Condition expressions pasted every environment variable value into the
expression as source. Block references in the same expression go through a
proper escape and get quoted; env vars went through neither. That left three
defects:
- A bare string placeholder was a SyntaxError. `{{NAME}} === 'alice'` resolved
to `alice === 'alice'`, so the form the Function block docs recommend could
not be used here at all.
- Ordinary data broke the block. An apostrophe (`O'Brien`) or a newline in a
legitimate value produced unparseable source and failed the run.
- The quoted form was injectable. A value of `x' || true || '` turned
`'{{NAME}}' === 'bob'` into `'x' || true || '' === 'bob'`, forging a true
branch out of a comparison that should be false.
Inline only structurally inert literals — numbers, booleans, and null, with
optional space/tab padding. Every other value keeps its `{{NAME}}` placeholder
and is bound as a string by the execution-boundary compiler, the same one
Function blocks and Custom Tools already use.
Legacy outcomes are preserved. `{{COUNT}} === 3` and `{{ENABLED}} === true`
still compare as literals, and an embedded `"Bearer {{API_KEY}}"` still
compares equal — now via compiled concatenation rather than a pasted value.
Padding is admitted rather than trimmed so the inlined text stays
byte-identical to the stored value, which is what keeps a padded number
correct both bare and quoted.
A resolved secret also no longer travels to the execution boundary inside the
condition source.
The one deliberate behavior change: a value whose text is itself a quoted JS
literal (a secret stored as `'foo'`, a plausible workaround for the bare-string
SyntaxError) now compares as the 5-character string rather than as source.
That form is the injectable one, so it cannot be kept.
Docs: state the placeholder type contract, which was described mechanically but
never in terms of what a reader gets. `{{KEY}}` in Function and Custom Tool code
always evaluates to a string, so a bare `if ({{FLAG}})` is always true and a list
has to be stored as JSON. This is what a customer hit after the resolver lift in
#6247 moved Function blocks off source inlining.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
19e5efdfe6 |
Name agents in orchestration titles; '+ n more' overflow format
wait/tail/steer/interrupt titles humanize the slugified agent ids back to
their display names ('Waiting for the first of Digest Workflow Build + 4
more'), and the agent card's parallel-tool suffix uses the same '+ n more'
format.
|
||
|
|
b98ef9b70a |
Persist subagent lane lifecycle blocks from the span handler
Lane-scoped span events route to the span handler, which only recorded trace side effects — no subagent start block was ever persisted (verified: a seven-agent run stored 104 blocks with zero starts). Grouping then fell back to keying lane content by agent NAME, so a respawned agent of the same type merged invisibly into the first one's card until it resolved. The handler now persists the start block (spanId-keyed and deduped, carrying the display name) and stamps endedAt on close, giving every invocation its own card. |
||
|
|
1e51860a20 |
Keep the main Sim lane live-expanded; collapse only real subagent cards
The mothership group is the turn's own narration, not a delegation card — collapsing it hid main-lane text and tools until manual expand, which read as mis-ordered streaming while async subagents interleaved. It keeps the original live-expand behavior and no status suffix. |
||
|
|
af516f3f58 |
Replace intents with live tool-title status lines on agent cards
Intent parsing is fully removed (turn model, relay handler, persistence fields, group extraction). The collapsed card's status is the latest tool call in its RUNNING phrasing — never the completed rewrite, which stays in the expanded log. Parallel tools show the most recently started still-running title with a +N for concurrent siblings; between rounds the last title stays frozen; a closed lane shows the bare name. Nested agent cards compute their own status recursively from their own items. |
||
|
|
99397debde |
Drop the tool-title fallback: the status line is the agent's intent
With the intent protocol now injected into every spawn's task message, agents open with an <intent> tag; the card shows that narration or nothing. |
||
|
|
95153c0c41 |
Catch subagent <intent> tags in the server relay
The relay's subagent text handler now runs the split-safe intent extraction as chunks stream: the latest complete tag is stamped onto the lane's persisted subagent block (subagentIntent) and stripped from the stored prose, so live, persisted, and replayed views all agree. Per-lane carry handles tags split across chunks; a never-closing tag flushes back as plain text. |
||
|
|
9aaa42810b |
Fall back to the live tool title for the agent card status line
Persisted data proved tool-first subagents (grok search agents) emit zero prose, so intent tags never stream no matter what the prompt says. The collapsed card now always narrates: the agent's own <intent> tag when present, else the latest tool's display title while the lane is live. |
||
|
|
90d07b0e3b |
Extract intents from group text on every path, sync and async
The turn-model intent filter only fires for span-scoped subagent lanes, but this surface also delivers subagent text through the legacy block path — so <intent> tags flowed through unparsed and rendered as prose rows. Groups now extract intents from their accumulated text at append time: the last complete tag becomes the card's status line and every complete tag is stripped from the rendered prose. Covers span-scoped, legacy, and persisted-reload paths for both synchronous and background delegations. |
||
|
|
a7115e87ee |
feat(integrations): add Azure Data Explorer (#6701)
* feat(integrations): add Azure Data Explorer Add a 14-operation Azure Data Explorer (Kusto) integration covering KQL queries, schema and metadata discovery, table management, inline and query-sourced ingestion, ingestion-failure triage, and arbitrary management commands. Authentication uses a Microsoft Entra service principal through an internal proxy route, since the Kusto token audience is per-cluster and cannot be expressed as a static-scope OAuth provider. * fix(azure-data-explorer): only read partial-failure status from the QueryStatus table Scanning every returned table for Severity and StatusDescription columns misread an ordinary query as a failed request whenever the user's own result selected columns of those names — a common shape for a log table. Failure detection now consults only the table the response's table of contents names as QueryStatus, and primary-result selection reuses the same index instead of re-reading it. * fix(azure-data-explorer): keep the Show Operations and Show Table Details cards from painting empty check:canvas-sentences flagged the Show Operations sentence: it anchored `core` on operationId, which is an advanced-mode optional field, so an untouched card resolved to nothing and painted empty. Show Table Details had the same shape in milder form — table is optional there, since omitting it describes every table, leaving a dangling preposition. Both now lead with literal copy and treat their field as an optional refinement. Also simplifies the primary-table condition to a single `!= null` check. * fix(azure-data-explorer): authenticate sovereign clusters against their own Entra authority The cluster allowlist accepted Azure China and US Government hosts, but every token request went to login.microsoftonline.com. Those clouds are isolated instances with their own Entra endpoints, so a sovereign cluster passed URI validation and then could never obtain a token. Each Kusto service domain is now declared alongside the authority that issues tokens for it, so the two cannot drift apart, and the authority is part of the token cache key. * improvement(azure-data-explorer): warn that ingest-from-query matches columns by position Kusto aligns an ingested query result to the target table on column type and order, never on column name, so a query projecting the right columns in the wrong order lands data in the wrong columns without erroring. Surfaces that in the tool description and param the model reads, in the wand prompt that generates the query, in the rollup skill's steps, and in the docs. Also verifies the target schema first rather than after. * chore(azure-data-explorer): drop the unsourced kustomfa host from the cluster allowlist Every other entry traces to a Microsoft reference — the Kusto connection-string doc, the national-cloud endpoint tables, and the Fabric KQL-database REST reference. kustomfa.windows.net does not, and the connection-string doc states the trust boundary as hostnames ending in kusto.windows.net. An allowlist should only hold hosts we can justify, so this drops it and records the sourcing standard for anything added later. * fix(azure-data-explorer): handle commas inside quoted properties and empty extent IDs Two defects in the shared command helpers: buildWithClause split the property list on every comma before validating, so a value that legally contains one — a docstring sentence, or a tags array with more than one entry — was torn in half and rejected. Splitting is now quote-aware, and an unterminated quote is rejected outright rather than swallowing the rest of the clause. transformColumnListResponse dropped empty strings, but `.ingest inline` reports "no data shards were generated" as a single record carrying an empty extent ID. A no-op load therefore looked like a missing column instead of an empty result. Only non-strings are skipped now. |
||
|
|
1d342722ad |
feat(rabbitmq): add RabbitMQ integration (#6700)
* feat(rabbitmq): add RabbitMQ integration * fix(rabbitmq): strip auth on redirect, require https, and bound the retrieval response * fix(rabbitmq): reserve message metadata in the retrieval response budget |
||
|
|
dfccbac353 |
Persist resource side effects for in-band tool execution
Files/tables created through the internal execute route now register on the chat's resources exactly like the resume driver's executions — the route runs the same handleResourceSideEffects pass (persistence only; an out-of-band route has no live event sink, so mid-turn chip pushes are a follow-up). |
||
|
|
97cd8b7761 |
Add the internal in-band tool execution route for live mothership turns
POST /api/copilot/tools/execute (INTERNAL_API_SECRET, Go→Sim) runs one sim-server tool through the same server tool router the resume driver uses and returns the result synchronously — no checkpoint. This is what lets background (async) subagents write files/tables/knowledge, and lets the main lane keep streaming (instead of checkpoint-pausing and killing every background run) while async agents are live. |
||
|
|
c51683658e |
Collapsed-by-default agent cards with live intent status lines
Subagents now narrate their work through <intent>3-5 words</intent> tags (a fleet-wide prompt protocol on the mothership side). The turn model streams each subagent's text through a split-safe tag parser: complete tags update the agent's currentIntent and disappear from the prose, tags split across deltas are carried until their close arrives, and a tag that never closes flushes back as plain text. The agent card renders as one line — display name (or agent label) plus the latest intent, replaced inline as the agent shifts gears — and never auto-expands; expanding to the full tool log is a deliberate click. Only an outstanding permission prompt or a browser hand-back forces a group open. Intents persist on the subagent block (and through the legacy persisted- message paths) so reloads keep the last status, and a renamed reinvocation now takes the latest name instead of pinning the first. |
||
|
|
3051954396 |
fix(agent): show files in tool picker (#6666)
* fix(agent): show files in tool picker * chore(agent): remove redundant tool exclusions |
||
|
|
7f64d5e600 | perf(tables): stop a table write refetching every loaded page in the tab that made it (#6698) | ||
|
|
63bc10816a |
Show 'Waiting for the first of N agents' for mode-any waits
The wait_agents title ignored the mode argument, so an any-mode wait over three agents read 'Waiting for 3 agents' while the model narrated waiting for the first — contradicting the transcript. |
||
|
|
e0a14840c0 | Harden VFS resource operations | ||
|
|
41819124d7 | fix(search): stop cmd+k boosts from lifting weaker matches over stronger ones (#6668) |