Skip to content

fix(deps): upgrade adm-zip to 0.6.0 to fix GHSA-xcpc-8h2w-3j85 - #2643

Merged
sdziupin merged 1 commit into
openchamber:mainfrom
mel0nyrame:fix/adm-zip-cve-2026-39244
Aug 6, 2026
Merged

sdziupin merged 1 commit into
openchamber:mainfrom
mel0nyrame:fix/adm-zip-cve-2026-39244

Conversation

@mel0nyrame

Copy link
Copy Markdown
Contributor

Intent

adm-zip <0.6.0 has a high-severity vulnerability (GHSA-xcpc-8h2w-3j85): a crafted ZIP can trigger a ~4GB memory allocation. This upgrades the pinned dependency from ^0.5.16 to ^0.6.0 in the packages that declare it, closing the advisory. No behavior change for callers.

Non-goals

  • No refactor of the ClawdHub install flow beyond the dependency bump.
  • No change to the project version (1.18.1).
  • No removal of @types/adm-zip (0.6.0 ships built-in types, but dropping the type package is out of scope here).

Affected surfaces

  • packages/web — declares and uses adm-zip at server/lib/skills-catalog/clawdhub/install.js (new AdmZip(Buffer.from(zipBuffer)).extractAllTo(tempDir, true)).
  • packages/vscode — declares adm-zip as a dependency; no code reference, so runtime behavior is unaffected (declaration bump only).
  • bun.lock — resolves [email protected] → [email protected].

Repository guidance

Guidance Why it applies How the change complies
openchamber-change-discipline skill Dependency/lockfile change Smallest complete change (only the adm-zip lines + one focused regression test); ran workspace-wide checks + affected build per the matrix
AGENTS.md "Do not add dependencies unless explicitly requested" Touches package.json deps Only upgrades an existing dependency's version; adds no new dependency
packages/web/server/lib/skills-catalog/DOCUMENTATION.md Owns the install path using adm-zip Confirmed new AdmZip(buffer) + extractAllTo APIs are unchanged in 0.6.0 (only bug fixes), so documented behavior holds

Validation

Check Result
bun audit (project scope) No adm-zip advisory remaining
bun run type-check Pass (5 workspaces, exit 0)
bun run lint Pass (5 workspaces, exit 0)
bun run build Pass (exit 0)
bun run dead-code Ran; no adm-zip/new-test findings (pre-existing unused exports only)
bun run --filter '@openchamber/web' test New install.test.js: 2 passed — builds a real ZIP with adm-zip and asserts extractAllTo restores files incl. nested subdirectories into the target skill dir
CI pr checks (oc-review.yml) on the source branch Green (build + type-check + lint + electron architecture/updater tests)

Note: 1 pre-existing failure in packages/web/src/api/git.test.ts (getGitRangeDiff mock) is unrelated to this change and untouched by it.

Visual evidence

No user-visible change. Dependency version bump plus a backend regression test; it does not alter any rendered UI.

Risks and failure behavior

Low risk. The 0.6.0 changes to extractAllTo are bug fixes only (utimes failures now ignored per upstream #379; directory permissions restored per #530); the constructor and method signatures are identical to 0.5.16, so no call-site adaptation was required. Rollback is a simple revert of the dependency bump. The added test guards the extraction path against future regressions.

adm-zip <0.6.0 allows a crafted ZIP to trigger a ~4GB memory
allocation (GHSA-xcpc-8h2w-3j85). Bump the dependency in the web
and vscode packages to ^0.6.0. The new AdmZip(buffer) and
extractAllTo(dir, overwrite) APIs are unchanged, so no call-site
adaptation is needed.

Add a vitest regression test for the ClawdHub install path that
builds a real ZIP with adm-zip and asserts extractAllTo restores
files (including nested subdirectories) into the target skill dir.
@mel0nyrame
mel0nyrame requested a review from a team August 5, 2026 05:46
@mel0nyrame
mel0nyrame requested a review from btriapitsyn as a code owner August 5, 2026 05:46
@openchamber-bot openchamber-bot Bot added the review:pending Automated contribution review is in progress. label Aug 5, 2026
@openchamber-bot

Copy link
Copy Markdown
Contributor

Code Review Summary

This PR upgrades adm-zip from ^0.5.16 to ^0.6.0 in packages/web and packages/vscode (plus bun.lock) to close GHSA-xcpc-8h2w-3j85 / CVE-2026-39244, a high-severity DoS where a crafted ZIP declaring a huge uncompressed size forces a ~4GB Buffer.alloc and OOMs the process on all extraction/read APIs. The ClawdHub skill installer in packages/web/server/lib/skills-catalog/clawdhub/install.js is the only code path that consumes this dependency (new AdmZip(Buffer.from(zipBuffer)).extractAllTo(tempDir, true)), so the fix is a version bump plus a vitest regression test for that extraction path.

  • Advisory and fix verified: affected <0.6.0, patched 0.6.0; the new bun.lock resolution entry's integrity hash matches the npm registry (sha512-Xlery...ddgg==).
  • API compatibility verified against upstream source: new AdmZip(buffer) and extractAllTo(target, overwrite) are signature-identical between 0.5.16 and 0.6.0, and the only extractAllTo behavior delta is that utimes failures are now best-effort (previously they aborted extraction with CANT_EXTRACT_FILE). The documented Agent/chaos agent 5 4ecf2906 #306 subdirectory-preservation change affects only extractEntryTo(..., maintainEntryPath=false), not extractAllTo, so the "no behavior change for callers" claim holds for this call site. Path-traversal sanitization (canonical/sanitize) is present and unchanged in both versions.
  • No prior review comments exist on this PR; this is the first review pass.

No blocking correctness, security, guidance, or contribution-contract issues found.

Verdict: PASS

Reviewed HEAD: 22539ff6016667d5cf24a3948ae93d99f75316a9
Previous reviewed HEAD: none

Applied Repository Guidance

Source Why applicable Rules/invariants evaluated
AGENTS.md Root rules; dependency change and server runtime code "Do not add dependencies unless explicitly requested" (only an existing dependency's version was raised); correctness invariants around extraction/cleanup; runtime boundary: domain logic stays in packages/web server module
CONTRIBUTING.md Canonical PR policy and review handoff contract PR contract sections (Intent, Non-goals, Affected surfaces, Repository guidance, Validation, Risks) all completed with concrete evidence; "command name without a result is not evidence" — each check has a stated result; no-visible-change case explained concretely
.github/PULL_REQUEST_TEMPLATE.md Required handoff structure All template sections present and filled with content specific to this diff; non-goals explicitly stated (no @types/adm-zip removal, no version change)
openchamber-change-discipline skill Dependency/lockfile change plus a new source file Smallest complete change (4 files); validation matrix for "Dependency or lockfile" (workspace-wide checks + affected builds) and "Added source file" (dead-code) reported as run; focused regression test exercises the real extraction path rather than private helpers
packages/web/server/lib/skills-catalog/DOCUMENTATION.md Owns the ClawdHub install path that uses adm-zip Confirmed extractAllTo is used with the documented { ok, installed, skipped, error } contract; SKILL.md validation invariant preserved; "Temporary directories are cleaned up in finally blocks" note evaluated (see finding 1)
packages/web/vitest.config.ts + package.json test script Determines whether the new test is actually discovered Default vitest include (**/*.test.js) picks up server/lib/skills-catalog/clawdhub/install.test.js under vitest run; matches the established packages/web/server/**/*.test.js convention

No other project skills apply: the diff touches no UI, CLI, sync, relay, Electron, settings, theme/locale, or sortable-interaction code, and the ClawdHub API client is only mocked in the test, not modified. ui-api-decoupling (server API routes/data access) was considered and does not govern this change because the diff neither adds nor alters API routes, runtime fetch, or SDK access.

Findings

  1. non-blocker: skipped installs leave a temp directory behind (pre-existing, adjacent to the new test)
    File: packages/web/server/lib/skills-catalog/clawdhub/install.js:214-217
    Problem: when extraction succeeds but SKILL.md is missing, the code pushes to skipped and continues without safeRm(tempDir), so clawdhub-<slug>-XXXX directories (with their extracted contents) accumulate in os.tmpdir() on every skip. The new test 2 ("skips a package whose extracted contents lack SKILL.md") codifies exactly this path and asserts the skip result, so it indirectly freezes the leak in place. Pre-existing and untouched by this diff, so not blocking; the test's afterEach also only removes the user skill dir, not stray temp dirs.
    Suggested fix (optional, in a follow-up): await safeRm(tempDir) before the continue, or check SKILL.md after extraction via zip.getEntry('SKILL.md') and skip without extracting first.

Evidence and Residual Risk

  • Review evidence: the added install.test.js is relevant and proportionate — it builds a real ZIP with the dependency under test, drives the actual installSkillsFromClawdHub extraction path (mocked only at the network boundary ./api.js), asserts nested-subdirectory preservation and content integrity for the new version, and covers the missing-manifest skip path. Test/file assertions were traced against install.js and are consistent. Reported validation (type-check, lint, build, dead-code, focused web tests) matches the change-discipline matrix for a dependency/lockfile change; CI execution status was not inspected per review policy. The upstream [email protected] package carries its own regression tests for the CVE (e.g., test/decompress_bomb.test.js), so the new test not reproducing the crafted-header case is acceptable.
  • Security/supply-chain: the change directly closes the high-severity DoS advisory for both declaring packages and the single lockfile resolution; @types/[email protected] is retained but inert (no TS file imports adm-zip, and the web tsconfig does not type-check server/). No new dependencies, no auth/token/network behavior changes, and the remote-ZIP extraction path's path-traversal protection is unchanged (sanitize/canonical present in both versions).
  • Residual risk: I could not execute the tests, audit, or builds in this environment; the test file is also not covered by the web package's type-check/lint scope (server/ excluded), so any type-level drift in it would only surface at test runtime. [email protected]'s other listed fixes (data-descriptor acceptance, dir-permission restoration, proto-injection hardening) only widen accepted-input tolerance and are not expected to regress this call site.

@github-actions github-actions Bot added review:ready Ready for maintainer review. and removed review:pending Automated contribution review is in progress. labels Aug 5, 2026

@sdziupin sdziupin 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.

👍

@sdziupin

sdziupin commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Good one.

@sdziupin
sdziupin merged commit ebb02b4 into openchamber:main Aug 6, 2026
2 checks passed
sdziupin added a commit that referenced this pull request Aug 9, 2026
…command feature parity (#2777)

* fix(sync): route directory-less todo updates

* fix(settings): persist collapsed message preference

* Defer OpenCode restarts for config mutations

Co-authored-by: Serhii Dziupin <[email protected]>

* Accumulate OpenCode settings restarts behind Apply & Restart

Defer OpenCode reloads after settings mutations, track pending changes,
and expose a top-right Apply & Restart OpenCode action with a counter so
sessions stay available until the user explicitly applies.

Co-authored-by: Serhii Dziupin <[email protected]>

* Improve pending OpenCode restart counter contrast

Place a high-contrast count badge first on the Apply & Restart action so
pending settings changes remain obvious in the Settings chrome.

Co-authored-by: Serhii Dziupin <[email protected]>

* Move Apply & Restart into Settings footer with confirm

Replace the top-right pending CTA with a primary footer action that
replaces Reload OpenCode when changes are pending, and warn that running
chats will stop unless the user opts out of the confirmation.

Co-authored-by: Serhii Dziupin <[email protected]>

* Shorten Apply & Restart action label

Drop OpenCode from the Settings footer apply button and confirm title so
the action fits the narrow nav and stays concise.

Co-authored-by: Serhii Dziupin <[email protected]>

* Restyle Apply & Restart confirm dialog actions

Center nearly full-width Cancel and Apply buttons and move the
don't-show-again checkbox below them for a clearer decision layout.

Co-authored-by: Serhii Dziupin <[email protected]>

* Make Apply & Restart confirm actions nearly full-width.

Keep Cancel and Apply & Restart at about 40% each and centered,
with the don't-show-again checkbox stacked underneath.

Co-authored-by: Serhii Dziupin <[email protected]>

* Fix Apply & Restart confirm dialog vertical stack.

Stop using DialogFooter (sm:flex-row) so layout is text, then
centered ~40% action buttons, then the don't-show-again checkbox.

Co-authored-by: Serhii Dziupin <[email protected]>

* Enforce equal 40% widths for confirm dialog actions.

Wrap Cancel and Apply & Restart in fixed-percentage containers so
Button content sizing cannot skew the centered pair.

Co-authored-by: Serhii Dziupin <[email protected]>

* Force confirm dialog action widths with inline 40%.

Keep text → buttons → checkbox stack and avoid DialogFooter
row layout; use inline width so the pair stays nearly full-width.

Co-authored-by: Serhii Dziupin <[email protected]>

* Fix deferred restart review findings.

Drop phantom pending records when provider disconnect removes nothing
and when Behavior AGENTS.md payloads are not deferred. Clear the Apply
badge after external OpenCode manual-restart guidance. Align skills and
commands save toasts with deferred restart feedback, and fix French
OpenCode elision.

Co-authored-by: Serhii Dziupin <[email protected]>

* Add German pendingRestart locale keys after main merge.

de.settings.ts came from main without Apply & Restart strings;
add translations so i18n key parity stays intact.

Co-authored-by: Serhii Dziupin <[email protected]>

* Document deferred OpenCode restart contract and drop dead helper.

Add config-mutation-response.js to owning docs, describe restartDeferred
and external requiresManualRestart on /api/config/reload, and remove the
unused buildConfigMutationResponse export.

Co-authored-by: Serhii Dziupin <[email protected]>

* Load provider OAuth methods on reconnect, not only add mode.

Reconnect previously showed only the API key field because
provider.auth() ran solely in add-provider mode. Also keep the
original OpenCode method index when listing OAuth options.

Co-authored-by: Serhii Dziupin <[email protected]>

* Mirror Google API key env aliases into managed OpenCode.

OpenCode can mark Google connected via GEMINI_API_KEY while the Generative AI
SDK only reads GOOGLE_GENERATIVE_AI_API_KEY, so chat asked for a key that was
already present. Alias unset sibling names on managed launch for web and VS Code.

Co-authored-by: Serhii Dziupin <[email protected]>

* feat: add restore/unarchive for archived sessions

Archived sessions had no way back to the active list: the only available
action was "Delete permanently". Add restore per session (sidebar context
menu, Archive page row) and in bulk (sidebar selection bar).

The OpenCode server cannot clear time.archived over HTTP — session.update
only applies the field for a finite number, so an omitted key is a no-op
and null is silently ignored (verified against opencode 1.18.12). Restore
therefore writes time.archived = 0: every client-side reader classifies
archive state by truthiness, so 0 reads as active in the UI, the event
reducer, and the OpenCode app/TUI.

The server's time_archived IS NULL list filter still excludes such rows,
so the global session cache no longer issues an archived:false request
for its active list. Full and per-directory loads now fetch once with
the inclusive flag and split client-side via splitGlobalSessionsByArchived,
which also halves per-directory refresh requests. Directory bootstrap
keeps the server filter because live child stores must not hold archived
sessions; a restored session re-enters its live store through the
authoritative session.updated event.

unarchiveSession/unarchiveSessions follow the archiveSession contract:
wait for server confirmation before reconciling stores, runtime-guard
every reconciliation, preserve partial batch results, and fail loudly
when the server keeps the session archived instead of toasting a
successful no-op.

Closes #2346

* feat: add german translations for session restore

* fix(walkthrough): block unauthenticated providers with a friendly refusal

When the walkthrough small model resolves to a provider with no usable
login, readiness was still ready and generate returned a raw 500 message.
Refuse up front with no-provider-login and surface a blocker instead.

Closes #2607

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(walkthrough): hide unauthenticated models and disable Generate

Do not present a provider without a login as the selected walkthrough
model, and grey out Generate when readiness is false instead of showing
a login-error blocker or raw auth banner.

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(providers): hide API key form for OAuth-only providers

Only show the API key credential UI when a provider declares API auth
(or auth methods are still unknown). OAuth-only plugin providers such as
Cursor now show Connect/OAuth only, load auth methods on reconnect, and
skip an empty models section until models are discovered.

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(walkthrough): keep unauthenticated models out of the picker

Treat an empty allowedProviderIds list as allow-none, disable Generate
when no usable model is selected, and mute the button styling so it
reads as unavailable rather than actionable.

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(providers): hide models until credentials exist

For OAuth-only providers like Cursor, open the auth panel when
credentials are missing and omit the models list until auth/env
credentials are present so placeholder catalog entries are not shown
before login.

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(walkthrough): drop info tint from disabled Generate button

The status-info classes were winning over muted disabled styles, so the
button still looked actionable when no model was selected.

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(ui): keep manual model override after delegated subtask completes

Synthetic subagent-completion nudges were treated as the latest user model
choice and rehydrated the agent default, while setAgent preferred the agent
pin over the session override. Skip synthetic prompts for restore, preserve
manual selection-store overrides, and prefer session agent models in setAgent.

Closes #2404

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(walkthrough): stop the importance tag reading as a review finding

The "Critical" pill was painted in the status-error colour, so a stop marked
because it drives the change read as a severity reported against the code —
the one thing this feature never does. It is now "Key change", carries its
emphasis with weight and an outline rather than a status colour, and both tags
state their meaning in a tooltip. The panel links the guide from its header,
and the guide gained a section on what the tags mean and what they do not.

Also corrects two German strings that translated the noun "stop" as the verb.

* fix(walkthrough): name an outdated server instead of failing to parse its HTML

A server without these routes does not answer 404 with JSON. The unmatched
/api path reaches the OpenCode proxy, and OpenCode serves its embedded web UI
for anything it does not recognise — HTML, status 200 — so a client newer than
its server parsed a web page as JSON and put "Unexpected token '<', "<!doctype"
in the panel, naming neither the cause nor the remedy.

The client now checks the content type before parsing. A non-JSON answer on 2xx
or 404 blocks with "this server is older than the app, update it and refresh".
A non-JSON 5xx keeps its own failure: a server that answered badly is not a
server missing the feature, and sending that user to upgrade chases the wrong
thing.

* fix(providers): complete OAuth logins that finish in the browser

OpenCode's authorize response reports how the client must finish: `code`
expects a pasted code, while `auto` requires the client to call
oauth/callback immediately and hold it open — upstream blocks in there
polling for the device code or waiting on its loopback redirect, and only
that call persists the credential. Every auth plugin OpenCode ships uses
`auto`; none use `code`.

The page implemented only `code`. It opened the browser, showed a paste
field no provider can fill, and never called back, so a successful sign-in
stored nothing and the app sat unchanged. Authorization now drives the UI:
`auto` chains straight into the callback behind a waiting state with a
cancel, and the paste field appears only when a provider actually asks
for a code.

Two smaller failures shared that surface. Prompts were never collected,
which put GitHub Copilot Enterprise out of reach entirely, so a method
that declares them now asks first and passes the answers to authorize.
Device codes are also recovered from the instructions text, where they
actually live — the old code read fields the API does not return, so the
copy button never appeared.

The callback is exempt from the ordinary proxy deadline and gets a
15-minute budget, bounded by the shortest upstream expiry we know of.
A human sign-in with 2FA does not fit in four minutes, and expiring it
turned a completed login into a 504.

* release v1.18.1

* fix(walkthrough): use remote default branch

* feat(ui): numbered context-panel surface switching with configurable prefix

- Add switch_context_surface shortcut (default Cmd/Ctrl + 1..9, 0 for the 10th
  surface) that opens/closes/switches context panel rail surfaces by their
  visible order, configurable and persisted in Settings -> Shortcuts.
- Show order-number badges on rail icons while the modifier is held >500ms;
  dismiss on release, blur, or a number press until the next press-and-hold.
- Remove the legacy mod+2/3/4 (diff/terminal/git) and switch_tab_1..9 bindings
  so numbered surface switching goes only through the new mechanism.
- Replace the help-dialog 'Switch Project' row with the surface-switch row and
  update the shortcuts footer/header icons to the command icon.

* fix(git): resolve the base branch from the repository instead of its name

Follow-up to #2629, which stopped the walkthrough from comparing against a
branch that does not exist. The same guessing, and the same near-misses in how
the answer was applied, were left elsewhere:

- The default branch travelled as `rootBranchHint`, whose documented meaning is
  "the branch the project root worktree is on". It gets its own option, because
  a parameter that means two things is one the next caller gets wrong.
- A candidate equal to the branch being compared is skipped. In a plain checkout
  the root hint *is* the current branch, so it won every time and produced a
  comparison with itself; the repository default now wins there.
- The Changes and pull-request surfaces read the default branch too. A pull
  request opened against a branch that does not exist is a worse failure than a
  walkthrough that will not generate.
- `hasResolvableBaseBranch` matched `origin/feature/main` for a base of `main`,
  passing the check and then failing the comparison it exists to prevent.
- `getRangeDiff` promoted only `origin/<base>`. A base carried by any other
  remote stayed a bare name, which git resolves against refs/heads and nowhere
  else, so it failed exactly as before.
- `getBranches` dropped every branch of a remote that did not answer, turning
  "we could not ask" into "these branches are gone" — offline, that silently
  removed comparisons that work fine against local remote-tracking refs.
- A remote with no `remote/HEAD` is asked once with `ls-remote --symref` rather
  than falling back to the guess this data exists to replace.

The `defaultBranches` contract was documented under the status response; it
belongs to the branches response, which now has a section of its own.

* feat(usage): add xAI quota reporting

packages/ui/src/components/sections/usage/UsagePage.tsx\n  - What: Show selected configured-provider errors in Usage settings.\n  - Why: Preserve provider-specific failures when another provider succeeds.\n\npackages/ui/src/lib/quota/providers/index.ts\n  - What: Register xAI in shared quota metadata.\n  - Why: Expose xAI across shared usage surfaces.\n\npackages/ui/src/types/quota.ts\n  - What: Add xAI to the quota provider contract.\n  - Why: Allow typed xAI results across runtimes.\n\npackages/vscode/src/opencodeAuth.ts\n  - What: Preserve secure auth-file persistence for refreshed xAI credentials.\n  - Why: Reuse the VS Code auth boundary safely.\n\npackages/vscode/src/quotaProviders.ts\n  - What: Add xAI SuperGrok quota fetching, refresh, parsing, and failure isolation.\n  - Why: Provide VS Code usage parity.\n\npackages/web/server/lib/opencode/auth.js\n  - What: Enforce secure auth directory, auth file, and backup permissions.\n  - Why: Protect refreshed OAuth credentials.\n\npackages/web/server/lib/quota/DOCUMENTATION.md\n  - What: Document the xAI quota provider.\n  - Why: Keep provider ownership documentation current.\n\npackages/web/server/lib/quota/providers/index.js\n  - What: Register the xAI web quota adapter.\n  - Why: Expose xAI through the existing quota route.\n\npackages/web/server/lib/quota/providers/xai.js\n  - What: Add SuperGrok usage fetching, inherited OAuth refresh, strict parsing, and zero/error semantics.\n  - Why: Report xAI billing-period usage without new auth architecture or credentials.

* perf(ui): swap the session activity spinner for a dot and a turn timer

The spinner ran a CSS animation on every active row for the whole turn,
repainting a composited layer at frame rate. Rows now carry a static dot —
primary while running, info while unread — and the metadata slot on the right
shows how long the turn has been going, updating once per second in the dot's
colour. The counter is the motion the spinner used to provide, at 1 fps.
Collapsed groups, folders and projects take the dot only, since one counter
cannot speak for several running turns.

Elapsed time is measured client-side because SessionStatus carries no
timestamps, and starts are persisted so a reload resumes the same count. Two
rules keep that honest. Only a liveness stamp — refreshed while a session is
observed active, stamped as the page hides, and compared against the page's
navigation start so a slow bootstrap is not charged to the absence — and a 90s
adoption window may expire a record; a snapshot that cannot yet see a session
is not evidence its turn ended. And a busy event is never read as a turn
boundary, because OpenCode republishes busy at every step of the agent loop, so
after a reload one of those repeats normally beats the first status snapshot.
Idle and error events do end a turn, and retire the record with it.

Snapshot reconciliation walks the running turns and asks whether the snapshot
covers each one, rather than being handed everything it covers: only a live
start can settle, so the pass scales with timing work instead of with the
directory's session list, and allocates nothing per poll.

Also applied to the mobile sessions sheet and session switcher. The shared
duration ticker moves to hooks/ now that it has a second consumer.

* fix(test): restore the issue-2039 suite by completing its session-actions mock

The suite mocks `session-actions` by listing its exports one by one, and had
fallen behind `unarchiveSession`/`unarchiveSessions`. `session-ui-store` imports
both, so the file threw on import and ran zero tests — the draft auto-accept and
canonical-worktree-directory guarantees it covers were unprotected, and the
report looked almost like silence rather than a failure.

* fix(cli): generate a UI password for bare --ui-password in daemon/serve mode

The grand tunnel restructuring removed the CLI's auto-generated UI
password, so `openchamber -d --ui-password` (no value) silently started
an unauthenticated server instead of creating a password as in 1.8.1.

Restore generation for an explicit --ui-password flag without a value:
the password is generated before either launch path, passed to the
daemon/foreground process via OPENCHAMBER_UI_PASSWORD, persisted in the
instance state file, and surfaced once in human/quiet/json output.

Refs OPE-216

* feat(server): validate OPENCHAMBER_OPENCODE_HOSTNAME bind hostname

The env var was already read and passed to the managed OpenCode server
spawn, but any non-empty string was accepted. Reject values that are
not a valid IP (IPv4/IPv6, brackets allowed) or DNS-style hostname with
a clear [config] error and fall back to the secure loopback default so
a typo can never silently bind a non-loopback address.

Refs OPE-231

* fix(sync): route question/permission replies by the request's own session directory

Answering a question tool (or a permission prompt) could leave the session
permanently stuck on "asking question": resolveDirectoryForBlockingRequest
returned the containing child-store key, which only proves containment.
For a worktree session (or any session whose record is grouped under a
parent project store), the reply was addressed to the parent directory's
OpenCode instance, where the pending request does not exist - the server
answered QuestionNotFoundError, the local request was removed, and the
trailing question-tool part stayed running with no recovery until Stop.

Resolve the directory from the request's own session record (server-
confirmed ownership: session.directory, then project.worktree) before
falling back to the containing store key. When a reply/reject comes back
not-found, also enqueue the settled-running-tool tail materialization so
the tool part converges to the server's actual state instead of leaving
the UI stuck.

Refs OPE-236

* fix(web): parse agent frontmatter as leniently as OpenCode

parseMdFile now matches gray-matter (used by OpenCode) for file shapes
OpenChamber previously failed to parse: frontmatter whose closing '---'
sits at end-of-file without a trailing newline, a UTF-8 BOM prefix, and
YAML with unquoted colons in scalar values (via the same sanitizer
OpenCode applies). OpenCode parses these files, so OpenChamber must
too: otherwise the whole file was treated as the prompt body and a
save rewrote the existing YAML block into the body, prepending a
duplicate frontmatter block.

Refs OPE-178

* feat(chat): refocus composer after adding message to context

After the add-to-context (context pin) action completes successfully, move
focus back to the chat input so the user can keep typing immediately. Uses
the existing focusChatInput helper and the requestAnimationFrame refocus
pattern already used by the model/agent selectors.

Fixes #2447

* feat(ui): show the changed-files count badge on the Git rail surface

Replaces the plain activity dot on the context panel rail's Git button with
a numeric badge of the changed-files count from the git store status, so the
count is visible at a glance without opening the Git surface. Large counts
cap at 99+ to keep the pill within the 36px button. The badge is reflected
in the button's accessible label and the hover tooltip.

Fixes #2364

* feat(sessions): show a pending-question indicator on session rows

Adds a per-session pending-question badge to sidebar rows, driven by the
live directory-store question state through a dedicated per-session
subscription channel so unrelated streaming never re-renders rows.

Collapsed parent rows roll up pending questions of hidden descendants
from their owning directory stores without bootstrapping them. Question
state is cloned on session delete/archive so badges clear when sessions
disappear. Adds the questionChangeCallbacks sync performance counter,
i18n keys for all locales, and unit tests for the subscription channel
and scope selection.

Fixes #2634

* fix(composer): restore Shift+Enter newline on iOS

CodeMirror defers Enter on iOS (and Chrome Android): the real keydown is
captured without running the keymaps and the keymaps then run against a
synthetic keydown that dispatchKey builds from the key name alone, with
no modifier keys. The composer's Shift+Enter thus arrived as a plain
Enter, and on devices where Enter sends (iPad Safari/PWA, where the
desktop layout applies) it submitted the message instead of inserting a
newline.

Record the real Enter keydown's shift state on the view's contentDOM and
restore it onto the deferred synthetic event before the caller's
onKeyDown policy runs, so Shift+Enter means newline again on every
runtime. Plain Enter behavior is untouched: on iOS it still follows the
same deferred path it used before this change.

Fixes #2558

* fix(server): rebind message-stream upstreams after a managed OpenCode restart

When the managed OpenCode process exits but a server survives on the old
port (Windows: killProcessOnPort is a no-op, so the orphaned process tree
keeps the port), restartOpenCode() times out waiting for the port and
spawns a fresh server on a NEW port. HTTP/proxy traffic follows the new
port, but the global message-stream hub's upstream SSE reader stays pinned
to the old server's /global/event stream — that connection never closes —
so new events never reach the UI and the chat stops updating until the
app is restarted (#2638).

Lifecycle now fires an optional onOpenCodeRestarted hook after a
successful managed restart; index.js wires it to the new
messageStreamRuntime.rebindUpstream(), which restarts the shared hub
(its reader re-dials buildOpenCodeUrl → the current port) and closes
directory-scoped sockets so their per-connection readers rebuild against
the new port. External servers are untouched (their port cannot change).

Fixes #2638

* feat(tasks): support markdown scheduled-task loops in .agents/loops

Adds markdown-based scheduled-task definitions ("loops") discovered from
.agents/loops/*.md (project scope, ancestor directories up to the
worktree root) and ~/.agents/loops/*.md (user scope), mirroring the
skills discovery pattern.

File format: YAML frontmatter (name, schedule cron, enabled, model as
provider/model, optional agent/timezone) plus the markdown body as the
execution prompt. Discovery and parsing live in
scheduled-tasks/loops.js; project-config gains reconcileLoopTasks which
runs inside the project write lock on every syncProject:
- identity by task name; a loop takes over a matching task, preserving
  its id and runtime state (markdown wins on conflict with JSON)
- tasks whose loopFile is gone are unscheduled; JSON tasks are never
  removed
- new loops are created under deterministic loop:<scope>:<name> ids
- project scope shadows user scope on name collisions
- malformed files are skipped with a warning and never block valid ones

Runtime state stays in the project config/state store; it is never
written to the markdown files. Module documentation updated with the
file format and reconciliation rules.

Fixes #2583

* fix(sync): finalize tool parts orphaned by an interrupted turn after settlement

When a managed OpenCode process dies mid-turn (crash, health-check
restart), the persisted turn never settles: the trailing assistant
message has no time.completed and its tool parts stay pending/running
forever — the server never finalizes them (anomalyco/opencode#19023).
The existing settle-triggered tail refresh refetches the same stale
records, so the UI kept running tool timers and working styling
indefinitely (#2577).

Now, when a session is authoritatively settled (session.idle/
session.error event, or an authoritative status snapshot lowering a
previously busy session) and the trailing assistant message is still
unfinished with active tool parts and no pending question/permission,
the orphaned parts are finalized locally as error/"Interrupted" with
an end time — the same shape OpenCode itself writes for cancelled
tools. The mark is gated on an explicit idle status (absent status is
"unknown", never judged), never applies while busy (including
question/permission waits), and a later terminal event or refresh
supersedes it while a stale running refresh cannot regress it (the
reducer and materializer already preserve final statuses).

Fixes #2577

* docs(server): document onOpenCodeRestarted lifecycle dependency

* fix(ui): stop painting git activity dots on non-git rail surfaces

The badge PR inverted showActivityDot to !== 'git', so editor/terminal/diff
picked up the blue activity dot whenever git had changes. Git already shows
a numeric badge; other surfaces should stay quiet. Also split the count
aria/tooltip strings into singular/plural keys.

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(ui): let Escape reach the terminal PTY from the context panel

The desktop ContextPanel captured Escape on the panel aside and closed
the whole pane before ghostty-web's bubble-phase listener could forward
the key. Skip closing when the event target is inside the terminal so
apps like Vim can leave insert mode.

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(fs): keep list paths in requested space through symlinks

Closes #2627

Listing a directory through a workspace symlink was returning realpath
entry paths. The file tree then rejected nested expand toggles because
those paths fall outside the workspace root.

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(tasks): harden loop reconciliation against renames and malformed files

Review fixes for the markdown loop feature:

- Loop-owned tasks now adopt by loop file path, not task name, so renaming
  a loop (frontmatter name or UI rename) renames the task in place instead
  of leaving a stale duplicate that keeps running the old definition;
  orphan duplicates of the same file are unscheduled.
- Unparseable loop files are reported to the scheduler as
  definition:null entries: a task whose file still exists is kept with its
  last good definition, and only a genuinely removed file unschedules it.
  Transiently malformed files (mid-edit, bad merge) no longer delete tasks
  or their runtime state.
- Adoption preserves UI-only execution fields (goalEnabled, goalTokenBudget,
  permissionAutoAccept, variant) that the portable format does not define.
- DELETE on a loop-sourced task now returns 400 with guidance to remove the
  loop file, instead of being silently undone by the next reconcile.
- Loops default to enabled: false; discovery of repository content never
  auto-executes scheduled sessions unless the file explicitly enables them.

Regression tests for each fix; DOCUMENTATION.md updated.

* fix(tasks): guard loop name length and surface loop ownership in the UI

Review follow-up:

- Reject loop files whose frontmatter name exceeds MAX_TASK_NAME_LENGTH
  (80): task names are clamped at storage time, so a raw name longer than
  the limit could never match the stored task identity. The file is treated
  as malformed (definition: null) instead of creating an unreachable
  definition; MAX_TASK_NAME_LENGTH is now exported from project-config.js
  and shared with loops.js.
- Surface loop-sourced tasks in the scheduled-tasks dialog: tasks carrying
  loopFile show a 'Managed by loop file <path>' note, and the enable
  toggle / edit / delete actions are disabled with an explanatory tooltip,
  since the file remains authoritative and would revert any such change.
  run-now stays available. New locale keys added to all 11 message files
  (i18n parity test enforces exact key sets).
- ScheduledTask type gains an optional loopFile field (additive, unknown
  to older clients).

* fix(tasks): cover syncProject wiring and allow deleting orphans after file removal

Review follow-up:

- runtime.test.js: add syncProject wiring tests with a real temp-dir
  project and real project-config runtime — asserts reconcileLoopTasks is
  driven with the discovered loops when the project path is known (task
  created, nextRunAt computed) and that plain listing is used when the
  path cannot be resolved (reconcile not called).
- service.js: DELETE on a loop-owned task is rejected with a 400 only
  while its loop file still exists on disk; once the file is gone the
  orphan task can be deleted directly instead of waiting for the next
  reconcile. Tests use real temp files for both branches.
- DOCUMENTATION.md: delete semantics updated accordingly.
- PR description refreshed for the final HEAD (test counts, reconciliation
  contract, evidence wording).

* docs(tasks): add loops quick-start to the scheduled-tasks page

User-facing onboarding for markdown loop tasks: where .agents/loops
files live (project + user scope), a copy-paste sample file, the
frontmatter field table, and the behavior contract (file authoritative,
off by default, rename/malformed semantics, run-now still available).
Also lists the cron schedule type in the UI task creation steps, which
the page previously omitted.

* fix(chat): expand shell output by default (#2492)

* fix(desktop): preserve native taskbar minimize (#2494)

* fix(ui): make overlay scrollbar persistent on desktop shells (#2581)

PR #2520 fixed #2475 in the wrong layer (overflow classes on
SettingsView.tsx wrapper divs that aren't the actual scroll containers),
so the "no scrollbar in Settings" bug shipped in v1.17.2 unchanged, and
the sessions feed has the same underlying issue (#2402).

The actual scroll containers render through ScrollableOverlay ->
OverlayScrollbar, which hides the native scrollbar and draws its own
JS thumb that auto-hides ~1s after scroll activity stops, with a
second CSS rule fully hiding that thumb inside Settings specifically.

Fix both at the shared component/CSS layer so every consumer (Settings,
sessions feed, git panels, chat, etc.) gets a persistent scrollbar on
desktop shells (Electron + VS Code webview) in one change:

- OverlayScrollbar.tsx: skip the auto-hide timer and show the thumb
  immediately on mount when content overflows, on desktop runtimes.
  userIntentOnly consumers (chat auto-follow scroll, reasoning blocks)
  keep today's intent-gated behavior unchanged.
- index.css: scope the Settings-specific hide rule to non-desktop, so
  mobile/web keep existing behavior and desktop gets the thumb back.

* fix(settings): preserve notification template edits (#2300)

* fix(vscode): open notebook links in notebook editor (#2373)

* fix(ui): never auto-send the message queue into a streaming turn (#2642)

The queue auto-send gate treated a missing session status entry as idle,
but the server's /session/status map only lists busy/retry sessions — a
missed busy event leaves no entry while a turn is still streaming. On a
client that missed the busy event (e.g. mobile reconnect window), queued
follow-ups were dispatched into the running turn; OpenCode then merged
both prompts into one model response instead of serializing them.

Extract resolveQueuedSessionStatusType and mirror useSessionActivity's
fallback: a trailing in-flight assistant message means the session is
still busy, so the queue waits for the real idle edge. Subscribe the
effect to messages so the queue drains as soon as the turn completes
even when status events were missed.

* fix(ui): cascade-remove extracted document attachments when parent is removed (#2432)

When a document (PPTX, DOCX, XLSX, ODP, ODT, ODS) is attached,
extracted slide images were left orphaned after removing the
parent text entry. This adds a sourceDocumentId field to
AttachedFile that links all entries from the same document
extraction, and cascades removal of all entries in the group
when any one is removed.

Fixes #2426

Co-authored-by: chiamsun <[email protected]>

* fix(deps): upgrade adm-zip to 0.6.0 to fix GHSA-xcpc-8h2w-3j85 (#2643)

adm-zip <0.6.0 allows a crafted ZIP to trigger a ~4GB memory
allocation (GHSA-xcpc-8h2w-3j85). Bump the dependency in the web
and vscode packages to ^0.6.0. The new AdmZip(buffer) and
extractAllTo(dir, overwrite) APIs are unchanged, so no call-site
adaptation is needed.

Add a vitest regression test for the ClawdHub install path that
builds a real ZIP with adm-zip and asserts extractAllTo restores
files (including nested subdirectories) into the target skill dir.

* feat: add fallback parsing for pairing connection payloads in old Android WebViews (#2611)

* fix(files): handle reveal launcher failures (#2490)

* fix: wait for worktree bootstrap before prompt dispatch; resolve session directory for send/fork (#2708)

* fix(web): update foreground systemd services safely (#2542)

* fix(web): update foreground systemd services safely

* clarify desktop and remote updates

* Revert "clarify desktop and remote updates"

This reverts commit aaf4516.

* test-web-systemd-updates

* feat(ui,server): surface active instance service URLs in About settings (#2669)

Show the running instance's local server URL and tunnel URL (when a
tunnel is active) as labeled, click-to-open buttons on the About page.
/api/system/info now reports the instance port and tunnel URL, resolved
lazily from the tunnel runtime so each Git-worktree instance identifies
itself in the UI without parsing terminal output.

Refs OPE-194

* fix(chat): pin existing-session sends to captured target (#2424)

* fix(chat): pin sends to captured session

* fix(chat): handle runtime cancellation consistently

* fix(git): run post-checkout hook after worktree creation bootstrap (#2721)

Worktrees are created with `git worktree add --no-checkout` and populated
with `git reset --hard`, neither of which runs git's post-checkout hook.
Invoke the hook explicitly after population with git's standard arguments
(null ref previous HEAD, checked-out HEAD, flag 1) and the worktree as cwd,
mirroring `git worktree add` without --no-checkout.

A missing or non-executable hook is skipped (matching git) and a failing
hook is logged as a warning, never failing worktree creation or session
bootstrap. Applied to both the web server git service and the VS Code
runtime's git service.

* fix(desktop): recover from macOS directory permission failures

* fix(ui): use readable permission error color

* fix(ui): repair sync test contracts

* perf: cut cold-start download 58% and startup heap 22% via measured chunk-graph fixes (#2742)

* fix(ui): update session-switch-resync test to current handleEvent/setSessionTodos signatures

Co-authored-by: Serhii Dziupin <[email protected]>

* perf(build): split Shiki grammars/themes, CodeMirror legacy modes, and @pierre/diffs into on-demand chunks

Merging @shikijs/langs into one manual vendor chunk made the first language
request download every grammar (7.4 MB raw / 1 MB gzip). Letting Rollup split
these packages per dynamically imported module downloads only the languages,
themes, and modes actually used — matching how the worker build already
behaves. @pierre/diffs is split the same way so its pure patch parser (used by
the eager tool renderer) no longer drags the Shiki-importing render stack into
the startup graph.

Co-authored-by: Serhii Dziupin <[email protected]>

* perf(ui): restore lazy heavy views and stop eager settings-graph loading

- MainLayout: DiffView/FilesView/GitView/PlanView return to lazyWithChunkRecovery
  (they were silently made static in c545b51 while their Suspense wrappers
  remained), keeping the CodeMirror and @pierre/diffs stacks out of startup.
- ContextPanel: same lazy treatment for its Diff/Files/Git/Plan/Walkthrough
  tabs, with null Suspense fallbacks.
- CommandPalette imported getSettingsNavIcon from SettingsView, statically
  pulling the entire settings surface (SkillsPage -> CodeMirrorEditor -> vim
  mode, theme registry -> @pierre/diffs) into the eager graph; the helper now
  lives in lib/settings/metadata.
- The windowed SettingsWindow mounts only after its first open: rendering the
  lazy component closed made React fetch the SettingsView chunk graph at
  startup.

Co-authored-by: Serhii Dziupin <[email protected]>

* perf(ui): keep @pierre/diffs + Shiki out of the eager chat graph and defer diff worker warmup

- DiffWorkerProvider no longer statically imports @pierre/diffs/worker or the
  theme registry, and no longer spawns 3 workers plus a main-thread shared
  highlighter during mount. Pools are created on demand through a dynamic
  module load, warmed via requestIdleCallback after startup settles, and
  useWorkerPool notifies consumers when a pool becomes available.
- ToolPart's rich diff preview moves to lazily loaded ToolPartDiffPreview;
  the plain-text patch (PlainDiffFallback) renders while the chunk loads,
  mirroring the existing error fallback. Theme registration happens during
  render inside the lazy module so PatchDiff never renders unregistered ids.
- ChatInput mounts its lazy ToolOutputDialog only after the first attachment
  preview opens instead of fetching the dialog chunk on the draft screen.
- getMarkdownSyntaxVars moves to a pierre-free markdownSyntaxVars module so
  eager code-rendering consumers stop importing the registration module.

Co-authored-by: Serhii Dziupin <[email protected]>

* perf(web): load ghostty-web and Nerd Fonts on first terminal use

- ghostty-web (638 KB raw JS + WASM VT) is dynamically imported when a
  terminal actually mounts; TerminalView stays eagerly importable for the
  bottom dock.
- The ~2 MB of CDN Nerd Fonts are no longer preloaded and force-loaded on
  every cold start. index.html exposes an idempotent
  __openchamberEnsureNerdFonts hook; TerminalViewport requests it on mount
  and waits up to 2s so a cached font is in place before the glyph atlas is
  built, while a cold CDN fetch never blocks the terminal. Runtimes without
  the hook (VS Code, mobile) resolve immediately, matching their existing
  fallback-font behavior.

Co-authored-by: Serhii Dziupin <[email protected]>

---------

Co-authored-by: Serhii Dziupin <[email protected]>

* fix(terminal): keep default terminal tab names unique after closing tabs (#2718) (#2731)

* fix(chat): do not replay entry animations for already-seen fresh messages (#2124) (#2732)

* fix(git): enable core.longpaths for worktree population (#2746) (#2747)

* fix(git): enable core.longpaths for worktree population

Worktrees live under a deep OpenCode data-dir path, so Windows checkouts
of deeply nested repos failed bootstrap with "Filename too long". Enable
Git core.longpaths before git reset --hard (web + VS Code) and surface
clearer path-length guidance when the filesystem still rejects a path.

Fixes #2746

Co-authored-by: Serhii Dziupin <[email protected]>

* chore(vscode): keep ensureWorktreeLongpaths private

Avoid an unused export in the VS Code git service; the helper stays
local to populateWorktreeWithLockRecovery.

Co-authored-by: Serhii Dziupin <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Serhii Dziupin <[email protected]>

* perf(terminal): relax transport keepalive from 20s to 45s

The ping has no pong-timeout on either side — it exists only to keep
intermediaries from idling out the socket. 45s stays safely under the
relay host-data idle reaper (90s) and common proxy read timeouts (60s),
while halving the events that wake the relay Durable Object during an
open session.

* fix(settings): restore integrations nav icon after main merge

Co-authored-by: Serhii Dziupin <[email protected]>

* feat(telegram): require multi owner user ids and link userinfobot

Fail closed when no owners are configured, reject invalid id 0, accept
multiple ownerUserIds with defaultUserId back-compat, and make
@userinfobot open https://web.telegram.org/k/#@userinfobot in settings.

Co-authored-by: Serhii Dziupin <[email protected]>

* feat(telegram): multi-group sync and Discord command/critique parity

Add per-chat policies and project sync for Telegram groups with
restriction messaging, a Telegram command palette matching Discord,
shared /critique on|off for both messengers, and type-aware /diff copy.

Co-authored-by: Serhii Dziupin <[email protected]>

* feat(discord): register native /critique slash command

Expose critique toggle as a Discord application command with optional
mode, mark it nativeSlash in the palette, and broaden integrations
search keywords for critique/commands/groups.

Co-authored-by: Serhii Dziupin <[email protected]>

* test(discord): expect /critique among slash commands with options

Co-authored-by: Serhii Dziupin <[email protected]>

---------

Co-authored-by: bashrusakh <[email protected]>
Co-authored-by: Pascal André <[email protected]>
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Serhii Dziupin <[email protected]>
Co-authored-by: Bohdan Triapitsyn <[email protected]>
Co-authored-by: RyderAsking <[email protected]>
Co-authored-by: Rajat Asthana <[email protected]>
Co-authored-by: Henry Moran <[email protected]>
Co-authored-by: Sérgio Pedro <[email protected]>
Co-authored-by: Aman Tahiliani <[email protected]>
Co-authored-by: TPOB <[email protected]>
Co-authored-by: Chiam <[email protected]>
Co-authored-by: chiamsun <[email protected]>
Co-authored-by: Mel0ny <[email protected]>
Co-authored-by: CallMeBill <[email protected]>
Co-authored-by: Bruno Fantauzzi <[email protected]>
Co-authored-by: Syu <[email protected]>
Co-authored-by: Wsyjq <[email protected]>
Co-authored-by: deatheros <[email protected]>
@mel0nyrame
mel0nyrame deleted the fix/adm-zip-cve-2026-39244 branch August 11, 2026 12:01
1056674754 added a commit to 1056674754/openchamber that referenced this pull request Oct 3, 2026
- bump packages/web + packages/vscode from ^0.5.16 to ^0.6.0
- add vitest regression test for ClawdHub install path exercising adm-zip
- ignore /tmp/ in .gitignore

Refs: upstream ebb02b4 (openchamber#2643), 7e95dfa
WI: openchamber#133
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:ready Ready for maintainer review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants