Skip to content

v0.8.24: security hardening, model capability advertisement, browser improvements - #7562

Merged
waleedlatif1 merged 14 commits into
mainfrom
staging
Sep 6, 2026
Merged

v0.8.24: security hardening, model capability advertisement, browser improvements#7562
waleedlatif1 merged 14 commits into
mainfrom
staging

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

waleedlatif1 and others added 13 commits September 5, 2026 13:12
* fix(knowledge): bound JSON/YAML chunker expansion

JsonYamlChunker re-parsed and re-serialized document content with no
expansion limit. ChunkBudget only counts emitted chunks, so every parse
and full-object stringify ran before it could fire — a small aliased YAML
source expands to tens of MB, and the same content was parsed twice
because isStructuredData and chunkJsonYaml each parsed it.

- Measure the parsed value with the shared measureYamlExpansion guard
  before anything materializes it, and skip parsing entirely when the
  source is already larger than the ceiling
- Size the ceiling to the most text the chunker could ever emit
  (maxChunks x chunkSize), floored at 4MB and capped at what the YAML
  file parser itself permits, so documents that fit the budget chunk
  exactly as before
- Replace isStructuredData + chunkJsonYaml with one chunkStructured entry
  point that parses once and returns null when the content is not
  structured, leaving chunker selection with the document processor

* fix(knowledge): allow proportionate expansion in the chunker guard

Comparing the expansion estimate against the output budget mixed two
units: measureYamlExpansion charges a flat per-node allowance, so a
document of many small values is charged several times its pretty-printed
size and was rejected even though its chunks fit the budget — a flat array
of a million booleans is charged ~22MB against ~8MB of real output.

