Skip to content

Promote dev to main (prod promotion) - #1063

Merged
jozer-rami merged 1298 commits into
mainfrom
dev
Oct 12, 2026
Merged

jozer-rami merged 1298 commits into
mainfrom
dev

Conversation

@jozer-rami

Copy link
Copy Markdown
Contributor

Promotes dev → main for the prod deploy (merge commit, not squash).

Dev HEAD 728c49d is the pin set dev deployed and verified (infra pin-status.yaml: verified: true, pinned by infra commit c680f2f, pipeline run 37957369785).

Included PRs

Deploy notes

  • charts/clerum-crds/** changed (2 CRDs): mcpserver (extended MCP OAuth schema, incl. relaxed required fields) and workflowrecipe (description-only default rate-limit text). Expect a CRD pre-apply (gcp-prod-deploy-crds) before mode=apply; mode=diff will confirm.
  • No RBAC or ServiceAccount file changes in deploy/base.

🤖 Generated with Claude Code

alfredolopez80 and others added 30 commits October 6, 2026 12:27
- Each tool-use loop site that injects a nudge or recovery user message
  is driven to inject it, and the message carries the loopInjected
  marker that the last-user-turn lookup skips.
- The marker survives the approval context snapshot, checked through the
  persisted JSON row in the real SQLite store.
- The marker never reaches the OpenAI, Codex subscription or Claude
  request bodies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tice

- The composer image budget check measures each attached image; when one
  holds malformed base64, the new attach is refused with a visible error
  naming that image instead of throwing out of the handler.
- composerImageBudgetBytes has unit tests for canonical and malformed
  base64.
- The Playwright verdict fails a report whose suite tree lists fewer
  specs than its counters, so a truncated report cannot pass.
- The blocked-send step drops a focus check that `press` satisfies by
  itself; the unchanged draft is the witness that Enter was consumed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An UpstreamHostError message carries up to 300 characters of the Host
response body, which can hold user content. Every route and wake-and-hold
log line printed error.message.

describeErrorForLog() logs the error name, the upstream status and the
network error code (sanitised, at most 64 characters), never the message.
Route tests drive each logging path with a body marker and assert it does
not reach the log, with the status line as the witness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extend the dynamic-tool bridge to native tools whose serialized definition
exceeds the new CLERUM_NATIVE_TOOL_DISCOVERY_BYTES budget (default 2048, 0
disables). Deferred natives stay searchable and describable via the
discovery catalog and callable through clerum__tool_call; bridge-to-bridge
recursion stays forbidden (LOCKED #11 amendment).

Measured on this HEAD, 5 of 7 document generators defer (pdf, xlsx, chart,
pptx, dashboard), removing ~18.5 KB of schema payload per model call while
markdown (401 B) and docx (2010 B) remain advertised.

Refs #1003
Control API
- Bound JWT verification per source IP on both Plugin Workload SDK routers
  before any limiter that verifies to pick a bucket. Only bearer requests
  count; the budget equals the verified SDK pre-auth allowance (6000/min),
  so invalid tokens still stop at IP600 and further tokens from a flooded
  IP are denied without being verified.
- Add an authenticated wiring test for the subscription capabilities route.

Control UI
- Stop rebroadcasting session invalidation from tabs that were already
  cleared remotely, and always announce the committed cookie change after
  a login or logout POST so peer tabs re-check their session.
- Clear subscription model catalogs when the connection changes, so a
  previous connection's models are never offered under another one.
- Error Retry buttons and the capability hook re-run their load through the
  shared read cache instead of forcing a refresh that spends another read.
- Show the capability probe failure on the wizard step that runs the probe.
- Wizard model validation uses the models the picker offers, so an empty
  catalog blocks Next instead of keeping a previous model.
- Synchronize BroadcastChannel tests on actual delivery and count sends
  with a spy, so they pass and fail deterministically in isolation.

Docs: describe the SDK verification budget.

Refs #985

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The in-place JSON redaction matched each rule against the original output
on its own, while the text pass applies the rules one after another. A
rule that only matches across an earlier replacement (a password label
after an [INST] marker, for example) was missed, so the in-place result
could keep a value the text pass removes. A seeded fuzz over 50,000 JSON
documents found two such leaks.

- Redaction ranges now come from replaying the text pass's rules in its
  order on a working copy that maps every code unit back to the original
  range it stands for. The in-place result redacts what the text pass
  redacts, and the fuzz finds no leak and no invalid JSON.
- A match that covers a number or a literal replaces it whole with a JSON
  string, instead of falling back to the text pass.
- A password value stops before the next password label, so that label's
  value is redacted too; a short value up to the label is matched whole,
  as before. The scan stays linear.
- The assistant filter removes an unclosed <tool_output opener as well as
  whole tags.

Known limit: a non-ASCII configured secret that the producer escaped as
\uXXXX is not matched in that form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The loop test that keeps both same-turn attachment pages now also
  carries a page from an earlier user turn and asserts it reaches the
  model collapsed. Without that witness the test passed with the collapse
  pass disabled.
- The loopInjected projection test covers the Grok subscription driver:
  the authorize request and the proxy stream request carry the injected
  text as a plain user message and no marker field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route tests for Host error logging covered the message and wake-hold
paths only. Seven more logging sites now have a test that drives a Host
error carrying a user-content marker and asserts no fragment of it
reaches the log:

- host activity and host health read failures, through the real
  forwarders;
- the cancel route's inner catch;
- the outer catches of the host message, task result and cancel routes;
- the duplicate-suppression branch of respondUpstreamUnavailable, driven
  directly because no route can reach it with a committed response.

Each test asserts the failure line was logged exactly once, with the error
name and upstream status, as the witness that the path ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… IPs

Hosts behind one NAT or proxy share the per-IP budget, and an invalid-token
flood from the WRC source IP can deny WRC revocation calls until the window
resets. Follow-up tracked in #1006.

Refs #985

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n test database

pg-pool resolves end() before its clients' backends have closed, so the
forced DROP DATABASE could terminate a still-open connection and surface
57P01 as an unhandled error (Real Postgres CI exit 1 with 502/502 passing).
Use the shared teardown helpers and a plain DROP, as the other real-PG
suites do. Locally the old teardown failed 3 of 8 runs; the new one 0 of 8.

Refs #985

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…es out

The authorize location inherited the approval gateway's 30 s
proxy_read_timeout and proxy_send_timeout, while a retained authorize can
take up to 80 s: 40 s queued, 10 s reading the body and 30 s of work. A
request queued behind a slow holder got nginx's bare 504 before
control-api could answer queue_wait or authorize_timeout. mcp-host read
that uncoded 504 as provider_unavailable, so the subscription providers
classified it as a retryable ModelOverloaded and failed over with a
300 s cooldown.

- Set proxy_read_timeout and proxy_send_timeout to 90 s on the authorize
  location only. control-api pauses a queued body, so the send timer
  stalls as well as the read timer.
- Read a 504 with no JSON code from authorize as a terminal
  authorize_timeout. A connect timeout is already answered as
  control_plane_unavailable, so this 504 means control-api accepted the
  connection and authorize outlived the gateway.
- G1-12f pins both location timeouts above the admission policy's queue,
  read and work deadlines, and checks that the server keeps 30 s.
  T-R7-1c pins the 504 mapping through both providers with no failover
  class; the 502 and coded-504 rows are the controls.
- Document the gateway bound and the 504 mapping in both contracts.

Addresses M-B of review 5426789128.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
control-api's retained-body admission refuses before any read with 503
authorize_capacity_exceeded. That refusal was terminal, so a short queue
burst failed the turn even though nothing had been recorded.

control-api now sends Retry-After on every capacity refusal (policy
field retryAfterSeconds, 10 s: the holder's read deadline, below the
Host's 30 s retry ceiling; uncertified like the other bounds, #813).
authorize_timeout carries no header.

The Host reads Retry-After on that coded 503 and retries the authorize
once in the same provider, with the next attempt index, after the
advised delay. The second refusal, a refusal without the header (an
older control-api) and a caller-pinned attempt index stay terminal as a
non-retryable LLM_API_CALL_FAILED: no failover and no cooldown. A proxy
error that happens to carry the same code is not retried.

Tests cover the header on all three refusal reasons, the authorizer's
header read per status and code, the 9999/10000 ms boundary, the
terminal second refusal, the missing header, the pinned index, the
proxy-error exclusion, an abort during the wait, and the failover
engine staying on primary with authorize called exactly twice. Each
change was disabled once to confirm a test turns red.

Refs: review 5426789128 on #806 (M-A)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The redaction rules ran in order over one working copy, so a rule whose
match covered the label or prefix of the next secret hid that secret
from every later rule:
- `xoxb-1234-abcdBearer <token>`: the Slack rule took `Bearer`, and the
  token stayed in the output.
- `xoxb-…password=<value>`: the Slack rule took `password=`, and the
  value stayed in the output.
- a configured secret holding `<system>`: the injection rule broke the
  literal, and the configured value was not found.

Each rule is now also matched on the original text. The text pass
applies the union of both sets of ranges: overlapping ranges merge, to
`[REDACTED]` when their replacements differ, and a match on the original
that the in-order pass already covers is dropped, so outputs that were
redacted correctly before are unchanged. A rule's warning is reported
when either matching finds something. The JSON-preserving tool-output
path uses the same ranges.

The password value no longer runs into the next password label when
that label's value has at least 8 characters: the next match starts
there. Otherwise the whole run is matched, as before, so coverage is a
superset of the old pattern.

Scalar bounds for the JSON-preserving path are computed once per output,
so many matches inside one long number cost one pass over it.

A redaction pattern that matches an empty string now throws, since no
range can be redacted for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`vi.clearAllMocks()` keeps implementations and queued `*Once` values, so
a test that stopped early handed them to the next test and its failure
was reported against the wrong test. The suite now resets every mock and
re-arms the defaults in `beforeEach`.

The cancel-forward test comment no longer claims it tells the inner and
outer catch apart: both log the same prefix and answer 502.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two checks turned a committed authorize into 503 authorize_timeout: the
transaction helper threw on an aborted signal after COMMIT was
acknowledged (19093dc), and the route threw again after the authorizer
resolved (62d6ca8). The Host then saw a timeout while the ticket and
the budget reservation stayed held until their TTL, about 36 min.

The helper now returns an acknowledged COMMIT's result; its finally
still evicts the aborted session instead of restoring it. A lost COMMIT
reply keeps rejecting, unchanged. The route answers a resolved
authorize with 200 while the response is open and writes nothing to a
destroyed one; the admission then sees the abort and returns.

Tests: an abort in the continuation of the COMMIT reply and an abort
during session restoration both resolve with the committed result; a
late resolve after the work clock fired answers 200; a late resolve
after the client left writes nothing; an uncommitted authorize on the
work clock still answers authorize_timeout without Retry-After.
Re-adding either check, or dropping the destroyed-response guard,
turns one test red.

Refs: review 5426789128 on #806 (post-COMMIT abort)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th split

The proxy request-limit paragraph in both transport contracts still said
every local refusal reaches the Host as provider_unavailable. Since this
PR the proxies answer proxy_capacity_exceeded, visual_gate and
visual_host_share; provider_unavailable remains only for an invalid
deadline, a caller or admission deadline that ends the wait, and ticket
expiry. The paragraph now lists each code, the request_limit metric
label and the admission-refused log reasons.

The admission-limits comment said text-only authorizes never take a
unit. The split is by declared length: a V2 text-only body carries its
text twice (content and contentParts) and can exceed the text envelope
while inside the non-image budget, so it takes the unit. The comment and
both contracts now say so.

Refs: review 5426789128 on #806 (minors 1 and 2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l-scan boundaries

Review 5426789128 listed mutants that every suite let through. Each now
fails at least one test, and the unmutated tree stays green.

- Grok contract parity corpus: three rows asserted against the golden
  verdict in both copies: a full PNG with a wrong eighth signature byte,
  a JPEG frame header declaring 7 bytes (refused) and one declaring 8
  (the minimum, accepted). visualPayload.cjs `length < 8 ||` -> `&&`,
  and every `segmentLength < 8` variant (< 7, <= 8, < 5, < 4, removed),
  now fail.
- mcp-host imageSource: a user turn with an empty contentParts array
  stays byte-identical to V1, and an assistant turn with no parts is not
  refused. Dropping the `parts.length === 0` guard now fails.
- control-api: an express.json spy pins the ordinary parser at the text
  envelope and the retained parser at the visual envelope, both with
  inflate:false. A same-subject request from a different Host set is
  admitted under its own principal share, so a sub-only key fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tches

Two defects found by the round-4 re-review of the redaction plan:

- A password label inside an earlier password value lost its own value
  when that value started with another label. With
  `PASSWORD:pwd :Zq8Lm2Xvpasswd =pwd=Ab3x`, the match of `pwd :` ran to
  the next separator and swallowed `passwd =`, and the label-stopping
  pattern refused a value that starts with `pwd=`, so `pwd=Ab3x` stayed
  in the output. The value that runs to the next separator is now found
  from every password label, and overlapping values are joined. The scan
  stays linear: the next separator is shared by every label in the same
  run.
- Collecting the direct matches with `push(...matches)` passed one
  argument per match and overflowed the call stack at about 120k matches
  (`'[INST] '.repeat(200_000)` threw a RangeError).

The password values are matched twice and the coverage is the union:
stopping before the next password label, and running to the next
whitespace, `,` or `;` from every label. Where both cover the same text
the output can now be a single `[REDACTED]` where it used to be two.

Tests pin the two leak shapes in plain text and inside JSON, the 200k
match run, a configured secret made only of JSON punctuation, and
`was_modified` when the replacement equals the input. Each fix was seen
red by assertion first; six vacuity mutations (spread push, each password
finder dropped, no merge, no minimum length, separator rescanned per
label) turn the suite red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The verification-budget limitation claimed an in-cluster caller must share
the affected address. With trust proxy set to 1, req.ip is the rightmost
X-Forwarded-For entry, so a caller reaching Control API directly can be
charged to any address it names.

The SDK upgrade guide and the quota tracker comment still listed 3000 for
the authenticated request bucket and pre-auth limits; code and both
ConfigMaps default to 6000.

Refs #985

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The re-review of the previous commit found that the label-stopping
password regex added no coverage: from the same label, the finder that
runs to the next whitespace, `,` or `;` covers everything it matched.
It only split the output into several `[REDACTED]` markers, and through
the in-order replay it let a short value join a later replacement and
reach the 8-unit minimum (`pwd=apasswd = hunter2hunter2` redacted
`pwd=a` too). The password rule is now the finder alone, so a short
value stays (`pwd=a[REDACTED]`) and adjacent values give one marker.

Correction to the previous commit message: its first example
(`PASSWORD:pwd :Zq8Lm2Xvpasswd =pwd=Ab3x`) did not leak before that
commit. The shape that leaked was a label whose short value is the next
label, e.g. `pwd=AAAAAAAApwd=pwd= S3cretValue1`, which left
`S3cretValue1` visible; its tests are "redacts the last value after ...".

Tests added:
- a value of 8 code units is redacted and one of 7 is kept;
- an uppercase label inside a value;
- `;` ends a value;
- a short value before a label with a long value stays;
- a configured secret inside a password value is redacted as part of
  that value, with no warning of its own (an earlier rule's match covers
  it whole, as for any other rule).

The first three were not caught before: a minimum of 7, a case-sensitive
label and a separator class without `;` each left the suite green; each
now fails exactly its test. Vacuity: dropping the finder fails 41 tests;
no merge, no minimum, per-label rescan and the argument spread each fail
the test that pins them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The in-order replay behind plain-text redaction kept two Int32Array
entries per code unit of the copy and rebuilt both on every rule that
matched, and the coverage check built two more arrays the size of the
input. Every rule with a match therefore cost time and memory in the
length of the text, not in the number of matches.

RedactionReplay now stores only the replacements, as segments sorted
along the copy (`[textStart, textEnd)` stands for `[origStart,
origEnd)`); the code units between segments are original and map by
offset from the previous segment. A match strictly inside a segment
cuts it into the part before, the match and the part after, so a
replacement allocates at most `count + 2 * matches` segments, checked
after the copy. Coverage of the matches on the original text uses the
replay ranges joined and a binary search. The direct pass on the
original text is skipped while no rule has replaced anything, because
its matches are then the replay's own.

Behaviour is unchanged. Evidence against the previous commit, on a copy
of both files:
- equality fuzz over sanitizeFreeformContent, sanitizeAssistantResponse,
  sanitizeOutput and previewOutputForLlm, 5 seeds x 80,000 inputs with
  secret, label and marker fragments: 0 differences, 0 thrown errors;
- replay fuzz comparing text and ranges after random replace
  sequences: 0 differences in 700,000 sequences;
- comparison fuzz against the base of the PR, 5 seeds x 25,000: 0
  regressions, 0 JSON leaks, warning differences equal to the previous
  commit's.

The fuzz found one defect while this was written: a capacity of
`count + matches` dropped the part after a match strictly inside a
segment, because typed arrays ignore writes past their end. The
capacity is now `count + 2 * matches` and an overflow throws.

Timing, same run, sanitizeFreeformContent at 2 MB (this commit vs the
previous): `pwd=` repeated 18 vs 39 ms, dense configured secrets 180 vs
386 ms, `[INST]passwd = ` repeated 77 vs 115 ms; no shape got slower,
and every shape still grows linearly with the input.

Tests added (a later configured secret matching inside an earlier
replacement): the replacement cut in three, the part before and the
part after the match mapped to the replaced range, and adjacent
replacements covering the text they span. The suite missed four
mutants of the new code before them (part after dropped, part before
dropped, touching ranges not joined, capacity of one segment per
match); each now fails its test. All eight mutants fail the suite;
`textEnd > b` changed to `>= b` is equivalent (it leaves an empty
segment that holds no code unit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The public boundary check (scripts/tests/test-minikube-t2-public-boundary.sh,
run by test-minikube-t2-contract.sh) rejected the safety test added in
db03bb0: `PASSWORD=abc=PWD=defghijk` matches its shell-style credential
assignment expression. The value after the short part is now
`fixturevalue`, which the check exempts as a synthetic test value; the
lowercase case uses the same value so the two rows differ only in case.
The test still needs a case-insensitive label: a case-sensitive one
leaves `PASSWORD=...` untouched and fails the expected `[REDACTED] tail`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since 0ef36f1 skips the direct pass while no rule has replaced anything,
the 200,000-marker test no longer reached the loop it was written for: a
`push(...)` spread survived the suite. A leading `<system>` match now puts
the `[INST]` rule on the direct pass, and a 2 s bound catches a coverage
lookup that scans every range (about 20 s on that input).

New tests pin three guards no test covered: password values that overlap
are joined (unjoined, a later configured secret makes `sanitizeOutput`
throw), a joined coverage range keeps its end when a range inside it ends
earlier, and an empty replacement is refused. The overlapping-label rows
and the punctuation-only secret case now assert exact output instead of
`not.toContain`, with the text pass breaking the JSON as a precondition.

Each guard was removed in turn and its test failed, with no other test
failing: spread, no join, join on the value start, linear coverage,
unconditional range end, no empty-replacement check.

Two comments in safety.ts are corrected: rebuilding the working copy costs
time in its length (only the segment bookkeeping is bounded by the segment
count), and the password value is no longer a regex class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8262337 replaced the route test "returns terminal authorize_timeout after
the real work clock aborts, without publishing late success" with three
cases: the uncommitted authorize still answers authorize_timeout, a
committed authorize is answered 200, and a committed authorize whose client
already disconnected writes nothing. The pinned-case manifest still named
the removed test, so the control-api CI job failed with "expected exactly 1
matching case, found 0". Pin the three replacement cases instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndow

A recovery that ended denied, or whose subscribers all left, stayed in the
recovery map as failed. reserveReadRequestRecovery refused every later
denial in that read family until a success or a full cache clear, so no
further background recovery was scheduled. The entry is now released,
checked by identity, while a denied recovery still schedules no further
attempt of its own.

The SDK verification budget used a first-hit window while the gates it
bounds reset on the calendar minute, so a budget spent late in a minute
kept denying after the gates had reset. It now uses
CalendarMinuteRateLimitStore with a key without the :ip: marker. Passing
the store alone would keep the first-hit window for :ip: keys.

Refs #985

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The stream gate grants a freed slot to whichever queued waiter polls first,
not in queue order. Four cleanups this PR added freed the held slots and then
awaited the waiters in queue order. When a later waiter won a slot, the loop
blocked on an earlier waiter that could never get one, until its real 60 s
queue clock.

On a loaded CI runner this hung "visual admission overflow answers 503
visual_gate" past its 30 s timeout (run 37470624129). The timed-out test kept
running, so the two T-VG-QW cases after it found the shared gate occupied and
failed as well. The same code passed on the previous two heads and on the
re-run.

- codex-llm-proxy server.security: the overflow test queues its waiters with
  an AbortController signal and aborts them before freeing the held slots, so
  no waiter can take a slot. A beforeEach in the stream-gate describe requires
  an empty visual gate, so a leftover now fails the next test by name instead
  of cascading.
- codex-llm-proxy and grok-llm-proxy requestLimits: each waiter releases its
  slot as soon as it gets one (Promise.all), whatever the grant order.

Falsified by forcing the out-of-order grant (draining in reverse):
- With the old cleanups, the overflow test times out at 30 s, the next test's
  beforeEach names the leftover {running: 2, queued: 6}, and the three
  requestLimits cases time out at 5 s.
- With the fix, all of them pass.
- A cleanup that leaves the waiters queued fails the next beforeEach with
  {running: 0, queued: 8}.

The same in-order loop in the codex "rejects the 11th visual waiter" case
predates this PR (43075cf, in origin/dev) and is left unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zach88 and others added 24 commits October 8, 2026 16:16
…t opened them

The renderer filtered sandboxUi:closed, refreshError and titleChanged by
appRef. When the same app was relaunched, a closed emitted by the crash of the
old view matched the new launch, released its owner in App, and leaving the tab
never closed the freshly mounted view.

Each open now carries a renderer-generated launchId (crypto.randomUUID). The
sandboxUi:open handler validates it (/^[A-Za-z0-9-]{1,64}$/, rejects otherwise)
and echoes it on every event of the view that open mounted; the renderer drops
events whose launchId is not the current launch. Generated in the renderer so
events that precede the open's resolution are not lost; launchSeqRef was not
reused because it restarts at 0 on page remount. The two chat-drawer title tests
ran against a stubbed SandboxUiPage with no launch; their coverage moved to the
ownership suite, which drives the real page.

Class-check: grep -rnE "sandboxUi\.on[A-Z][A-Za-z]*" desktop-app/ui/src --exclude-dir=__tests__ + grep -rnE "appRef (!==|===)" desktop-app/ui/src --exclude-dir=__tests__ → 5 subscribers + 1 appRef comparison → fix onClosed, onRefreshError (SandboxUiPage.tsx) and onTitleChanged (App.tsx). Not touched: onFindResult (already keyed by clientRequestId), onDeepLink (not bound to a view), onOauthError (not wired in IPC), App.tsx readOutgoingRouteDecision (a getLocation response, already guarded by sandboxUiActivationGenRef and skipped on same-app relaunch).
Repro-test: desktop-app/ui/src/__tests__/App.sandboxUiOwnership.test.tsx:500, :512, :625; desktop-app/src/__tests__/ipc.sandboxUiLaunchId.test.ts:79, :99, :108 (fail on 4520708: close() called 0 times, stale banner shown, tab renamed by stale title, payload lacks launchId)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a live session

A login while another user is still signed in replaced the token without
moving sandboxUiSessionEpoch or closing the embed, so an open of the
outgoing user with its mint in flight mounted under the new user, and a
live embed plus its refresh loop survived the switch. The epoch bump and
queued close now live in invalidateSandboxUiSession(), shared by the
session clear and the hadAuthenticatedScope branch of
installAuthenticatedLogin. Team hops keep the epoch: same user.

Class-check: `this.sessionToken =` / `this.me =` in appService.ts → 4 token
replacements + 5 `me` refills → 1 identity change outside
clearAuthenticatedSessionState (installAuthenticatedLogin), fixed;
commitSessionToken is same-user team switching, restoreSavedSessionOnce
only runs without a session, `me` refills come from getMe on the same token.
Repro-test: desktop-app/src/__tests__/appService.sandboxUiLifecycle.test.ts:835,861,874 (fail on a4c1672)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ges during its write

The mount re-checked the session only after the awaited cookie write, so a
logout or login-over-login landing during the write returned without a view
but left the outgoing user's sandbox-ui cookie in the recipe partition.
removeSandboxUiCookie() mirrors writeSandboxUiCookie() (same name, origin and
path) and runs when isCurrent() fails after the write. A superseded
generation does not remove: a newer mount owns the partition.

Class-check: async sandbox-ui cookie writes guarded only by a session check
before the await → 2 sites (mount in sandboxUiDriver.ts, refresh installCookie
in appService.ts) → fix the mount; the refresh runs outside the lifecycle
queue, so a removal there could delete the next user's cookie for the same
recipe, and its late write only replaces the outgoing user's own cookie.
Repro-test: desktop-app/src/__tests__/sandboxUiDriverLifecycle.test.ts:448 (fails on fc917c5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at launchId subsumes

Main sends appRef and launchId as a fixed pair from the same open, and the
renderer publishes the launchId in the same effect flush that activates the
tab of that app; tab appRefs never change. An event with the current launchId
and another tab's app cannot exist, so the guard had no reachable case to test.

Class-check: appRef filter after a launchId filter in sandbox-ui event
handlers → 3 sites (App.tsx onTitleChanged, SandboxUiPage.tsx onClosed and
onRefreshError) → only onTitleChanged had it; removed, the class is uniform.
Repro-test: n/a (dead guard removed; existing title tests in
desktop-app/ui/src/__tests__/App.sandboxUiOwnership.test.tsx:625 still pass)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in over a live session

The field doc still said the epoch advances only when the session is cleared;
since r1-m2 a login that replaces a live session moves it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lifecycle tests

The public boundary check reads a token: '<value>' literal as a credential
assignment unless the value carries a fixture marker. The r1-h1/r1-m2 tests
added session-token-* literals without one, so the Minikube T2 contract job
failed with PUBLIC_BOUNDARY_REJECTED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rphan-view

fix: stop the sandbox-ui embed from outliving its owning tab
…pprovals to the task

- Free-space floor max(15% of the volume, 16 MiB) at admission, restored at
  startup and every 5 minutes up to floor + 5%; default storage percent 70.
- Protected caps: half the budget per caller, three quarters per Host.
- Reuse skips copies with less than 1 hour left; a removal refused with
  EACCES/EPERM restores owner access on the verified parent (O_NOFOLLOW +
  fchmod) and retries once; gfsRuntime.stop() waits for the cycle under a
  deadline.
- shell_exec, http_request and cron_manage: one approval covers that tool for
  the rest of the task, "always" for later tasks; never '*', and '*' does not
  cover them. On stateless Hosts cron_manage create/enable asks on every call.
- Playwright spec for the per-task approval scope (opt-in, not run here).
- Runbook and architecture docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An adopted entry (found on disk at startup) that has expired while still
indexed must answer download_missing through readManagedFile and
readManagedFilePrefix, like any copy this process did not publish.
Without the provenance check in managedEntry it answers download_expired,
because the expiry check runs before openEntryContent, the second guard.

Witness: the caller's own published copy is read at the same instant.
Vacuity mutation: dropping `entry.provenance !== 'published'` from
managedEntry turns only this test red (download_expired). Pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…otstrap

fix(hcc): prepare runtime bootstrap for safe resource rollouts
…ame task

ProgressStepper set approvalPending once the user clicked Approve or Deny
and never cleared it. The stepper stays mounted for the whole task, so when
another tool suspended the same task (a plain shell_exec approval followed
by http_request, now the normal flow with per-tool task approvals), the new
card rendered with both buttons disabled and could only be answered from
the notification bell.

The decided state is now keyed by the suspension's requestId, so a new
suspension starts with enabled buttons while the decided card stays locked
against a second submission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rofile

- fixtures/workflowUi: pace password sign-ins to the control-api admission
  rate (one login per 7.5 s server-wide), which the appPage fixture and
  loginAs exceeded.
- workflowAgentChatTools: startFreshThread finds the chat row's More actions
  button by row, since its accessible label now names the thread.
- session-tool-approval-scope: select gpt-5.4 through the composer menu
  (gpt-5.4-mini answered later shell requests from earlier outputs and
  skipped requested http_request calls, so no approval card could appear),
  and use `echo <marker>; date` so the output cannot be predicted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parsed value is always assigned in the try block, and the catch block
throws, so the NaN initializer was never read (CodeQL
js/useless-assignment-to-local, alert 1761).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stdio

A shell whose leader exits naturally polls its process group every drain
interval. The poll published the "stdio held by a detached process" result
as soon as one probe found the group gone. When the last in-group writer
exits just before a probe, its pipe EOF can still be in flight, and under
load it misses the setImmediate after the probe. The call then ends with
is_error and the held-stdio notice although no process outside the group
holds the pipes (CI run 37944477227, P1-d: `(sleep 2; printf late) & printf
early` returned "earlylate" with the notice).

The poll now publishes only after two consecutive absent probes; a `close`
in between keeps the natural result. A real detached holder settles about
two drain intervals after exit instead of one.

Tests: P1-i (EOF after the first absent probe keeps the natural result) and
P1-j (two absent probes without EOF report the holder at the second probe),
both pinned; P1-c renamed to "about two drain intervals" with its lower
bound raised to two intervals. Reverting to one probe turns P1-c, P1-i and
P1-j red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tail-view

fix(control-ui): refresh connector detail view
Run the large-file spec as the profile's seeded Desktop user instead of a
hard-coded test@clerum.io. Every task now checks on the Host that the
receipt's copy has the uploaded CSV's sha256. A new serial test downloads
as the owner, deletes the Host pod and waits for a new Ready pod whose store
adopted the copy, downloads again into a new receipt under the same user
key, then downloads as a second user (a named SQL precondition) into a
separate user key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
launchAndLogin spends a password attempt through the API in loginAs and then
clicked Sign in immediately, so control-api could answer the click with 429.
The form stayed on screen and the poll reported "pending" for 60 s. Wait for
the password pace before the click, and treat a login error alert as a
terminal failure that reports its text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ease-lifecycle

fix(mcp-host): redesign the GFS download store, bound it on the workspace PVC, and scope native tool approvals to the task

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@jozer-rami
jozer-rami merged commit eb00b07 into main Oct 12, 2026
137 of 138 checks passed
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.

6 participants