Repository navigation
Promote dev to main (prod promotion) - #1063
Merged
Merged
Conversation
- 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>
…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>
…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
Contributor
There was a problem hiding this comment.
CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
This was referenced Oct 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes
dev→mainfor the prod deploy (merge commit, not squash).Dev HEAD
728c49dis the pin set dev deployed and verified (infrapin-status.yaml:verified: true, pinned by infra commitc680f2f, pipeline run 37957369785).Included PRs
Deploy notes
charts/clerum-crds/**changed (2 CRDs):mcpserver(extended MCP OAuth schema, incl. relaxedrequiredfields) andworkflowrecipe(description-only default rate-limit text). Expect a CRD pre-apply (gcp-prod-deploy-crds) beforemode=apply;mode=diffwill confirm.deploy/base.🤖 Generated with Claude Code