Allow the larger of two admissible expansions: one that fits the output
budget, and one proportionate to the source. Alias expansion overshoots
its source by orders of magnitude, so it is still rejected, while an
ordinary large document is no longer charged for the estimator's
conservatism. Neither allowance ever exceeds the file parser's own cap.
…-bomb check (#7526)

`extractDocxText` rescues a parse failure by re-reading the package as a
possibly-empty document, and that rescue handed the buffer to JSZip with no size
guard — so a zip bomb, whose rejection is exactly what routes it there, got its
`word/document.xml` read into a string uncapped. Guard the rescue the way every
other JSZip call site in the file already does, outside the catch so the
rejection propagates.

`extractDocAssets` handed an attacker-supplied .pptx/.docx straight to JSZip and
inflated every media entry into a retained Buffer, with only a compressed-size
ceiling upstream. Apply the same shared guard the document parsers and
`extractDocumentStyle` already apply to the same class of input.

No new limits are introduced — both call sites now use the existing shared
ceilings, so a file rejected here is one Sim's text-extraction path already
rejects today.


Claude-Session: https://claude.ai/code/session_01VmwJP8EmSo3KcMFoKjpd6y

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fig (#7525)

* fix(chat): bound deployed-chat callers and stop leaking chat gate config

Two authorization/throttling defects on chat deployments.

**Denial of wallet on POST /api/chat/[identifier].** A deployed chat resolves
its execution principal from the workflow's workspace, so the plan rate bucket,
the usage/credit check and the concurrency reservation all belong to the owner
while the request belongs to whoever found the link. Nothing bounded the caller,
and an abort refunds none of it. Both the per-IP and the per-deployment bucket
now run after auth and before `preprocessExecution`, on every execution
regardless of `authType` — an email or SSO visitor is still not the payer.

`GET /api/chat/validate` answered for any anonymous caller, so `available:false`
inventoried live deployments; it now needs a session and a per-user bucket.

**Chat gate config exposed at workflow `read` on GET /api/workflows/[id]/chat/
status.** The route reimplemented the admin-gated detail projection inline,
serving the `allowedEmails` allow-list, `hasPassword` and the customization blob
to any workspace viewer, and asserting no `deploy.chat` capability. It is now an
adapter over `chat_deployments.list` — the same operation `GET /api/v2/chat-
deployments` binds — returning only the deployment's id and identifier, which is
all the editor reads before fetching the detail from `/api/chat/manage/{id}`.

The two buckets are the existing `enforceIpRateLimitWithIndependentBackstop`
plus a new `enforceResourceRateLimit` beside its siblings in `route-helpers`.
The IP bucket is consulted first and returns on refusal, so one flooding IP
cannot drain the deployment's budget and 429 the real audience with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj

* fix(chat): drop the env knobs and put the ceiling under the plan bucket

Two corrections to the execution throttle.

The per-deployment ceiling was 300/min, at or above the workspace `sync`
counter it debits on every plan but enterprise — 50 free, 150 pro, 300 team.
A flood therefore drained that shared counter, which the owner's API, webhook
and scheduled runs draw from too, before the ceiling ever refused: the
availability half of the report went unmitigated on exactly the plans most
workspaces are on. It is now 60/min sustained, under even the cheapest paid
plan, with a test that pins it there against `RATE_LIMITS`. The per-IP bucket
drops to 30/min so one host cannot take a deployment's whole allowance, and
both gain the 2x burst allowance the plan buckets already use.

Both limits go back to plain constants. Every sibling deployment throttle —
password, OTP, SSO, on chat and on public file shares — is a hardcoded
`TokenBucketConfig`, so the two env vars were the only configurable ones of
their kind and bought speculative tuning for a control with sane defaults.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj

* fix(chat): derive the chat ceiling from the plan table it must stay under

The 60/min ceiling still sat above the free plan's 50/min sync rate, so on free
the shared counter — the one the owner's API, webhook and scheduled runs also
draw from — still emptied before the ceiling refused. Every plan rate is also
operator-overridable through `RATE_LIMIT_*_SYNC`, which no hardcoded number can
track.

It is now derived: 80% of the smallest configured plan sync rate, which is
40/min with the defaults and stays under every plan by construction. The per-IP
bucket follows at half that. Tests assert the invariant against each plan in
`RATE_LIMITS`, on burst as well as sustained rate, rather than pinning numbers
that would need editing the next time a plan default moves.

This floor is shared by all plans, so enterprise is held to the same 40/min as
free. Sizing the slice to the payer's own plan needs the subscription, which
`preprocessExecution` resolves just after this runs — that is the follow-up,
and the same hook bounds the generic-webhook surface that is still unbounded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj

* docs(chat): note the one plan rate where the derived ceiling lands equal

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj

* refactor(rate-limit): scope the per-IP bucket by resource id, not by bucket name

The chat call interpolated the deployment id into `bucketName`, which produces
a correct key but puts a per-deployment value into the field both log lines
emit as `bucket` — high cardinality on a label meant to name a bucket family,
and asymmetric with the `enforceResourceRateLimit` call beside it that takes
the id as its own argument.

`enforceIpRateLimitWithIndependentBackstop` now takes an optional `resourceId`,
so the pair reads the same way and `resourceId` is logged as its own field.
The unscoped key shape is unchanged for the existing callers, with a test
pinning both shapes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj

* test(rate-limit): drop needless any casts on the mock request

createMockRequest already returns NextRequest, so the casts weakened the
helper's input contract in the new tests for nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWh9WFMYNZ6uTFQFUzF8Bj

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(models): gate forced tool use by model capability

* fix(models): derive forced tool support from capability metadata

* refactor(models): use catalog capabilities for forced tool support
* feat(editor): replace canonical mode toggle with icon switch

* fix(emcn): avoid icon switch package import cycle
Revert a0f38db because the new icon switch is not ready to ship. Keep the restoration and remaining mode-toggle migrations in a separate follow-up.
* feat(browser): add verified form filling and horizontal scroll

* fix(browser): align form schemas and activity titles

* fix(browser): detect truncated popup and dialog observations
…dels (#7555)

* fix(quickbooks): harden the app-level webhook ingress

Three defects on the QuickBooks CloudEvents ingress:

- The pre-ack path loaded and decrypted every account connected to the
  addressed Intuit app before checking the signature. Verifier tokens are
  now produced by an async generator and consumed one at a time, stopping
  at the first match, so a legitimate delivery no longer burns the whole
  app's fan-out inside Intuit's 3-second acknowledgement budget.
- One unmodelled element rejected the entire delivery with 400. Intuit
  retries a 400 indefinitely and withholds later events until one is
  acknowledged, so a single bad payload stopped webhooks for every Sim
  workspace on that Intuit app. The array shape is still bounded, but
  elements are parsed individually, unparseable ones are dropped with a
  warning, and the delivery is acknowledged with 200.
- formatInput emitted the lowercase wire token ("invoice") as entityType.
  It now resolves the trigger definition from the parsed entity and emits
  the canonical QuickBooks name ("Invoice") the read tools expect;
  eventType still carries the raw wire string.

Also count an unroutable company id as ignored rather than failed, so a
permanently impossible event no longer retries three times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): stop item full updates from corrupting inventory and transaction history

Two of these silently rewrite a customer's accounting records.

An Item full update echoes the record read back from QuickBooks. Intuit
documents InvStartDate asymmetrically: "For read operations, the date returned
in this field is always the originally provided inventory start date. For
update operations, the date supplied is interpreted as the inventory adjust
date, is stored as such in the underlying data model, and is reflected in the
QuickBooks Online UI." QtyOnHand is re-asserted the same way. Both are
"Required for Inventory type items", so neither can simply be dropped from the
body — refuse the update instead, matching create_item's existing
Service/NonInventory restriction. Intuit also documents inactivation as "Not
valid for Category item types", so an Active change on a Category is refused
too.

The Item update also posted to a bare endpoint. Intuit: "Add the query
parameter, include=donotupdateaccountontxns, to the endpoint to supress
updating the income or expense account on any existing transactions associated
with this Item object." Without it, changing an item's account rewrote every
historical transaction linked to it. The parameter is documented on the Item
update alone, so it is not applied to any other entity.

Also aligns the shared plumbing with the documented model:

- PhysicalAddress documents Line1-Line5; the write map carried only Line1/Line2,
  so an address Sim had just read could not be written back.
- Fault.type (ValidationFault / SystemFault / AuthenticationFault /
  AuthorizationFault) was discarded, hiding the classification that separates a
  bad payload from a dead token. Matched by prefix because Intuit's pages
  disagree between "ValidationFault" and type="Validation".
- The query string used the form-encoded "+" for spaces; Intuit's own example
  percent-encodes them.
- MAXRESULTS was capped at 100 against a documented maximum of 1,000.
- Validates documented constraints locally: DisplayName <=500, Item.Name <=100
  with no tabs, new lines, or colons, and an email address Intuit can store.
- update_item's activeStatus and update_employee's displayName now carry the
  Category and Payroll caveats Intuit documents.

Adds the missing query-builder coverage and a full-update test that would catch
feeding the sanitized record into the merge, which would null every vendor's
TaxIdentifier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): gate every internal tool operation and contract-bind the provider ops

The QuickBooks internal handler returned for all twelve provider create/update
tool ids above both the operation input cap and the trusted-identity check, so
those gates only ever ran for the three file tool ids. Hoist both above the
switch, matching the Asana handler.

The same twelve operations had no boundary schema — executeToolOperationImplementation
only checks the input is a non-array object before casting to the operation's
param type. Author a contract per operation and route them through
executeInternalJsonToolOperation, the canonical in-process path. The two file
operations now parse through their contracts as well, which were previously
declared but unreferenced.

Also pass the transfer signal, not the caller's, when reading a failed
transaction-PDF response body, so a stalled Intuit error body stays bounded by
the 60s transfer deadline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): stop persisting the Intuit identity token and share the webhook batch ceiling

The OIDC identity JWT is only meaningful at connection time, where profile.accountId
is already derived from it. Persisting it projected the token into the credential
payload of every QuickBooks tool call, none of which read it. Gating it in
token-resolution instead would break Shopify, which reads params.idToken as a shop
domain fallback.

Also exports QUICKBOOKS_WEBHOOK_MAX_EVENTS from the contract so the route no longer
carries a second copy of the batch ceiling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): stop replace-allocations detaching non-invoice payment links

buildPaymentLines returned only invoice LinkedTxn entries, and the
unapplyOmittedInvoices path sent that array as the payment's entire Line
collection. Intuit documents Payment Line.LinkedTxn.TxnType as one of
Expense, Check, CreditCardCredit, JournalEntry, CreditMemo or Invoice, and
an update as "send all the Lines that need to be present MINUS the lines
that need to be removed" — so replacing invoice allocations silently
detached every applied credit memo, expense, check and journal entry.
Non-invoice lines are now carried forward first, in the order QuickBooks
returned them, and counted against the payment total.

Also aligned with Intuit's documented model:
- salesreceiptrequest requires only Line, refundreceiptrequest only
  DepositToAccountRef and Line; neither lists CustomerRef, so customerId is
  optional on both receipt paths and CustomerRef is emitted only when given.
- Line.Amount, SalesItemLineDetail.Qty and UnitPrice carry no positivity or
  non-zero constraint, so zero is accepted (finite/2-decimal/safe-range
  checks unchanged).
- Enforce the documented maximum lengths locally: DocNumber 21,
  MemoRef.value 1000, Line.Description 4000.
- Add void_sales_receipt, the documented
  salesreceipt?operation=update&include=void sparse void.
- read_sales_transactions maxResults description now says 1–1000, matching
  validateQuickBooksPagination.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): align purchasing and accounting tools with Intuit's model

Validated against Intuit's live machine-readable model files
(EntityJsonObject_v1.json, CodesModelsJsonObjects_v2.json).

- CurrencyRef is "Conditionally required" on all seven purchasing and
  accounting create models ("This must be defined if multicurrency is
  enabled for the company") and was never written, so every create failed
  on a multicurrency company. Creates now accept an ISO 4217 currencyCode.
- GlobalTaxCalculation is "Conditionally required" on Bill, VendorCredit,
  PurchaseOrder, JournalEntry, and Deposit, and Optional on Purchase
  ("Not applicable to US companies; required for non-US companies"). Those
  six creates now accept it; JournalEntry accepts only the two values
  Intuit documents for it. BillPayment has no such property and is left alone.
- Add void_bill_payment, the documented
  POST /billpayment?operation=update&include=void operation.
- itembasedexpenselinedetail.Required is [] and ItemRef is Optional, so an
  item line no longer requires itemId.
- The Deposit sparse update leaves "missing elements untouched", so
  depositAccountId is no longer forced on every update.
- BillPayment DocNumber and APAccountRef are Optional writable fields and
  were unreachable; PurchaseOrder DueDate was emitted by the body builder
  but had no parameter.
- Correct the maxResults range in the two read tools to 1-1000, matching
  validateQuickBooksPagination.
- Record the unresolved BillPayment "Line [0..n]" / requiredFlag Required
  contradiction as a TSDoc note; behavior unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): correct the sparse-void TSDoc and narrow receipt customerId types

The sparse guard's TSDoc claimed Intuit documents `sparse` as required to void
any object. That is false for Invoice, whose void request model is
`deleterequest` (Id + SyncToken, no sparse); only the `include=void` form
carries it. The code was already correct; the comment would have led an editor
to 'fix' void_invoice.ts into breaking it.

Also narrows QuickBooksCreateSalesReceiptParams.customerId to optional so the
type matches the required:false declaration, per salesreceiptrequest.Required
listing only Line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): align reports and attachments with Intuit's report catalog

Every capability flag is now transcribed from that report's own `*query` model
in Intuit's machine-readable report catalog, which is the source the developer
docs render their parameter tables from.

Reports:
- ap_aging_detail now advertises accountingMethod; `agedpayabledetailquery`
  documents `accounting_method`, so Sim was throwing on a request Intuit accepts.
- ap_aging_summary now accepts customerId; `agedpayablesquery` documents `customer`.
- Adds trial_balance_fr. Intuit documents one report with two endpoints —
  TrialBalanceFR for FR-locale companies, TrialBalance otherwise.
- Adds the ten remaining documented report endpoints: AccountList,
  CustomerBalanceDetail, CustomerIncome, GeneralLedger, InventoryValuationDetail,
  InventoryValuationSummary, ClassSales, DepartmentSales, TaxSummary,
  VendorBalanceDetail. Each one's flags come from its own query model.
- Exposes `date_macro` and `qzurl`, both documented per-report query params.
  `qzurl` is what populates the quick-zoom `href` links the row outputs already
  declare, and `date_macro` is mutually exclusive with an explicit date range.
- Exposes the `employee` filter that only `profitandlossdetailquery` documents,
  and adds the `Employee` report-header echo Intuit's `reportheader` model lists.

Attachments:
- parseQuickBooksAttachableResponse takes an operation label. Reading an
  attachment by id reported failures as "attachment upload failed".
- A dotless file name no longer reports itself as its own extension, so an
  unattachable `backup` is refused as extensionless rather than as "the backup
  file type".
- Records why `.jpg` is canonicalized to image/jpeg: QuickBooks normalizes
  content type on ingest and `attachablerequest` has no ContentType property.

Replaces the file_operations tool-wiring tests, which asserted only that
`operation.input` is an identity projection, with coverage of the extension
allowlist, MIME canonicalization, file-name sanitization, Attachable metadata,
and fault labelling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): document-align the bill payment account check and refund receipt update

Intuit's BillPaymentCheck.BankAccountRef requires "Account.AccountType set to
Bank and Account.AccountSubType set to Checking", and
BillPaymentCreditCard.CCAccountRef requires "AccountType set to Credit Card and
AccountSubType set to CreditCard". The create-bill-payment guard only compared
AccountType, so a Savings or Line-of-Credit account passed the local check and
failed at Intuit.

Intuit documents RefundReceipt::UPDATE "Sparse update a refund receipt", which
"only elements specified in the request are updated. Missing elements are left
untouched." The operation used the read-merge-write full update instead, adding
a round trip and a read/write race. Invoice, Estimate and SalesReceipt already
post their sparse bodies directly; RefundReceipt now matches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): describe the refund receipt update as sparse

The update path now posts a sparse body directly instead of read-merge-write,
per RefundReceipt::UPDATE 'Sparse update a refund receipt'. The LLM-facing tool
description still claimed a full update.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): wire the block and registry to the Wave 1-2 tool changes

Registers the two Wave 2 void tools, feeds the create parameters that had no
UI, and corrects the block-level and report-metadata defects, all against
Intuit's published request/response and report query models.

Registration
- Export and register quickbooks_void_sales_receipt and
  quickbooks_void_bill_payment; both were reachable from no surface.
- Mirror the void_customer_payment sites: operation option, canvas sentence,
  transaction ID / sync token / confirm conditions, tools.access, and params.

Parameters that existed on tools but had no UI
- currencyCode on every create whose request model marks CurrencyRef
  conditionally required under multicurrency.
- globalTaxCalculation on the same set minus bill payment, which
  billpaymentresponse does not carry; JournalEntry narrows to the two values
  its model documents.
- dueDate on create and update purchase order, apAccountId and documentNumber
  on create bill payment.
- Report date macro, quick-zoom links, and the employee filter; the quick-zoom
  string is coerced in tools.config.params, never in tools.config.tool.
- Report dropdown now offers all 26 documented reports.

Block defects
- Sales receipt and refund receipt no longer require a customer; Intuit's
  salesreceiptrequest and refundreceiptrequest do not list CustomerRef.
- Pagination accepts the documented ceiling of 1000, not 100.
- The attachment file name no longer means two opposite things: the upload
  override is scoped to Add Attachment in File mode like its siblings, and the
  saved-file name gets its own field.
- Item account fields explain the locale rule instead of a bare placeholder,
  and the inconsistent advanced/basic pairs are aligned.

Report metadata
- summarize_column_by collapses to the single twelve-value list every one of
  the fourteen documenting models shares, Employees included.
- appaid, arpaid, and group_by are gated per report: the customer and vendor
  balance models and inventoryvaluationdetailquery document them too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): split the dual-semantic transactionId and register the missing subblock migrations

`transactionId` was one control across the three by-ID reads and all fourteen
updates and voids, and `tools.config.params` republished that single stored
value as the read target, `paymentId`, `billId`, `purchaseOrderId`,
`journalEntryId` and the rest. Because subblock values are keyed by ID and are
never cleared when the operation changes, a bill ID read under Read Purchasing
Transactions survived a switch to Update Purchase Order and addressed the wrong
entity while the block still validated. The read path moves to
`readTransactionId` and `transactionId` keeps the mutations.

Registers the operation-scoped migration for that rename plus the four fields an
earlier change orphaned without one: the three `summarize_column_by` subsets
that collapsed into `reportSummarizeBy`, and the download-side
`attachmentFileName` that moved to `downloadAttachmentFileName`.

`syncToken` is left alone: it is live only across updates and voids, never
across the read/mutate boundary, and carries one value space. A stale token
after an operation switch is rejected by QuickBooks rather than silently
targeting the wrong entity.

Regenerates the integration docs, tool metadata, and integration catalog, which
were already stale on this branch from the Wave 1-3 tool description changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* fix(quickbooks): declare the bill payment and purchase order fields the contracts dropped

A contract body is a Zod object, so any key it does not declare is stripped
before the provider operation runs - silently, with no validation error. The
contracts were authored before currencyCode/apAccountId/documentNumber were
added to Create Bill Payment and dueDate to Update Purchase Order, so those
params were dead: the block forwarded them and they never reached Intuit.

Adds a parity test across all twelve contract-bound operations so a param added
to a tool without its contract fails instead of silently disappearing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

* test(quickbooks): extend contract parity coverage to the file operations

The download body is a discriminated union and the add-attachment body carries a
superRefine, so neither exposes a flat shape - but their declared keys are still
introspectable, so all three file tools are now held to the same parity rule as
the twelve JSON operations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RB5M2gk7zZgGomnQqr7WRB

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…7558)

* fix(anthropic): keep enum and const as structured-output constraints

The SDK's transformJSONSchema pops every keyword it recognises and
stringifies whatever is left into the node's description. It has no
branch for enum or const, so both were silently demoted from grammar
constraints into prose, leaving native structured outputs unenforced
for them on every Anthropic and Azure Anthropic agent block.

Lift enum and const out before the transform and re-attach them after,
so the transform's sanitising is preserved verbatim. Only lift the
shapes the API grammar-checks: a non-empty enum of primitives matching
the node's declared type, and a primitive const. Anything else stays
in place and is demoted exactly as before, since sending it would make
the API reject the whole request.

* chore(anthropic): type the structured-output test helper instead of any

Replaces the two Record<string, any> casts in the new test helper with a
WireSchemaNode interface describing the subset of the wire schema the
assertions read back, per the repo's no-any rule.

* test(anthropic): lock the structured-output schema non-mutation invariant

The builder deep-clones before walking so it never deletes enum/const from
the caller's schema object. Agent blocks inside a parallel or loop reuse the
same responseFormat object across iterations, so losing that clone would
strip the constraints from every iteration after the first. Verified the
test fails when the clone is removed.
…py fixes (#7560)

Comparison pages get three optional profile fields — a direct-answer lead
paragraph, an "Is Sim better than {name}?" verdict, and per-section lead-ins
— and the single feature table splits into one H2 + table per section so each
section is quotable on its own.

Dust: adds the lead paragraph, the verdict section, and all seven section
lead-ins.

OpenAI AgentKit: adds section lead-ins (with contextual links to Sim's
pricing, docs, and self-hosting pages), drops the repeated
"shutdown November 30, 2026" clause from the eight rows that restated it
(the date stays in the intro and the shutdown limitation card), shortens the
custom-blocks, agent-skills, and environment-promotion rows, and states the
built-in-vs-third-party-MCP distinction directly instead of announcing it.
…per token (#7559)

* fix(copilot): stream append previews as deltas instead of a snapshot per token

An `append` preview is `existingContent + streamed`, so forcing a full snapshot on
every emission re-sent the entire file once per streamed token. Cost is
`O(file x tokens)` into the stream buffer, whose only trim is a 100,000-member
rank cap — a count cap does not bound a member whose size is the file. One 250 KB
file cost gigabytes.

The forced snapshot was correct when it was written: the preview consumer could
not merge a delta. It gained that ability two months later and the producer was
never relaxed, so the branch has been dead weight since — `update` has shipped
deltas through the same reducer ever since, and `deriveFilePreviewSession` is the
only thing in the app that reads `contentMode`.

Removing it changes no rendered text. The two conditions that make a snapshot
necessary still force one: a base that diverges mid-stream fails
`startsWith`, and the checkpoint interval still emits a recoverable full snapshot
so no delta chain runs longer than a second.

`file-preview-append-roundtrip.test.ts` drives the real producer into the real
consumer and asserts the reconstructed text is byte-identical, including at
token-scale chunking over a 250 KB base, across a diverging base, and under
duplicate delivery. Both halves were verified to fail when the behaviour they pin
is reverted.

Measured against the session that prompted this: 2.29 GiB -> 55 MB.

* chore(rules): drop the Redis payload rule until it has enforcement behind it

The transferable part of it — cap bytes, never entries — is real: three
independent writers made that exact mistake. But a rule pointing at a budget
module that two writers use and three ignore is prose, not a rule.

It belongs in `sim-caching.md`, trimmed to the part that transfers, alongside the
change that routes those writers through `redis-budget.server.ts`.

* test(copilot): pin that a failed delivery still reaches the stream buffer

Streaming preview content as deltas is only safe because the replay chain is
complete: `publish` persists after enqueuing and unconditionally, and a failed
enqueue marks the client disconnected rather than throwing. So an envelope the
client never received is still in Redis, and the producer is never told a delivery
failed — it cannot advance past a gap the buffer does not have.

That invariant was load-bearing and untested. Making persistence conditional on
delivery now fails this test.
@greptile-apps

greptile-apps Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Too many files changed for review (141 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Sep 6, 2026 10:46am UTC

Request Review

@github-actions github-actions Bot added the requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

⚠️ Cross-repo companion check

One or more companion PRs aren't merged into main yet (aggregated across the feature PRs in this release). Merging this without them will leave copilot and sim out of sync — merge them in lockstep.

  • ⚠️ simstudioai/mothership#479 — merged into staging (this PR targets main) — feat(browser): improve navigation and form tool guidance

…hs (#7561)

* test(comparisons): cover the split-table and prose-link rendering paths

Follow-up to #7560, which shipped the seven-table split without automated
coverage. Guards the regressions that split makes possible: a silently
dropped fact group, a section heading whose id no longer pairs with its
aria-labelledby, a table label that stops distinguishing the seven tables,
and a prose link that loses its external hardening or stops routing an
internal path through Next.

Covers one profile with every optional prose field and one with none.
Each assertion was verified red against a mutated build before landing.

* test(comparisons): assert the prose bodies render, not just their headings

The section-presence assertions checked the verdict heading and its id but
never the rendered prose, so they passed against a build that emitted an
empty lead answer, verdict, or section intro. Assert the text itself,
derived from the profile data, and assert its absence on a profile that
supplies none. Verified red against a build that keeps the headings and
empties the bodies.
@waleedlatif1
waleedlatif1 merged commit 477dcb2 into main Sep 6, 2026
48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires-mothership-merge Has a companion PR on the mothership/copilot side — merge in lockstep

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants