Skip to content

v0.8.13: cli enhancements, desktop app selfhosting, isolated vm version bump - #7150

Open
waleedlatif1 wants to merge 29 commits into
mainfrom
staging
Open

v0.8.13: cli enhancements, desktop app selfhosting, isolated vm version bump#7150
waleedlatif1 wants to merge 29 commits into
mainfrom
staging

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

icecrasher321 and others added 22 commits August 26, 2026 14:05
* fix(chat): resolve a caller-supplied conversation id through its owner

The v2 chat route used the caller-supplied conversationId verbatim, with no
existence, owner, or workspace check, against a store keyed by bare text with
no owner column. A caller who knew another user's conversation id reached that
conversation. Ids now resolve through the same owner-scoped loader the web chat
path uses, and anything unresolvable answers one uniform 404 before any
lifecycle work runs. Omitting the id mints a server-issued conversation.

The contract also accepted any 1-128 character string for a column typed uuid,
so a malformed id raised a driver error and rendered 500 while an unknown but
well-formed id rendered 404 - a shape oracle, and a 500 on ordinary input.

The ownership predicate had no coverage anywhere: the route test mocked the
module and the lifecycle test drove a chain mock that ignores its where clause,
so deleting the owner condition left both suites green. It is now asserted by
composition and by condition count, which is what catches a dropped condition.

Also renames the reply's model identifier away from a term the project's own
copy rules forbid on a user-facing surface.

* fix(v2): conceal workspace absence, and stop archived tables faulting their page

Two reads answered a caller more than they were entitled to know.

A workspace a caller cannot reach at all returned FORBIDDEN while one that does
not exist returned NOT_FOUND, so a workspace-key holder could enumerate which
workspace ids exist by diffing the two. Both now answer the same absence, using
the concealment policy the billing routes already use. A refusal from inside the
workspace - a member whose role is too low - still answers FORBIDDEN, because
that caller already knows the workspace exists.

Separately, archiving a folder cascades onto its tables but leaves each table
pointing at the archived folder row. The archived listing resolved those paths
strictly, so one such row faulted the whole page and no cursor could step past
it - which also made the ids undiscoverable and left restore unreachable for
exactly the tables that need it. The archived scope now resolves leniently to
the root, where a restore would place them, matching the shipped workflows
behavior. Active listings still fault loudly on a dangling folder.

* fix(knowledge): validate upload processing options without stranding live sessions

recipe and lang were accepted as free strings up to their length caps, silently
discarded, and echoed back nowhere, so a typo was unobservable: uploading with a
misspelled recipe returned 200 and quietly used the default. Both are now
validated at the boundary and a bad value answers 400 naming what is accepted.

The accepted recipe set deliberately includes the sentinel every first-party
caller sends today alongside the three real chunker recipes, and the three are
derived from the chunker's own union so removing one there is a compile error
here rather than a silent 400 in production.

The same schema also parses metadata read back off a persisted upload session,
so tightening it would have thrown out of resume and complete for any session
created before this - a 500 on work that could then never finish. The read-back
path now drops a value it no longer recognises instead of rejecting it; the
request boundary stays strict.

Neither field reaches chunking, so nothing here moves chunk boundaries,
embeddings, or search results.

* fix(v2): honour a requested stats window, and answer a claimed graph id with a conflict

Log statistics accepted a start and an end, filtered the totals by them, and
then built the series against wall-clock now. Bucket width was computed over a
span the caller never asked for, and every bucket past the requested end was
structurally empty - so a bounded historical query returned a wrong-width series
with fabricated trailing buckets, under a window label that disagreed with the
request. Each edge now honours the bound it was given and keeps its previous
derivation when omitted, so an unbounded request is unchanged.

Separately, block, edge and subflow ids are global primary keys while the
delete that precedes a state replace is scoped to one workflow. An id owned by
another workflow survived that delete, the insert violated the key, and because
callers pass their own transaction the driver error escaped unclassified as a
server fault. The write now refuses such an id up front with a conflict naming
it, and re-classifies the same violation if one races past the check, since the
lock covers only the workflow being written. The dry run checks the ids a commit
would insert and reports the warnings a commit would report, which is what its
own contract already promised.

* fix(secrets): let a workspace secret change its metadata without resending the value

Restoring redaction cost more than removing it. The only way to flip a secret
back to redacted was to re-send the plaintext, because the write required a
value and omitting it fell into an interactive prompt that cannot run in CI.
A workspace secret can now change its description or visibility on its own; the
stored value is never re-encrypted or rewritten, a write that names no existing
secret answers not-found rather than creating one, and a personal secret still
requires a value because it has no other writable field.

The path parameter was also one shared schema across the write and the delete,
so a single description had to cover both and the delete documented an argument
that could create and replace. Split, mirroring the credentials pair.

The metadata write is a new update against the credentials table, so its scope
is asserted by composition and by condition count: an unscoped update would let
one workspace flip another workspace's identically-named secret out of
redaction, and the cache invalidation would then carry that flag into the other
workspace's runtime catalog.

* fix(v2): say what an error means in terms the caller can act on

A size-limit refusal collapsed every value under a kilobyte to "0 Bytes", so a
28-byte file over a 27-byte ceiling read "is 0 Bytes, above the 0 Bytes limit" -
self-contradictory, and useless for choosing a value that would work.

Errors and field descriptions also told callers to invoke raw HTTP endpoints.
These strings serve the REST reference and the CLI's own help equally, so they
now name the operation and its object rather than a method and a path. A sweep
test walks every v2 schema description and holds the line, with the remaining
offenders in files this change does not own recorded explicitly rather than
left to be rediscovered.

Listing the editors of a built-in skill claimed the skill did not exist, while
reading the same id succeeded - a well-formed request for a real resource is
not malformed, so the list answers an empty roster and only the mutations
refuse.

Bulk folder deletion recorded only the leaf name in its audit trail while the
single delete recorded the full path, leaving two same-named folders under
different parents indistinguishable after the fact.

Bulk chunk enable, disable and delete each treated an unmatched id differently
behind one sentence of documentation. They now follow one rule.

A workspace-scoped list refused with the name of a resource the caller never
addressed, which reads as an empty workspace rather than an unreachable one.

* fix(cli): stop a config value forging a section it was never meant to write

The config file is written by joining names and values into INI lines, and
nothing checked what was in them. A profile name carrying a newline and a
section header wrote a section that merged into a different profile and took
over its endpoint - and the next command sent that profile's stored API key
there. A workspace value could do the same from the other side, since only the
endpoint flag validated its input.

The refusal now lives at the writer, the single place untrusted text enters the
document, with the flag-level checks kept for the better message. Either alone
blocks the forgery; the pair is deliberate.

Rejecting rather than escaping, because the format has no escape syntax and
these files are hand-edited and read by other tools that would not decode one
we invented. The forbidden set covers control characters and the two Unicode
line separators, which the previous guard missed - those parse as an unreadable
line, so the key silently vanished on read and the next write appended a
duplicate while the command reported success.

Login also wrote the key before the settings, so a malformed response from the
deployment could leave a key on disk with no endpoint beside it, and the next
command would send it to the default host. Settings are written first, and the
response is checked before anything touches disk.

Name validation applies only when creating a profile, so a hand-written one
that predates the rule keeps working.

* fix(docs): tell the reader which key a command needs, and stop the ids contradicting the CLI

Around sixty v2 operations refuse a workspace API key, and the CLI's help said
nothing about it - the caller found out from a 403 after the request went out.
The restriction is already stated in the API spec, so the generator now reads it
from there and the command description carries it. The sentinel sentences are
imported from the spec's own constants rather than copied, so a reword cannot
silently unmark every command, and the test pins the count as well as named
operations because a reword confined to one family would otherwise slip past.

The generated reference also rendered an empty default as a sentence pointing at
nothing - "Defaults to ." - for every repeatable filter. Omitted now, while
false and zero still render, which is the trap that shape of check usually
walks into.

The hand-written guides used a workflow-shaped id for workflows that the CLI's
own help says never names one, and five other families were equally wrong. All
of them now match the scheme the CLI declares, consistently per entity across
pages, with the shared ones taken from that help text so the two read as one
voice.

The page documenting every flag was linked from nowhere; both landing links
pointed at the overview instead. And the generator's test file was absent from
the hand-maintained list CI runs, so its guards never executed.

* fix(cli): stop a page-size default capping a destructive filter

Every request field named limit inherited the pager's default of 100, but only
a cursor-paginated command interprets that flag. The two filter-based row
mutations declare no cursor, so the default went onto the wire as a row cap: a
filter matching 250 rows deleted 100, exited 0, and said nothing - while the
confirmation the user had just answered promised every matching row. The flag's
own help offered 0 for everything, which those endpoints reject; the unbounded
form is the field being absent. The pager's default now applies only where the
pager runs, and the tests pin the omission on the request body rather than in
help text.

A cap typed alongside an explicit row list was silently ignored; it is now
refused on the client, where refusing costs nothing to already-installed
versions.

Lists also truncated at a hundred with no signal in any format, and the two
inventory endpoints that do report truncation had that field dropped on the way
out - so a caller reconciling against a clipped list could not tell. One note
now goes to stderr while stdout stays a bare array, and a flag raised on a later
page survives the fold.

Also: a folder whose name contains the separator no longer prints a path that
resolves to a different folder; validation errors name the flag the user typed
instead of the wire field; an unknown subcommand with --help exits non-zero
instead of printing the parent's help; a fractional or negative page size is
refused rather than floored; an empty query filter is refused rather than
silently returning everything; and the two spellings of the missing-workspace
message became one.

* fix(cli): gate destructive table imports and fix follow-mode rendering

A `tables import --mode replace` empties the table before its first batch,
so the only warning was in the describe. It now confirms, and the wording
tells the truth per mode: cancelling a replace leaves a prefix of the new
file with the originals already gone, while an append re-adds its rows if
the file is imported twice. `--yes` skips the gate, and the gate runs
before the file is opened.

Import and export cancellation carried no describe at all; the import one
now confirms, the export one records why it deliberately does not.

Follow-mode output truncated cells to whatever the first row happened to
measure, so a longer status or workflow name arrived clipped with no
signal. Cells now clamp at a shared ceiling and pad to the lock, and the
log columns carry width floors so a short first page cannot pin a column
narrower than its own values.

Interrupting a staged download left the staging directory behind; it is
now removed on SIGINT and SIGTERM before the signal is re-raised.

`--select-output` without `--follow` selected from a response that does
not carry outputs, and said nothing. It is refused client-side, with a
separate message for `--async`. Its describe now names what the path
addresses.

`secrets set` always read a value, even when only metadata flags were
passed. Off a TTY that was an immediate refusal, so a metadata-only edit
exited 1 in CI for a value it was never asked for; on a TTY it stopped to
prompt, and the prompt rejects an empty entry, so there was no way to say
"leave the stored value alone" short of re-typing the secret. The read is
now skipped and the field omitted, which is what lets a metadata-only edit
run unattended. On a TTY, setting only a description no longer prompts.
Passing both spellings of the reveal flag is refused rather than silently
resolved.

Four mandatory hand-authored flags now say so, `billing logs` names its
key-type scope, and the dispatch list declares its columns.

* fix: close the gaps an adversarial review of this branch found

A conflict handler added earlier in this branch was dead code. It read the
Postgres error code off the thrown object, but the driver error arrives
wrapped with the real one on `cause`, so the check returned false on its
first line and the 409 never fired. Its test passed only because it threw a
flat shape production never produces. It now reads through the cause chain
with the shared helpers, compares the constraint name exactly instead of
matching a substring of the SQL, and its test throws the real wrapped error.

Resuming a conversation checked its workflow and its workspace but not its
type, so a conversation created by the web surface could be continued as a
CLI turn. It now refuses through the same uniform 404 as every other
mismatch, which closes the same omission on the web posting path. Minting
one no longer leaves a blank untitled row at the top of the Chat list.

The pre-write check on a minted API key refused fewer characters than the
writer does, so a key the check accepted could still fail at the write —
after the endpoint beside it was already stored, pairing a new endpoint with
the previous key. The two had drifted because the set was spelled three
times; there is now one.

A description claimed a processed count reported only the chunks that
changed. The update returns every row it matched, so re-enabling chunks that
were already enabled counts them all. Two OpenAPI sentences promised no
conflict detection and no persistence warnings in a dry run, both of which
the same branch had just made false. A described window was wrong whenever a
start was supplied without an end.

Listing the editors of a built-in skill answered a read with a modification
refusal on the internal surface. Archived table listings could reach the
strict folder projector again through a third scope value the input type
still allowed. A metadata-only secret write skipped the guard its
personal-scope twin has. The internal document boundary still took the two
processing fields as unbounded strings. Truncation was reported only from
the response envelope, so a clipped file body, row search and workflow-stats
list said nothing.

A staged download stopped watching for signals before it finished removing
its directory, and cleared every listener for the signal rather than its own.
Three tests asserted a contract constant against itself; they now drive
rendered help, real argv, or real render output.

* chore: regenerate the API reference, CLI surface, and CLI docs

The published reference still marked a secret value required and described
the delete parameter as one that also creates, the CLI surface still lacked
the marker that says which operations refuse a workspace key, and the
reference rendered an empty sentence for every repeatable filter whose
default is an empty list.

* test(cli): use the package's own delay helper in the staging poll

The audit bans a hand-rolled setTimeout promise. `sim-cli` does not depend
on the shared utils package, and its own idiom is `node:timers/promises`.

* fix: act on a second review round, and correct two earlier claims

The conflict pre-check read block ids from the wrong side. The writer
inserts each block's own `id` field while the check read the record key,
and the two can diverge because preparation copies a value under its key
without reconciling them. Edges already read the value and subflows are
genuinely keyed by the record key, so only blocks were wrong — collecting
every family from the values, as first suggested, would have broken
subflows instead.

A minted API key carrying leading or trailing whitespace passed the
pre-write check but failed the writer, leaving the new endpoint on disk
beside the previous key. It is refused up front now rather than trimmed: a
key is opaque, so trimming would store a value the server never issued and
turn a loud failure into an unexplained 401 later. The endpoint normalizer
does trim, which is what made a padded `--endpoint` fail only after the
browser flow had already minted a key.

A metadata-only secret write raced with deletion returned 500, because the
follow-up read that only assembles the response body threw an unclassified
error; it now reports the same not-found the non-racing miss already gave.
An unusable output format in the environment silently printed a table
instead of refusing. Two validation messages printed control characters
verbatim. A dry run now reports the preparation warnings its own commit
path returns.

The chat route created a titled conversation and never wrote a message, so
it appeared in the Chat list promising content it did not have. Both sides
of a successful turn are now persisted; a failed turn still writes nothing,
so a question is never stored without its answer.

Two claims of mine were wrong. The earlier commit message said `secrets
set` sent an empty value that overwrote the stored secret — it did not; the
prompt refuses off a TTY and rejects empty on one, so the old behaviour was
a clean refusal. And the delay helper commit said this package's idiom is
`node:timers/promises`; the package carries its own `sleep`, which is the
audit's sanctioned home and has five callers. It uses that now.

Also: a test asserting a deadlock stays unclassified could not fail, since
every candidate rejects it; it now pins a unique violation carrying no
constraint name. Workflow ids spelled with the file prefix are corrected in
the remaining fixtures, leaving the genuine file ids alone.

* fix: close a credential-misdirection path this branch had opened

Making the endpoint normalizer trim handled whitespace around a value but
not a control character inside one, and the URL parser removes those from
anywhere in its input — so a value that reads as one host could resolve to
another, and the profile's key went with it. The flag and environment paths
never touch the config writer, so its guard did not cover this. The
normalizer now refuses the same character set the writer does, which also
keeps the invariant that nothing it blesses can be refused by the write
that stores it. Comparing the parsed URL back against its input was the
alternative and is wrong: the parser rewrites percent-encoding, case,
internationalized hosts and default ports, so legitimate endpoints would be
refused.

The blank-query guard tested for exactly empty, so a whitespace-only value
still reached the wire — as a real zero on a numeric filter, an explicit
false on a boolean one, and as an encoded space the server then rejected.
It now refuses any value that is blank once trimmed, while a body string
keeps its meaning, an explicit zero still sends, and a value with content
around its whitespace is passed through untouched rather than trimmed.

A graph-id conflict reported 409 on the v2 route and fell through the older
persistence wrapper as an unclassified 500. That wrapper now classifies
orchestration failures through the cause chain, which also fixes a
pre-existing case where a workflow archived between authorization and the
locked read reported 500 rather than 404.

Persisting a chat turn claimed its row by id alone, so a conversation
soft-deleted mid-turn still received the messages and was bumped back up
the list. It now requires a live row. A turn whose caller hung up after the
model had already answered persisted nothing, though the work was done and
billed; it now persists and still reports the connection as closed.

An empty workspace id from the login response was read as no workspace at
all. A published description still promised a language-tag standard the
schema does not enforce.

The test asserting that a turn is stored before the final event drained the
whole response first, so it held whichever order the code used. It now
reads the stream incrementally and fails if the write moves after the
event.
* fix(landing): stabilize previews across browsers

* chore(landing): use absolute stage export
* refactor(billing): retire protocol rollout flags

* refactor(copilot): drop unused steering billing metadata
* fix(connectors): fail closed on incomplete SharePoint traversal

* fix(knowledge): enforce OCR request limits by bytes and pages

* fix(knowledge): retain OCR policy literals
Co-authored-by: Sim Pi Agent <pi@sim.ai>
* fix(landing): harden cross-browser compatibility

* fix(landing): scope loader compatibility token

* fix(landing): blur legacy inert focus
* docs(blog): update enterprise

* revert: restore original authors and tags on blog/enterprise

---------

Co-authored-by: Sim Pi Agent <pi@sim.ai>
* feat(desktop,cli): support self-hosted desktop installs

The desktop shell was already origin-agnostic at runtime — navigation, CSP,
cookie partition, and the update feed all derive from the configured origin,
and every deployment already serves /api/desktop/update/download and its
updater manifest. The one thing missing was a way to change that origin:
ConfigStore.setOrigin had no IPC channel, menu item, or UI behind it, so a
self-hoster installing the signed build was stuck on the baked default.

Adds the native server picker (Sim → Server…, plus a "Change server" button on
the offline page, since a shell pointed at an unreachable origin lands there
with nothing else to click). Its IPC family is gated to bundled file: senders:
the surface that repoints the shell must keep working when the current server
cannot be reached, and must never be drivable by a page that server serves.
A confirmed change relaunches rather than swapping in place — the origin keys
the cookie partition, update feed, encrypted per-origin task state, and every
live browser view and PTY.

Adds `sim-setup desktop`, which resolves the installer from the operator's own
deployment, checks that the update feed resolves too, and prints the server URL
to paste in. Documents the whole path under self-hosting, including the
build-your-own escape hatch for organizations with their own Developer ID.

* refactor(desktop,cli): review pass on self-hosted desktop support

Two real defects found while auditing the change for hardcoded assumptions.

`lastRoute` is a single global setting that carries a workspace id, so it
survived an origin change and opened /workspace/<old-id> on the new server.
resolveStartRoute cannot rescue that — it discards a route only on a confirmed
403, and a fresh partition draws a 401. Cleared on change, via a named list
that is now the documented home for deployment-scoped settings; the agent
browser's jar and its known-sites metadata are deliberately kept, together,
since changing deployments does not imply the account changed.

The offline page's "Check status" sent self-hosters to status.sim.ai, which
reports on Sim's deployments and is always green for theirs. Withheld for a
non-sim.ai origin, as is the same link in the Help menu, through one
isSimCloudOrigin predicate. Hiding it needed `button[hidden]{display:none}`:
the page's own `button{display:inline-flex}` is an author rule and outranks the
UA `[hidden]`, so the attribute alone left it rendering. The e2e offline test
now asserts the whole path, which covers the `server:` local-page IPC gate.

Review cleanups: setOrigin no longer rewrites settings when handed the origin
it already stores; the picker window installs a permission handler and pre-
paints its background like every other window, and its page is theme-aware so
that background is not a flash; the CLI reuses httpHealth and the
cross-platform openBrowser instead of reimplementing both, skips Compose/Helm
discovery when --url makes it dead, and folds two parallel switches into one
exhaustive one. Value-flag parsing is now one helper instead of a third copy.

* fix(desktop,cli): scope deployment capabilities to their origin

Changing the server left two device-global stores in place that grant the
INCOMING deployment authority the user only handed the outgoing one: local
filesystem grants (directories its agent may read, plus security-scoped
bookmarks) and the agent browser's cookie jar (live third-party sessions its
agent may drive). Sign-out clears exactly this pair; an origin change is the
same boundary, so it now clears it too — awaited before the relaunch, since a
quit racing an async clear could leave either behind. browserKnownSites goes
with the jar it describes, so Sim is never left believing in sign-ins the
profile no longer has.

The CLI printed the redirect's filename straight to the terminal. It is read
out of a Location the deployment chose, so percent-encoded ANSI or OSC survives
decodeURIComponent as real control bytes and could forge CLI output; control
characters are stripped and the name is bounded before it reaches the spinner.

resolveDeploymentUrl took the first source naming an app URL. A machine with
both a local checkout and a real deployment would be probed, printed, and
opened at whichever enumerated first, silently — so disagreeing sources are now
an error naming each candidate and asking for --url, the way
resolveFeatureSetupDestination already refuses ambiguity.

* fix(desktop,cli): fail closed on origin change, widen terminal sanitizer

The capability teardown could partially fail and still let the shell move.
Sequential awaits meant a filesystem-grant rejection skipped the browser-profile
clear entirely, and the new origin was already persisted by then, so the
incoming deployment inherited whatever survived — and startup restores it.

Now the two stores clear independently via allSettled and report which ones
survived, and the whole teardown runs BEFORE anything is written. A store that
cannot be emptied refuses the change outright and names what it could not
clear. Nothing is persisted at that point, so refusing leaves the shell exactly
where it was rather than half-applying. Validation moved up front for the same
reason: a typo now costs no teardown.

The terminal sanitizer matched only C0/DEL/C1 by range, so percent-encoded bidi
overrides and isolates survived decodeURIComponent and could still reorder what
the reader sees without emitting one control byte. Matched by Unicode class
instead — Cc covers the cursor controls, Cf covers the bidi ones.

Configuration discovery compared raw strings, so a trailing slash, a default
port, a host-case difference, or an ignored path read as two different servers
and demanded a --url override to settle an ambiguity that did not exist. Now
compared on the parsed origin, which is what the command ends up using.

* fix(cli): sanitize the installer name by Unicode group, not by escape list

U+2028 and U+2029 are Zl/Zp, so the Cc/Cf filter let them through and a
deployment-controlled redirect filename could still forge a status line.

Enumerating what to strip had cost a patch per class found — C0 and C1, then
the bidi overrides, now the line separators — so this keeps whole groups
instead. `C` removes every control, format, surrogate, private-use, and
unassigned code point, covering ESC and OSC, the bidi overrides and isolates,
zero-width characters, and the BOM; `Z` removes every space, line, and
paragraph separator. Separators become a plain space rather than vanishing so
a name is not run together at the seam, and runs are collapsed so the result
cannot be padded to push text off the line.

The regression table now names each class that reached the terminal in an
earlier round, so a future bypass says which one came back.

* fix(desktop): serialize server changes and report partial teardown honestly

Two problems in the same transaction.

The picker re-enabled Connect whenever the field changed, including while a
request was in flight, so typing and pressing Enter could start a second change
that interleaved its teardown and its write with the first — the later write,
not a single transition, deciding the next server. The transaction is now
serialized in the main process, the way the sign-out coordinator guards its
own teardown, since the IPC boundary is reachable regardless of what the page
does; the page keeps its button disabled for the whole request so it never asks
for something it will only be refused.

The stores clear independently, so one can succeed while the other fails. There
is nothing to roll back to — a revoked cookie jar and deleted security-scoped
bookmarks cannot be un-deleted — and moving anyway would hand the incoming
deployment whatever survived. So the change is still refused, but the message
no longer names only the failed store as though nothing else had happened: it
says some local access may already have been cleared, and that retrying
finishes the job. Clearing an already-empty store succeeds, so a retry is safe.
* chore: run the orphaned migration-safety test, and stop publishing real-looking ids

One of the eight script tests was reachable from no entrypoint, so it had
never run in CI — it passes, it was simply never invoked. This is the second
time that hand-maintained list has drifted from the files beside it; the
audit runner's own header records the first. The guard against a third is a
`check:*` script rather than a test, because the runner derives its list
from that namespace and so picks the guard up by name — a test would have
had to be hand-added to the very list it guards.

A published spec sat outside the generator's manifest and so outside its
drift check, and carried six example ids with the texture of real generated
ones rather than the pandigital placeholders the rest of the repo uses. The
new check globs the directory instead of reading the manifest, since the
manifest is what omitted the file. The one pre-existing borderline id is
allowlisted with a reason: loosening the threshold to admit it would have
hidden one of the six.

* fix(cli): close the gaps black-box testing the shipped CLI found

`sim profiles <anything>` still exited 0, so a probe reading the exit code
to ask whether a command exists was told yes — the one group the earlier
guard missed. The exemption was written for commands that are both a group
and a leaf, but only `files restore` takes an operand; `profiles` takes
none. Registering its listing as a default subcommand puts it back among
the pure dispatchers the existing guard already covers, so the guard itself
did not need widening.

Three commands refuse a workspace API key and said nothing, while their
menu siblings said so — reading as though they accept one. They are
hand-written, so they never reached the code that appends the note. That
note now comes from a helper taking the operation, so a command names the
operation it invokes and the two cannot disagree, and a test fails if a
hand-written command ever calls a restricted operation without it.

A blank numeric value in a request body still became a real zero, the same
coercion already fixed for query strings: the guard keyed off the slot when
the distinction is the field's declared type. Twenty-one fields across
fifteen operations were affected. An empty body string still clears a
description.

Blank values for the root endpoint, workspace and profile flags fell back
to what was configured instead of being refused, and a whitespace workspace
was accepted verbatim. A hand-written profile name carrying padding listed
as reachable but resolved to defaults rather than erroring. Two schema
descriptions named request fields that no flag spells, and a rejected value
was echoed unredacted by four messages while their siblings redacted it.

A write now re-emits a section header it was not asked to touch byte for
byte. The blank-line normalisation around it is left alone: making the
writer position-faithful is a change to its model, not a fix.

* fix(scripts): match example uuids case-insensitively in the spec audit

The pattern only recognised lowercase hex, so an uppercase id in a
published spec was never examined and the audit reported success without
having looked at it.

Matching case-insensitively is not enough on its own: hex is
case-insensitive, so a mixed-case id counts `A` and `a` as two digits and
reports twenty distinct ones rather than sixteen. That inflated count
clears the threshold the texture test uses to recognise a hand-authored
placeholder, so a real id could have passed for one. The allowlist is an
exact-string lookup and would likewise have missed an uppercase spelling of
an entry. Both checks and the lookup now take a normalised id, while the
finding still reports the spelling as it appears in the file.

* fix(cli): stop a refusal being swallowed, and gate example ids by name

A blank root flag was refused everywhere except `profiles`, where the catch
that lets a broken profile still list absorbed it and the command exited 0
after printing the table. The refusal now carries its own error class, which
is what the listing rethrows on — the two are distinguished by type rather
than by matching message text, and a genuinely broken profile still lists.

The unknown-profile message redacted the name the caller typed but not the
suggestion or the list of configured names beside it, which come from the
same file and are equally attacker-influenced once it has been hand-edited.
Those are redacted now, as is every other message in these two files that
quotes a name read out of the config, and the profile listing flattens the
names it renders the way it already flattened the error column.

The example-id audit judged a uuid by its digit texture, on the premise that
a real one essentially never looks hand-authored. Measured against ten
million generated ids, 0.81% of them do — one in 124, where this change
alone replaced six. Requiring each digit exactly twice takes that to zero
but rejects all fourteen placeholders now in the specs, so it is no cheaper
than the alternative. The audit now holds the eighteen ids the specs
actually use, which is one file rather than the twenty-seven a reserved
format would touch, and a new id fails until someone lists it — which is the
review the check exists to force.

* fix(scripts): match the uuid sentinels exactly rather than by shape

Accepting any id built from at most two distinct hex digits let something
through that was never on the approved list. A generated id essentially
never has that shape, so the practical risk was small — but this check had
just stopped being a shape test and become a list, and a structural
exception is the one thing that undoes that. The two ids it exists for are
the nil and max sentinels, and both are matched by value now.
* fix(desktop-browser): add recoverable page failure states

* fix(desktop-browser): align recovery interaction paths

* fix(desktop-browser): expire stale recovery history
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
* feat(chat): add live voice input waveform

* fix(desktop): hide unsupported microphone settings action
…drop (#7134)

* improvement(db): stop declaring retired usage columns ahead of their drop

Contract-prep for the legacy usage-column drop. Removes the drizzle
declarations for 19 retired user_stats counters,
organization.departed_member_usage, and workflow_execution_logs.cost, so
generated SELECT lists stop referencing them, plus the last incidental
references: the cycle-close departed_member_usage resets, the v1 admin
exposure of departedMemberUsage/currentPeriodCopilotCost (both serving
dead values since the ledger cutover), the testing schema mirror, and a
lock-order fixture. The DROP COLUMN migration ships separately once this
is the deployed production app; contract-pending markers in schema.ts
track it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(db): reserve the drop migration as a journaled no-op for schema parity

CI requires drizzle-kit generate to be a no-op on every PR, so the
declaration removal cannot ship without its migration. Committing the
generated 0309 with every DROP commented out (a SELECT 1 no-op) keeps
the parity gate and future generates clean while deferring the
destructive half until this change is part of an already-deployed
production release; the file documents the follow-up procedure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* improvement(db): keep declarations, route reads through live-column maps

Restructures the contract prep per review: the no-op 0309 migration is
gone and schema.ts keeps the deprecated declarations (with the
contract-pending markers on the columns), so the follow-up PR ships the
declaration removal and the real generated DROP migration together. What
makes that safe is that no deployed query names the doomed columns
anymore: every argless select()/returning()/relational read of
user_stats, organization, and workflow_execution_logs (29 sites) now
goes through omit(getTableColumns(...)) live-column maps, following the
workspaceFileColumns precedent. scripts/check-pending-drop-tables.ts
enforces this — it derives the guarded tables from the contract-pending
markers themselves (picking up workspace_files' pending size drop too)
and fails CI on any argless read, so the invariant holds between the two
PRs and retires itself when the markers are deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(db): backfill residual cost_total projections before the cost drop

The 0220 procedure projected every then-existing legacy cost json into
cost_total, but a transition-window writer added 23 rows (all 2026-05-30,
verified on the prod replica) carrying a numeric json total with no
projection. Script migration 0009 re-runs 0220's exact candidate filter
and projection in bounded batches at deploy time, so the pending cost
DROP abandons nothing cost_total should hold. The contract PR that drops
the column must deregister the script in the same change — it reads the
column; the contract-pending marker says so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): validate live-column builders and inline aliases in the pending-drop audit

Hardens check-pending-drop-tables per review: the audit now derives each
guarded table's doomed columns from schema.ts (@deprecated tags and
column-level contract-pending markers), so a sanctioned
omit(getTableColumns(t), [...]) or destructure-omit must name every
doomed column away — an unrelated or incomplete key list is reported
with the columns it misses, and schema.ts itself is no longer skipped,
so the <table>Columns helpers fail the audit if a later deprecation is
not added to their omit lists. Inline alias(t, ...) expressions passed
directly to from()/getTableColumns()/insert/update/delete now resolve to
their canonical table instead of escaping the checks. The testing
mirror's *Columns aliases now omit the same doomed keys as the real
helpers (and gain the missing proPeriodCostSnapshotAt entry), so tests
cannot keep passing against doomed-column references the app no longer
serves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): derive pending-drop columns from the AST, and align the mock schema

The doomed-column derivation matched `@deprecated` with a line regex, so a
column documented with a multiline TSDoc block was never marked doomed and
the live-column helpers were free to omit nothing for it. Derivation now
runs off the parsed schema: guarded tables are pgTable calls whose enclosed
comments carry a drop-flavored contract-pending marker, and doomed columns
come from each property's own leading TSDoc block, so single-line and
multiline forms read alike. Verified by mutation: making a deprecated
column's TSDoc multiline and dropping it from the helper's omit list now
fails with the column named, where the previous version reported clean.

Also fills the seven live columns the testing mirror was missing
(user_stats.limit_notifications, four organization settings columns,
workflow_execution_logs.cost_total/models_used) so mock-derived selections
have the same shape as production ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): resolve namespace and renamed imports in the pending-drop audit

Table references were matched by bare identifier only, so an argless read
reached through `import * as schema` — a live pattern here, e.g.
`db.insert(schema.userStats)` in lib/auth/anonymous.ts — or through a
renamed import was invisible to the audit. Resolution now runs over the
file's schema-module import bindings (named, renamed, and namespace) as
well as alias() bindings, at every position the audit inspects. Verified
by probe: argless select() via `schema.userStats`, via a renamed import,
and an argless .returning() on `db.insert(schema.userStats)` are all
reported now, where the previous version reported none of the three.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): close the pending-drop resolver under variable assignment

A table parked in an ordinary binding (`const t = userStats`) escaped the
audit, since only alias() results were re-bound. Binding collection now
resolves any declarator whose right side names a table — the table
itself, a namespace member, an earlier binding, or an alias() call — and
repeats to a fixpoint so declaration order does not matter. Verified by
probe: argless reads through a direct assignment, a namespace-member
assignment, and a chained binding are all reported now, where the
previous version reported none; the repo stays clean, including a file
where a local array shadows a pending table's name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): match relational reads by table name, not by a literal query chain

An argless findFirst/findMany reached through a binding that holds
db.query escaped the audit, because the check required the receiver to be
a literal `<x>.query` member. Drizzle's relational API is keyed by table
name however the root is reached, so the check now keys on the
table-named property itself, closing the whole class rather than the one
aliasing form. Verified by probe: an aliased root (`const q = db.query`)
is reported now where it previously was not, `tx.query` still is, a read
that passes `columns` still is not, and the repo stays clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): preserve resource view collapse state

* fix(ui): follow resources after selection removal

* fix(ui): synchronize resource selection updates
* fix(ui): optimize universal paste handling

* fix(ui): keep paste limits as safety hatches

* improvement(ui): reduce large-paste admission overhead

* fix(ui): preserve large context paste behavior
@waleedlatif1
waleedlatif1 requested a review from a team as a code owner August 27, 2026 04:04
@greptile-apps

greptile-apps Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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

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

@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 27, 2026 6:43am

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 352 files

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Re-trigger cubic

Comment thread apps/sim/lib/secrets/application/use-cases.ts
Comment thread packages/sim-cli/src/commands/configure.ts Outdated
Comment thread apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx Outdated
Comment thread apps/sim/lib/copilot/chat/lifecycle.ts
Comment thread apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx Outdated
Comment thread apps/sim/app/_shell/paste-admission-guard.tsx Outdated
Comment thread apps/sim/app/_shell/paste-admission-guard.tsx Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

The voice-input change in this release PR is relevant to the blocked-permission UX. Browsers cannot reopen a microphone permission prompt after a user has blocked it, so #7155 adds a browser-only “Show steps” recovery action with concise site-permission instructions. Supported desktop shells keep the native “Open Settings” action, and unsupported desktop platforms do not receive a misleading browser action.

* fix(chat): guide blocked browser microphone access

* fix(ui): align microphone permission modal
… requirement (#7158)

* docs(self-hosting): correct what a server change clears, and the cert requirement

The page said the built-in browser's profile survives a server change. It does
not — the teardown clears the browser's saved sessions and the agent's folder
grants along with the saved route, because those are capabilities granted to a
specific deployment. The page now lists what is cleared and what is kept, says
why, and notes that a change which cannot complete is refused rather than
half-applied.

Adds two things a self-hoster hits in practice. Certificate errors are rejected
outright with no "continue anyway", so a private CA that is not in the system
trust store will not load however correct the URL is — worth saying, since a
private CA is a normal self-host setup. And packaging your own shell needs
Xcode 26 or newer, which otherwise fails with an opaque actool error.

Also notes that the CLI asks which deployment you mean when a machine has more
than one configuration, and states signing/notarization for a self-built shell
as a requirement rather than predicting what happens without credentials.

* docs(self-hosting): point custom builds at package:mac, not package:share

package:share is the "send someone a build to try" path. It passes
-c.mac.timestamp=none to skip the per-file round trip to Apple's timestamp
authority, and its own docstring notes distribution builds need those
timestamps. Apple's notary service requires a secure timestamp, so a build made
that way cannot be notarized however many credentials the operator supplies —
which is exactly what the section was telling them to do.

package:mac inherits notarize and hardenedRuntime from electron-builder.yml and
leaves timestamps on, and bun run build honours SIM_DESKTOP_DEFAULT_ORIGIN the
same way, so the baked-origin instruction is unchanged. Its artifact path and
name differ from the share script's per-channel overrides, so those are
corrected too, and the two stacked warnings are merged into one.

* docs(self-hosting): spell out that APPLE_API_KEY is a path to the .p8

The variable holds an absolute filesystem path to the App Store Connect key
file, not the key material, and @electron/notarize reads it through Node fs so
a leading ~ is not expanded — the release workflow carries a comment saying
exactly that. Listed alongside the other credentials with no explanation, it
reads like somewhere to paste the key, and notarization then fails while every
variable looks set.
* fix(ui): close paste admission edge cases

* fix(ui): account for multi-cursor pastes

* fix(ui): preserve rich image pastes

* fix(ui): match distributed multi-cursor paste

* fix(ui): defer exact Monaco paste admission
* fix(cli): refuse what the help already says is invalid

- validate integer fields against the generated spec's own `kind`, so
  `--limit 1.5` names the flag and the value instead of surfacing zod's
  `expected int, received number`
- enforce `--limit` >= 1 on the two non-paginated row mutations, which the
  help already promised and the server already required
- refuse a blank `-c/--conversation`: it is falsy, so it was dropped from
  the body and silently started a new conversation instead of continuing one
- refuse a blank `chat` message before the request rather than after
- constrain `--recipe` to the recipes the generated body type declares, so
  regenerating the surface breaks the build if they diverge
- announce truncation on `ls` the way `list` already does; the capped answer
  was printed silently
- stop a successful message translation from vetoing itself: the veto now
  reads the message the server sent, not the rewritten one, which restores
  `--folder must name an existing folder` and unblocks 100+ operations
- drop "destructive" from the `--yes` help line; the gate also covers
  operations that only add, such as `files unzip`
- fix the `logs follow` help, which illustrated `--workflow` with a file id

* fix(cli): refuse a fraction the integer parse silently drops

Above 2^52 a double's spacing is 1, so Number('4503599627370496.5') is an
integer and the safe-integer guard passed it — the API received a value the
caller never typed. Read the raw text alongside the parsed number. Digits
that are all zero are not a fraction, so 1.0 stays a whole number.

Also corrects a chat test comment that described a UUID check the command
does not perform; it refuses only a blank -c.

* fix(cli): redact the value the root-flag refusal suggests

The refusal prints a command for the caller to run, and interpolated the
value verbatim. A U+2028 in it split the terminal line, so the tail rendered
as a second, plausible-looking suggestion. redact() is what the other twenty
messages in this package already use, including one forty lines below.
The workflow panel's copilot draft key was scoped by workspace + workflow
only. A draft is cleared on submit and never on chat switch, and the scope
key is what remounts the input (`key={draftScopeKey}` in mothership-chat),
so selecting a different copilot chat in the same workflow carried the
previous chat's typed text, contexts, and file attachments into it. Key the
draft on the selected chat as well, matching the home chat.

Two consequences of per-chat keys handled here:
- deleting a chat now prunes its draft, which would otherwise be unreachable
  in persisted storage forever
- drafts persisted under the old workflow-only key are dropped by a store
  migration rather than left as unreadable entries
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants