Skip to content

fix: fifteen bug fixes in isolation, orchestration, UI, plugins, GitHub sign-in and tests - #159

Open
BIackFIame wants to merge 15 commits into
howdeploy:mainfrom
BIackFIame:pr/bug-fixes
Open

BIackFIame wants to merge 15 commits into
howdeploy:mainfrom
BIackFIame:pr/bug-fixes

Conversation

@BIackFIame

@BIackFIame BIackFIame commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Branch pr/bug-fixes, fifteen commits on main (0fa6895d), one commit per fix, grouped by area, so any one can be
reviewed, cherry-picked or reverted on its own. Every entry fixes behaviour that exists on main today; there is no new
API, setting or surface (fix 13 adds one opt-in environment variable, CANVASTTY_AGENT_PASS_ENV; fix 15 adds an
"Open settings" button to the existing notice). Fixes 13 to 15 were added after the PR was opened: 13 sits under
Isolation, after fixes 1 and 2; 14 and 15 sit under UI, after fixes 6 and 7. Text that only Settings shows (the
sign-in outcome lines of fix 8, and the canvas wheel/navigation settings fix 7 translates) moves out of the startup
string table into the lazily loaded Settings chunk (-2,788 bytes), and renderer-bundle-splitting now checks that this
text stays out of the startup bundle. Fixes 14 and 15 change what HOME and the canvas show, so their few lines stay in
the startup bundle (+4,922 bytes); the startup bundle ends 2,134 bytes above main, under the 1,700,000 budget.
Size: about 950 lines of source, 2,100 of tests, 30 of docs.

Merge order: 1 of 12 (first). It depends on nothing, and no feature PR depends on it, except the source-change PR (12),
which is rebased on top of it for a single CSS rule.

Fixes 6 to 10 were part of #149 (with its Codex-review follow-ups). #149's two plugin-page commits are
not bug fixes: they add manifest keys and will come as a feature PR. If you take this PR, #149 can be closed in its
favour; that is your call.

Isolation

1. Other CLIs inside isolation could read Kimi Code's sign-in. fix(isolation): treat Kimi Code's KIMI_CODE_HOME as Kimi's own home

  • Problem: inside the isolation layer, an agent of another CLI (Codex, Claude Code, ...) could read ~/.kimi-code,
    where Kimi Code keeps its OAuth refresh token. The homes of Claude Code, Codex and Grok are already hidden from each
    other. Kimi launched with a moved home (KIMI_CODE_HOME) could not write it, so it could not refresh its sign-in.
    To reproduce, start an isolated Codex card and read ~/.kimi-code/ from it.
  • Cause: the isolation tables knew only KIMI_HOME, which neither Kimi Code (KIMI_CODE_HOME, ~/.kimi-code)
    nor the older kimi-cli (KIMI_SHARE_DIR, ~/.kimi) reads. The rest of the core already uses KIMI_CODE_HOME.
  • Fix: Kimi's folders are KIMI_CODE_HOME or ~/.kimi-code, KIMI_SHARE_DIR or ~/.kimi, and KIMI_HOME when
    set. All three are Kimi's own state for a Kimi launch and hidden from every other CLI.
  • Test: tests/agent-isolation.test.mjs (the Kimi home is writable for Kimi and hidden from Codex; the default
    Kimi folders are hidden from other CLIs; KIMI_SHARE_DIR).
  • Area: isolation (security).

2. Grok and Kimi cards in a contributed home stayed "working" and had no CanvasTTY tools. fix(runtime): give Grok and Kimi cards in a contributed home their status hooks and tools

  • Problem: a launch:contribute plugin may start a CLI with its own home. The docs mention this for an account's
    config home. Such a Grok or Kimi card never left "working", approvals were not shown, wait_for_agent timed out,
    and Kimi had no canvastty_browser / canvastty_agents. To reproduce, have a launch contribution set GROK_HOME
    or KIMI_CODE_HOME to a folder in its data folder and start a card.
  • Cause: Grok Build reads hooks only from <GROK_HOME>/hooks, and Kimi Code reads them (with its MCP servers and
    permission rules) only from <KIMI_CODE_HOME>/config.toml. CanvasTTY wrote its overlays into the default homes
    only.
  • Fix: CanvasTTY now writes the same overlays into that home instead, shared by the cards there and removed with
    the last one. This applies only when the home is strictly inside the data folder of the plugin that set it (the
    host's record), at least <plugin>/<home> deep. Nothing else is ever written. The existing lock, journal, backup
    and marker rules restore the person's settings byte for byte. No symbolic link is followed or written through;
    otherwise the launch is refused with a clear message.
  • Test: tests/agent-runtime-account-homes.test.mjs (14 tests): ownership, overlays with the default home
    untouched, byte-for-byte restore, crash recovery, links refused, Kimi tools, and end to end from LaunchPipeline
    through TerminalManager to both bridges.
  • Area: isolation / agent runtime.
  • Note: fix(isolation, safety): keep CanvasTTY's private data out of agents' reach #148 adds its own account-home rule (contributedAccountHome). If fix(isolation, safety): keep CanvasTTY's private data out of agents' reach #148 lands first, this commit is
    rebuilt on it so the rule is not checked twice; that variant is ready.

13. Every agent, subagent and plugin-started card inherited the person's provider keys, tokens and admin keys. fix(agents): keep ambient admin keys from agents and credentials from delegated cards

  • Problem: an agent card's environment was CanvasTTY's own environment, minus only CanvasTTY's reserved variables.
    So every agent, every subagent and every card a plugin started, isolated or not, got whatever was exported where the
    app was started: OPENAI_API_KEY, ANTHROPIC_API_KEY, GH_TOKEN, cloud credentials, and organization-level admin
    keys such as OPENAI_ADMIN_KEY and ANTHROPIC_ADMIN_KEY (projects, members, usage; the new openai CLI reads
    OPENAI_ADMIN_KEY from the environment). The isolation layer restricts files, not variables, although the docs say other
    agents' credentials cannot be read. The launch builders (status hooks, browser and orchestration MCP config) also
    copied the person's OPENCODE_CONFIG_CONTENT, which can hold keys, into every OpenCode card. And a plugin could start
    a plain terminal card (sessions.create with provider terminal), which kept the whole environment, and drive it.
    To reproduce, start CanvasTTY from a shell that exports OPENAI_ADMIN_KEY, let an orchestrator spawn a subagent and
    run env in it.
  • Cause: terminalEnvironment() filtered no credentials, and the agent runtime and browser bridges read
    process.env directly.
  • Fix: a new ambientProviderKeys.ts, applied once in TerminalManager.planSpawn. Every launch builder now reads
    the filtered environment, never process.env.
    • Admin credentials (*_ADMIN_*KEY, *_ADMIN_*TOKEN) and CanvasTTY's own (CANVASTTY_GITHUB_TOKEN, any
      CANVASTTY_*TOKEN / *KEY / *SECRET / *PASSWORD) never reach an agent card, whoever starts it.
    • The person's own top-level agent cards keep the rest of their environment, ordinary provider keys included, in
      every profile. A CLI signed in by API key (Claude Code with ANTHROPIC_API_KEY, Qwen Code with OPENAI_API_KEY,
      OpenCode, ...) keeps working.
    • Subagents and every card a plugin starts, a plain terminal card included, get only an allowlist: path, home, user,
      shell, locale, terminal, temporary and XDG folders, Windows system folders, proxy and certificate variables,
      EDITOR / VISUAL / PAGER, and the CLIs' home folders (CODEX_HOME, CLAUDE_CONFIG_DIR, ...). No provider
      key, token, cloud credential, SSH_AUTH_SOCK or OpenCode inline config.
    • A plain terminal card the person starts keeps everything except CanvasTTY's own credentials, which only the app
      uses. Orchestrators cannot start plain terminal cards (unchanged; now tested).
    • What a launch sets explicitly (a launch contribution's env / secretEnv) is added on top, so a plugin that hands
      its agent a key keeps working.
    • Opt-in: CANVASTTY_AGENT_PASS_ENV=NAME[,NAME] in the environment CanvasTTY starts with keeps the listed variables
      for every agent card. It is read from the app's environment only, so an agent cannot widen it for itself.
  • Behaviour change: a subagent or plugin-started agent that signed in only through an exported key (for example
    ANTHROPIC_API_KEY) now needs CANVASTTY_AGENT_PASS_ENV=ANTHROPIC_API_KEY or a launch contribution's secretEnv.
    Sign-ins kept in the CLI's own files or the keychain are not affected. Documented in
    docs/installing-and-security.md (en/ru/zh-CN) and in the delegation table of docs/agent-orchestration.md.
  • Not covered (documented): this is about inherited variables. Without the isolation layer an agent runs as the
    person and can still read a key from their shell profile file, and a plain terminal card runs the login shell, which
    reads it.
  • Test: tests/agent-ambient-keys.test.mjs (fake values set on the test process): plain terminal; the person's
    Claude, Codex, OpenCode and Qwen cards keep ordinary keys but get no admin or CanvasTTY credential, also under a
    recording isolation layer; subagents with and without a layer; a plugin-started plain terminal; an orchestrator
    cannot start a plain terminal; the environment actually handed to the PTY of OpenCode, Claude, Codex and Qwen
    subagents built through the real runtime and browser bridges holds none of the fake values (and fails without the
    fix); the opt-in; case-insensitive names (Windows). tests/launch-contributors.test.mjs: a plugin-started agent
    drops ambient keys but gets the key its contribution names in secretEnv.
  • Area: isolation / agent environment (security).

Orchestration and decisions

3. Decision services could not tell an OpenCode write from an edit. fix(opencode): send OpenCode's file tools to decisions in Claude Code's shapes

  • Problem: command review and other decision services saw every OpenCode write, edit and multiedit as an
    edit with only {file_path, content}. An edit's replaced text was missing, so a service could not see what it
    removes.
  • Cause: the OpenCode decision bridge mapped all three tools to one shape.
  • Fix: send Claude Code's names and shapes, which decision services already read: Write {file_path, content},
    Edit {file_path, old_string, new_string, replace_all}, MultiEdit {file_path, edits}. Bash and apply_patch
    are unchanged.
  • Test: tests/decision-hooks.test.mjs: each shape end to end, and a MultiEdit outside the project is still
    denied.
  • Area: orchestration / decisions.

4. An OpenCode subagent's shell prompt reached the orchestrator, not the person. fix(opencode): bring a subagent's shell ask to the person, never to its orchestrator

  • Problem: on a launch contributor's model, OpenCode keeps bash on "ask" (your rule, unchanged). In a live run,
    a subagent's ls prompt sat unanswered. wait_for_agent returned the same pending prompt on every call, so the
    orchestrator gave up. It could also have answered the prompt itself, because send_to_agent typed into the
    terminal whatever its status, and Enter answers OpenCode's prompt.
  • Cause: send_to_agent did not check needs_approval, and wait_for_agent could not wait for the person's
    answer.
  • Fix:
    • send_to_agent is refused while the subagent is needs_approval (not retryable). Nothing is written, and the
      refusal says to tell the person.
    • wait_for_agent returns a given prompt once. A later wait on the same prompt waits for the person's answer and
      the end of the turn.
    • bash is not allowed in any new case, and decision services answer as before.
  • Test: tests/opencode-subagent-ask.test.mjs. It runs the real OpenCode plugin module, RuntimeGateway,
    DecisionHooks, TerminalManager, AgentControlService and ScopedOrchestrationHandler, with no model.
  • Area: orchestration.

5. An orchestrator could approve a Codex subagent's command through the control CLI. fix(orchestration): an orchestrator's control connection never answers a subagent's approval prompt

  • Problem: with its own control connection, an orchestrator could choose <id> --choice 1 on a Codex subagent's
    "Would you like to run the following command?". That approved the command for the person. dismiss answered it
    "No". The orchestrator skill even described these as menus choose/dismiss can act on.
  • Cause: choose/dismiss checked only that the menu was the one observed, not what it asks. send was refused
    there, but with a retryable NOT_READY that said "wait".
  • Fix: while a card is needs_approval, or while Codex's own approval prompt is on screen (it can appear before
    the hook report), the orchestrator's choose, dismiss and send get NEEDS_PERSON. That refusal is not
    retryable and tells the orchestrator to tell the person.
    • Unchanged: menus that are not approvals (hooks review, model, rate-limit, update), reads, interrupt, and the
      person's own connection.
  • Test: tests/orchestrator-approval-answers.test.mjs. It runs the real control CLI request path over a socket
    with an orchestrator's grant: before the fix, the first choose wrote the keys. It also checks send_to_agent at
    each CLI's approval signal.
  • Area: orchestration.

UI and language (6 and 7 from #149)

6. Parts of the UI ignored the UI scale. fix(ui): scale the card menu, hints panel and plugin settings with the UI scale

  • Problem: at Settings → Appearance → UI scale 0.85 or 1.25, these stayed at 1.0:

    • the terminal card's options menu;
    • the canvas shortcut-hints panel;
    • the plugin card in Settings → Plugins (only its description scaled).

    To reproduce, set the scale to 1.25 and open each.

  • Cause: their rules used fixed px sizes instead of --ui-scale.

  • Fix: the existing classes use calc(... * var(--ui-scale, 1)). Nothing changes at 1.0.

  • Test: tests/ui-scale-chrome.test.mjs. Checked on Linux (xvfb) in ru/en and two palettes at 0.85/1.0/1.25;
    screenshots in fix(ui): follow the UI scale and the selected language in canvas chrome, Settings and plugin surfaces #149.

  • Area: UI.

7. Untranslated text in the Russian canvas hints and wheel settings; wrong key caps. fix(i18n): finish the Russian canvas hints and format their key caps like the reference

  • Problem: with the app in Russian, the canvas hints, the wheel/pinch settings and the navigation description were
    partly English. Long labels in Settings → Keyboard shortcuts were cut off. The hints showed "Alt+ArrowUp", and the
    shortcut reference showed ",nd+Comma" on macOS.
  • Cause: missing ru strings, no wrapping on those labels, and raw key names in the hints formatter.
  • Fix: the strings are complete, labels wrap, and key caps use the reference's formatter ("Alt+↑"). The
    wheel/navigation settings' text (only Settings shows it) moves from the startup table to canvasNavigationText.ts
    next to Settings, so the fix makes the startup bundle smaller instead of larger.
  • Test: tests/ru-shortcut-hints-i18n.test.mjs; tests/renderer-bundle-splitting.test.mjs (the settings text is not in
    the startup bundle).
  • Area: UI.

14. HOME's model-limit rows showed a bare "9%", which read as 9% left. fix(ui): say what is used and what is left in HOME limit rows

  • Problem: HOME → Model limits showed the used share of a window as a large bare "9%" with a bar and the reset
    countdown. "Used" was only in the hover / screen-reader text. Codex's own status line shows "% left", so 9% used read
    as 9% left. Usage history's latest observation (the "Latest observation" value and the "No observations in this
    period" note) also showed a bare percent. To reproduce, spend a little of a Codex or Claude window and open HOME.
  • Cause: the row rendered only the number. The data is right: Codex app-server usedPercent and Claude
    utilization are the used share.
  • Fix: the row reads "9% used · 91% left" (ru «9% использовано · осталось 91%»), and so does the hover /
    screen-reader line. Usage history uses one helper, quotaShareText, for both of its texts. Two i18n keys
    (limitUsedPercent, limitLeftPercent, ru/en) and three CSS rules that keep the countdown on the right and let the
    share text shorten with an ellipsis in a narrow row. Nothing else on HOME changes.
  • Test: tests/quota-used-left-labels.test.mjs: Codex and Claude fixture payloads keep 9% spent as
    usedPercent 9; the real HOME LimitRow, rendered without Electron in ru and en, shows used and left in the row and
    in its hover line; usage history's templates have no bare percent left.
  • Area: UI.

15. A plugin page whose native code or module was off only got "Plugin service is not running." fix(plugins): say when a plugin's native code or module is off, with a way to the setting

  • Problem: when a plugin page asked its own service and the person had not trusted the plugin's native code, or
    had turned off the module that runs the service, the page and the host notice only said "Plugin service is not
    running." (in English, also in the Russian UI). Nothing said why or where to change it. A trusted service that had
    crashed gave the same text without its error. To reproduce, install a plugin with a service and a page (for example
    examples/plugins/service-echo), leave its native code off and open the page.

  • Cause: PluginManager.assertService checked only that the service is in the active manifest, so every such
    request reached the supervisor, which knows only "not running".

  • Fix:

    • the host now refuses those requests with a fixed text: native code not trusted, or the service's module off
      (src/shared/pluginServiceNotice.ts);
    • the plugin frame shows it localized (ru/en), and the host notice carries an Open settings button that opens
      Settings → Agents → Extension native code (or Settings → Plugins for a module). A notice with a button stays 8 s
      instead of 2.6 s and takes clicks only while it has one; other notices are unchanged;
    • a trusted service that crashed, failed to start or waits to restart keeps the generic text, followed by its last
      error.

    The request still fails as before; nothing is trusted or turned on by the notice.

  • Test: tests/plugin-service-notice.test.mjs: through the real PluginManager and PluginServiceSupervisor
    with the service-echo example, native code off and module off give their texts and settings sections, in ru and
    en; a crashed trusted service keeps the generic text with its last error and no settings notice; the toast shows the
    button only when it has one; the frame, App and Settings are wired to it.

  • Area: plugins / UI.

GitHub sign-in (plugin showcase, from #149)

8. A started GitHub sign-in could not be cancelled; failures were silent or showed raw IPC errors. fix(github): cancel device sign-in and report denied, expired and failed outcomes

  • Problem:

    • a device sign-in could not be cancelled;
    • it retried failures until the code expired;
    • it ended silently on "access denied" or "expired";
    • a sign-in that could not start showed "Error invoking remote method 'github-auth:start': ..." in English at the
      top of Settings → Plugins, also in the Russian UI.
  • Cause: no cancel path. Every poll error was treated as retryable, terminal OAuth answers were ignored, and the
    renderer showed the raw IPC error.

  • Fix:

    • a Cancel button next to the code (cancelDeviceFlow over the existing IPC);
    • deviceFlowState reports idle, pending, denied, expired, failed or cancelled, with a localized status line in the
      Settings error style;
    • transient errors back off, and five in a row end the flow;
    • raw errors go to the log only.

    The new lines live in PluginSettingsSection (the Settings chunk), not in the startup string table.

  • Test: tests/github-auth.test.mjs, tests/github-auth-integration.test.mjs,
    tests/github-signin-errors-ui.test.mjs.

  • Area: sign-in.

9. A timed-out profile request left the sign-in "pending" forever. fix(github): report a timed-out profile request as a failed sign-in

  • Problem: when GitHub issued the token but the /user request then timed out, the flow stopped silently. Its
    status stayed "pending", and Cancel did nothing.
  • Cause: the timeout's AbortError was taken for a cancellation.
  • Fix: only this flow's own abort counts as a cancellation. A timeout ends the sign-in as failed.
  • Test: tests/github-auth.test.mjs.
  • Area: sign-in.

10. A 429/5xx with an OAuth error body ended the sign-in after one answer. fix(github): retry HTTP 429/5xx token answers that carry an OAuth error body

  • Problem: a transient 429 or 5xx from the token endpoint with a body like {"error":"temporarily_unavailable"}
    ended the sign-in as an unknown error.
  • Cause: the OAuth body was read before the transient-status check.
  • Fix: these statuses are retried with back-off whatever the body says. slow_down keeps its own handling.
  • Test: tests/github-auth.test.mjs.
  • Area: sign-in.

Tests

11. browser-engine-tabs failed about 1 run in 10 under load. test: wait for the click's navigation in the engine-tabs click test

  • Problem: "engines without layout: observe skips the fake geometry and clicks go through element.click()" failed
    now and then on main with "the click navigated: a new revision". It failed 6 of 50 and 9 of 100 runs under load.
  • Cause: a race in the test, not in the product. The fake engine answers the click before it sends the navigation
    events, and the test's ready() returned on the old document's ready state.
  • Fix: h.ready(tabId, { after }) also waits until the document revision has passed after. The click test
    waits for the navigation the click started and checks the new URL.
  • Test: the same file. 0 of 100 runs failed under the same load, and a deterministic 30 ms event delay fails 5 of 5
    before the fix and 0 after.
  • Area: tests.

12. terminal-session-store failed on every run from some checkout folders. test: ignore the checkout path in the session store's secret-word check

  • Problem: "terminal window descriptors persist atomically without scrollback or environment" fails whenever the
    checkout path contains "environment", "token", "buffer" or "capability" (for example ~/environment/CanvasTTY).
  • Cause: the saved descriptor's cwd is process.cwd(), and the test scanned the whole file, cwd included, for
    those words.
  • Fix: the scan leaves the descriptor's own cwd value out. Everything else is checked as before.
  • Test: the same file, run from a folder named environment-check: it fails before the fix and passes after.
  • Area: tests.

Verification

Results on this branch (macOS arm64, throw-away HOME/XDG/CLI homes, head 93cc2bb4):

Conflicts with open PRs

Left for you to decide

  • Hooks need review ("Trust all and continue") stays the orchestrator's to choose, as the docs say. One more
    pattern in codexApprovalPrompt would make it the person's.
  • If you would rather let guarded subagents run shell commands without asking, with base protection's guard and the
    decision services as the gate, say so; that variant exists. Fix 4 implements your rule as written.

The Kimi provider runs Kimi Code CLI, which keeps its configuration and
OAuth sign-in in KIMI_CODE_HOME (default ~/.kimi-code); the older kimi-cli
uses KIMI_SHARE_DIR (default ~/.kimi). The isolation layer only knew
KIMI_HOME, which neither CLI reads. So:

- a Kimi launch whose home was moved with KIMI_CODE_HOME (an account home a
  launch plugin hands it, e.g. one sign-in of an account pool) did not get
  that folder as its own writable state, and
- other CLIs inside the layer could read ~/.kimi-code, where Kimi Code's
  refresh token lives.

Kimi's own folders are now KIMI_CODE_HOME (or ~/.kimi-code), KIMI_SHARE_DIR
(or ~/.kimi), KIMI_HOME when set, and the usual named folders; all three
variables move Kimi's home. Nothing else changes.
…atus hooks and tools

Grok Build reads hooks only from GROK_HOME/hooks and Kimi Code only from
KIMI_CODE_HOME/config.toml; Kimi also reads its MCP servers (mcp.json)
and permission rules there. CanvasTTY wrote its lifecycle hook overlays
and Kimi's browser/agents configuration into the default homes only, so
a card a launch contributor started in its own account home (an account
pool) ran without working/idle/needs_approval status and without the
canvastty_browser / canvastty_agents tools.

When a launch contributor sets GROK_HOME / KIMI_CODE_HOME to an account
home below its own data folder (<userData>/plugin-data/<pluginId>, from
the host's record), the launch pipeline reports it as the launch's
account home and the same overlays are written there instead, shared by
the cards in that home and removed when the last one ends. A variable
pointing anywhere else, including another plugin's data, leaves the
launch as before. Inside plugin data every folder from plugin-data down
and every file CanvasTTY writes must be real (no symbolic link), or the
launch is refused. The overlays keep the existing lock, journal, backup
and marker rules, so the person's own settings are restored byte for
byte. The checks live in accountHomes.ts and are shared by both
overlays.

Claude Code, Codex, Qwen Code and OpenCode take their hooks from the
launch's arguments or environment and already work in any home.
…ts orchestrator

An OpenCode subagent on a launch contributor's model keeps bash on "ask"
(a launch contributor's other model keeps bash asking, like accept-edits
elsewhere). When no decision service decides, OpenCode's own prompt waits
in the subagent's card for the person. That part already works: the card
turns needs_approval, Needs attention lists it, and wait_for_agent
returns at once with the prompt in output. Two things stood in the way:

- send_to_agent still typed into that card. Any prompt text ends with
  Enter, which answers OpenCode's prompt, so the orchestrator (on the same
  third-party model) could approve the command for the person. It is now
  refused while the subagent is needs_approval, with a reason that tells
  the orchestrator to ask the person and wait.
- wait_for_agent returned the same pending prompt again on every call,
  so an orchestrator could only poll or give up. It now returns a prompt
  once; a later wait on the same prompt (the card has not changed since)
  waits for the person's answer and then for the subagent's turn, so the
  orchestrator carries on in the same turn once the person has answered.
  A new prompt is reported at once again.

Unchanged: bash stays "ask" for a third-party model in every card; a
decision service's deny still fails the call before OpenCode asks and its
allow (from a plugin the person let allow) still answers OpenCode's
prompt once; no verdict is never turned into an allow; person cards and
OpenCode on its own model are untouched; no new UI.

The test runs CanvasTTY's real OpenCode plugin, runtime gateway, decision
hooks, TerminalManager and orchestration tools: allow answers the prompt,
deny fails the call, no verdict reaches Needs attention and the
orchestrator, send_to_agent writes nothing, and the person's answer
resumes the subagent and the orchestrator's wait.
…s a subagent's approval prompt

Asks are answered by the person, or by a decision service the person
allowed, never by the orchestrator. The canvastty_agents tools already
refuse send_to_agent while a subagent is needs_approval. The control
endpoint did not: on an Orchestrator session's own connection, choose
answered Codex's approval prompt ("1. Yes, proceed") and dismiss answered
it with Escape ("No, and tell Codex what to do differently").

On an orchestrator's connection, choose, dismiss and send are now refused
with NEEDS_PERSON (not retryable, nothing is written) while the worker
is needs_approval, the status each CLI's own approval signal sets. For
Codex's choose and dismiss the menu is also checked: Codex can draw its
approval prompt before its PermissionRequest hook's report arrives, and
choose acts on that exact screen. codexApprovalPrompt matches Codex's
command, edits, permissions, network and MCP approval prompts, not hooks
review, folder trust, model or update menus.

Unchanged: menus that are not approvals stay the orchestrator's; status,
screen and result still read; interrupt still stops its own turn (the
asked call does not run); the person's own app-wide connection, the
card's input and decision services are untouched.

Tests run the real TerminalManager, AgentControlService, gateway and
control CLI request path with an orchestrator's session grant: approval
prompts refused before and after the hook report, non-approval menus
chosen and dismissed, the person's connection still choosing, and
send_to_agent refused at every CLI's approval signal and delivered once
the person answered.
…e UI scale

Some UI chrome kept fixed pixel sizes, so Settings -> Appearance -> UI
scale (0.85-1.25) did not reach it:

- The terminal card's options menu (Don't restore this window, plugin
  card actions) had a 12px font and pixel paddings while the card header
  around it scaled.
- The canvas shortcut-hints panel looked the same at 0.85 and 1.25.
- In Settings -> Plugins the plugin description scaled with the settings
  panel, but the permission list, the contribution rows ("App on
  canvas"), the plugin title, author, version, buttons, module and
  native-hook lists kept fixed 9-17px sizes, so at 1.25 one card mixed
  large and small text.

The menu and the hints panel now take their font size from
calc(Npx * var(--ui-scale, 1)) and size their paddings, gaps, radii and
minimum widths in em; the hints icon is sized in em too. The plugin
settings font sizes use the same calc. At scale 1.0 every size is the
same as before. UI_CONTRACT names the menu and the hints panel among the
chrome that follows --ui-scale.

The title bar is left as is: its height is shared with the native
window chrome (macOS traffic-light position, the startup page) and with
every viewport-bound overlay through --titlebar-height.
…like the reference

The Russian canvas hints and Settings still showed English: the
wheel/pinch hint, the hard-coded "Shift + drag" key cap in the hints
panel and the shortcut reference, the canvas navigation override
description ("wheel, pinch и drag ... Drag колёсиком") and the
Settings > Canvas wheel capture strings and Off/On/Key labels.

- Translate them, reusing the existing on/off keys and adding
  shortcutShiftDrag and canvasWheelCaptureKey; the hints use one word for
  the drag gesture ("перетаскивание"). The group-selection label no
  longer repeats its key cap (ru/en). A test checks that these ru strings
  hold no Latin words besides key names.
- The wheel/pinch and navigation override rows in Settings > Keyboard
  shortcuts wrap instead of cutting their longer Russian labels off.
- The hints panel showed raw bindings ("Alt+ArrowUp") while the shortcut
  reference showed "Alt+↑". Both use one formatter now. It maps key by
  key, so on macOS "Meta+Comma" reads "Command+," instead of the
  reference's ",nd+Comma" (the Comma key was replaced inside "Command").

The canvas wheel and navigation settings' text (only Settings shows it)
moves from the startup string table to canvasNavigationText.ts, next to
Settings, so it loads with the Settings chunk; renderer-bundle-splitting
checks it stays out of the startup bundle.
When GitHub returned a token but the /user request hit the request timeout,
the timeout's AbortError was taken for a cancellation: the poll stopped
silently, the flow was cleared and the status stayed "pending", so Cancel
did nothing. Only this flow's own abort now counts as a cancellation; a
timed-out profile request ends the sign-in as failed.
…or body

A transient 429/5xx from the token endpoint with a JSON body such as
{"error":"temporarily_unavailable"} skipped the transient-status check and
ended the sign-in as an unknown provider error after one answer. These
statuses are now retried with backoff whatever the body says; slow_down
keeps its own handling.
browser_click on an engine without layout returns when element.click()
answers; the fake engine reports the navigation it started afterwards.
Until Page.frameNavigated arrives the tab still shows the old document
as ready, so h.ready() could return before the revision advanced and
the test failed under load. h.ready() now takes the revision to wait
past.
"terminal window descriptors persist atomically without scrollback or
environment" saves a descriptor whose cwd is process.cwd() and then
checks the whole file against /buffer|environment|capability|token/.
Run from a checkout whose path contains one of those words (for example
a worktree named after an environments feature), the cwd alone matched
and the test failed on every run. The check now leaves the descriptor's
own cwd value out; everything else in the file is checked as before.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-22T14:51:06.663232Z
We couldn’t complete this review. Comment @codex review to try again.
15ba2e6 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@BIackFIame

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@BIackFIame

Copy link
Copy Markdown
Contributor Author

Self-review while the bot review fails: fixed three things, each in its own commit with a regression test. Kimi's config.toml (its permission rules and hooks) is now write-protected inside isolation, like Codex's, now that Kimi's home is writable there. send now gets NEEDS_PERSON too while a Codex approval prompt is on screen before its hook report, as the description says. A contributed account home that does not exist yet is created (private, one folder at a time) instead of refusing the launch.

@BIackFIame

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Failed to sample tokens
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

… delegated cards

Agent cards inherited CanvasTTY's whole environment, so every agent, every
subagent and every plugin-started card, isolated or not, received provider
API keys, tokens and organization-level admin keys (OPENAI_ADMIN_KEY,
ANTHROPIC_ADMIN_KEY) exported where the app was started.

Now an agent card never inherits admin credentials (*_ADMIN_*KEY,
*_ADMIN_*TOKEN) or CanvasTTY's own (CANVASTTY_GITHUB_TOKEN and any
CANVASTTY_*TOKEN/KEY/SECRET/PASSWORD); the person's own top-level agent cards
otherwise keep their environment, so CLIs signed in by API key keep working.
Subagents and every card a plugin starts, a plain shell card included since
the plugin drives it, inherit only an allowlist (path, home, user, shell,
locale, terminal, temporary and XDG folders, Windows system folders, proxy
and certificate variables, the CLIs' home folders); what they need beyond
that comes explicitly through a launch contribution's secretEnv. The launch
builders (agent runtime hooks, browser and orchestration bridge, OpenCode
inline config) now read the card's filtered environment instead of the
app's, so they no longer hand the person's OPENCODE_CONFIG_CONTENT to a
delegated agent. A plain shell card the person starts keeps everything but
CanvasTTY's own credentials. CANVASTTY_AGENT_PASS_ENV in the app's
environment lists variables to keep anyway.
…'s shapes

OpenCode's decision bridge sent write, edit and multiedit all as toolName
"edit" with {file_path, content}. A decision service could not tell a
whole-file write from a partial edit, and an edit's replaced text was
dropped, so it could not see what the edit removes.

The bridge now uses Claude Code's names and shapes, which hooks and
decision services already read:
- write: Write {file_path, content}
- edit: Edit {file_path, old_string, new_string, replace_all}
- multiedit: MultiEdit {file_path, edits: [{old_string, new_string,
  replace_all}]}
- bash and apply_patch are unchanged.

Base protection reads file_path for Write, Edit and MultiEdit as before.
A test goes through DecisionHooks: the service gets kind "edit", the
path and both texts, and a MultiEdit outside the project is still
denied. The docs (en/ru/zh-CN, plugin-api.d.ts) list the shapes
OpenCode's tools arrive in.
@BIackFIame BIackFIame changed the title fix: twelve bug fixes in isolation, orchestration, UI, GitHub sign-in and tests fix: thirteen bug fixes in isolation, orchestration, UI, GitHub sign-in and tests Oct 22, 2026
@BIackFIame

Copy link
Copy Markdown
Contributor Author

Added fix 13 to this PR: agent cards no longer inherit credentials from the environment CanvasTTY was started with.

Until now every agent, subagent and plugin-started card got the app's whole environment, so provider keys, tokens and
organization-level admin keys (OPENAI_ADMIN_KEY, ANTHROPIC_ADMIN_KEY) exported in the person's shell reached all of
them, isolated or not. Now:

  • admin keys and CanvasTTY's own credentials (CANVASTTY_GITHUB_TOKEN) never reach an agent card;
  • the person's own top-level agent cards keep ordinary provider keys, so CLIs signed in by API key keep working;
  • subagents and every card a plugin starts (a plain terminal included) inherit only an allowlist (path, home, locale,
    terminal, temp/XDG folders, proxy and certificate variables, the CLIs' home folders); a key they need comes through a
    launch contribution's secretEnv;
  • the launch builders (status hooks, browser/orchestration config) read the filtered environment, so they no longer
    copy the person's OPENCODE_CONFIG_CONTENT into a delegated OpenCode card;
  • CANVASTTY_AGENT_PASS_ENV=NAME[,NAME] in the app's environment keeps listed variables for every agent card.

Behaviour change: a subagent that signed in only through an exported key now needs CANVASTTY_AGENT_PASS_ENV or a
launch contribution. The details are in entry 13 under Isolation in the description. The commit sits after fixes 1 and 2;
#160, #161 and #162 are rebased on the new head with their content unchanged.

The HOME limit row showed the used share as a bare "9%" with a bar and a
reset countdown; "used" was only in the hover/screen-reader text. Codex's own
status line shows "% left", so 9 % used read as 9 % left. The row now says
"9% used · 91% left" (ru «9% использовано · осталось 91%»), and the
hover/screen-reader line "9% used · 91% left" («использовано 9% · осталось
91%»). Usage history's latest observation says the same instead of a bare
percent.

The normalizers were right: Codex app-server usedPercent and Claude
utilization are the used share; a test pins both with fixture payloads.
…a way to the setting

A plugin page whose service could not run because the person had not trusted
its native code (or had its module turned off) only got "Plugin service is not
running.". The host now refuses those requests with a fixed text; frames show
it localized (ru/en), and the host notice carries an Open settings button that
opens Settings -> Agents -> Extension native code (or Settings -> Plugins for a
module). A trusted service that crashed or failed keeps the generic text, now
followed by its last error.
…led outcomes

A started GitHub device sign-in could not be cancelled, retried poll
failures until the code expired, and ended silently when GitHub answered
access_denied or expired_token. A sign-in that could not start showed
the raw IPC error ("Error invoking remote method 'github-auth:start':
...") at the top of the Plugins page, in English in the Russian UI.

- GithubAuthService.cancelDeviceFlow() is public and exposed through the
  existing github-auth IPC (main-renderer only) and preload.
- status() reports deviceFlowState: idle, pending, denied, expired,
  failed or cancelled.
- access_denied, expired_token, unknown provider errors and non-transient
  HTTP errors stop polling with that outcome; OAuth error bodies on HTTP
  400 are read as well.
- Network errors, timeouts, HTTP 408/429/5xx and unreadable bodies back
  off and retry, but five failures in a row end the flow as failed.
- slow_down keeps adding five seconds to every later poll.
- The host clock ends the flow as expired even if GitHub keeps answering
  authorization_pending.
- Settings shows a Cancel button next to the pending code and a localized
  status line (en, ru) for the outcome, using existing classes: denied,
  expired and failed use the Settings error style, cancelled is a plain
  status line. A start or cancel failure shows a localized line ("not
  configured" for a build without an OAuth client); the details go to
  the log.

The new sign-in lines live in PluginSettingsSection (the lazily loaded
Settings chunk), not in the startup string table; renderer-bundle-splitting
checks they stay out of the startup bundle.
@BIackFIame BIackFIame changed the title fix: thirteen bug fixes in isolation, orchestration, UI, GitHub sign-in and tests fix: fifteen bug fixes in isolation, orchestration, UI, plugins, GitHub sign-in and tests Oct 25, 2026
@BIackFIame

Copy link
Copy Markdown
Contributor Author

Added fixes 14 and 15 to this PR, both under UI, after fixes 6 and 7:

  • 14, HOME limit rows: HOME → Model limits showed a window's used share as a bare "9%". Codex's own status line
    shows "% left", so it read as 9% left. The row and its hover line now say "9% used · 91% left" (ru «9% использовано ·
    осталось 91%»), and so does usage history's latest observation. The data was already right; a test pins the Codex and
    Claude payloads.
  • 15, plugin service notice: a plugin page whose native code was not trusted, or whose service module was off, got
    only "Plugin service is not running.". The host now says which one it is, localized, with an Open settings button
    to Settings → Agents → Extension native code (or Settings → Plugins). A trusted service that crashed keeps the generic
    text, now followed by its last error. Nothing is turned on by the notice.

The startup bundle is now 1,691,536 bytes (+2,134 vs main, under the 1,700,000 budget). The details are in entries 14
and 15 of the description. #160, #161 and #162 are rebased on the new head with their content unchanged, and the stack
plus #163 still merges cleanly. Fix 15 meets the coming plugin locale/scale PR only at two adjacent import lines in
PluginFrame.tsx.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant