Skip to content

[Review 133] [3/7] feat: isolate subagent edits and review task changes - #3

Draft
BIackFIame wants to merge 71 commits into
backlog/1-orchestrationfrom
backlog/2-worktrees
Draft

BIackFIame wants to merge 71 commits into
backlog/1-orchestrationfrom
backlog/2-worktrees

Conversation

@BIackFIame

Copy link
Copy Markdown
Owner

Review purpose

Review-only copy of the prepared CanvasTTY series in the contributor's fork. This step covers: [3/7] feat: isolate subagent edits and review task changes.

Related: howdeploy#133

The head is the exact upstream PR commit 2b988218680596776177862ef2fb53daa5549c6b. The base is backlog/1-orchestration at 72accd26e8451e187de4436b7d51a4b22a6a1eaf. The upstream PR is cumulative against upstream main; this review compares only this step against its predecessor, with inherited code available as context.

This PR is for review, not merging. Keep feedback attached to this exact commit. Source validation, live-test limitations and series context are recorded in the upstream PR; no new tests were run when creating this review copy.

tab11pm and others added 14 commits October 4, 2026 18:26
Adds the Files canvas card: a read-only browser and reader rooted either at a
live terminal session working directory or at a folder picked in a native
dialog. Main owns access through a new FileAccessService that canonicalizes root
and target with realpath, rejects absolute paths, `..`, escaping symlinks and
non-regular files, and enforces byte, entry, depth and result caps; the renderer
only receives an opaque root id plus relative paths over the allow-listed
`files:*` channels. Cards persist and restore their root, bounds, open file and
expanded folders, and show an unavailable state when a root is gone.

Text and code are rendered richly: Markdown files render as GitHub-Flavored
Markdown (headings, lists, task lists, tables, blockquotes, rules, inline and
fenced code) and recognized code files are syntax-highlighted by extension.
Rendering is sanitized (no raw HTML, event handlers or dangerous schemes; no
innerHTML), http(s) links open in the built-in browser, only embedded `data:`
images render, and files above the ~250 KB threshold fall back to plain text
with a notice. The heavy Markdown/highlight renderers and the card are
lazy-loaded to keep them out of the startup bundle.

Excluded: the unrelated custom-agents WIP is intentionally not part of this
branch. Also raises the renderer static-graph budget in
tests/renderer-bundle-splitting.test.mjs (still well under the old ~1.6 MB
single chunk) to account for the new card's startup orchestration.
`FileAccessService` registered roots with `realpathSync` but canonicalized
targets with the async `realpath`, which on Windows can return a differently
cased or 8.3-shortened path. The prefix containment check then treated the root
itself (and every child) as outside the root, so listing/reading failed. Resolve
targets with the same `realpathSync` used at registration and compare
normalized (case-insensitive, `\\?\`-stripped) paths on Windows.

Windows CI: tests/file-access-service.test.mjs and files-ipc-security.test.mjs
were the 13 failing tests; they now pass on Linux and should on Windows.
@BIackFIame

Copy link
Copy Markdown
Owner Author

@codex review

Please review this PR's diff for correctness, regressions and security issues. Review only; post findings on GitHub without changing code.

This is the contributor-fork review for howdeploy#133, at the identical head commit 2b988218680596776177862ef2fb53daa5549c6b. The PR base isolates this step of the series; use inherited code as context and focus findings on problems introduced by this diff. Include the concrete failure scenario and affected code in each actionable finding.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 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 ✅ Completed 2026-10-08T03:19:09.765889Z 8d496bb 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b98821868

ℹ️ 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".

Comment thread src/main/services/SubagentWorktreeResolver.ts Outdated
Comment thread src/main/index.ts Outdated
Comment thread src/renderer/src/features/terminal/PluginChangesReviewDialog.tsx
@BIackFIame

Copy link
Copy Markdown
Owner Author

@codex review

Additional verification round 1 of 5. Review current head 321df1f. The reviewed orchestration fixes have been merged from the parent branch. Initial findings were addressed: worktree selection respects the child provider, mutation review results replace stale entries, and turn-start checkpoint capture gates hook acknowledgement with bounded cancellation that discards late snapshots. Please review correctness and regressions across these integrations, including cancellation and provider boundaries. Report actionable defects; please do not apply changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 321df1f7cb

ℹ️ 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".

Comment thread src/main/index.ts Outdated
Comment thread src/main/services/AgentControlService.ts Outdated
@BIackFIame

Copy link
Copy Markdown
Owner Author

@codex review

Additional verification round 2 of 5. Review current head d1fdef1. The previous findings were fixed: checkpoint cancellation follows actual turn invalidation, and worktree retries restore review tracking without duplicate observers or premature review. Please focus on concurrency, cancellation, and resource ownership across worktree retry, checkpoint, and review workflows. Report actionable defects; please do not apply changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1fdef11cd

ℹ️ 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".

Comment thread src/main/index.ts Outdated
Comment thread src/main/ipc/registerBacklogIpc.ts Outdated
@BIackFIame

Copy link
Copy Markdown
Owner Author

@codex review

Additional verification round 3 of 5. Review current head 69ff25b. Prior findings were fixed: failed checkpoint attempts persist for the turn, and restoration holds a scoped owned-process lease with input/launch gates while the complete Git transaction shares the capture queue. Review security and provider/device boundaries, error cleanup, cancellation, and correctness of the fixes. Live restoration on unsupported suspension platforms is deliberately refused. Report actionable defects; please do not apply changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69ff25bf9e

ℹ️ 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".

Comment thread src/main/services/TerminalManager.ts Outdated
@BIackFIame

Copy link
Copy Markdown
Owner Author

@codex review

Additional verification round 4 of 5. Review current head dbb7414. The previous isolated-environment finding was addressed with captured launch-time ownership/scope evidence; related remote/container workloads are refused for restoration even if their local wrapper exited. Please review compatibility and error handling across local worktrees, checkpoint capture/restore, retry/review state, and plugin reconfiguration. Report actionable defects; please do not apply changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbb7414fbc

ℹ️ 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".

Comment thread src/main/services/TerminalManager.ts Outdated
Comment thread src/main/services/GitCheckpoints.ts Outdated
Comment thread src/main/services/PluginChangeReviews.ts
@BIackFIame

Copy link
Copy Markdown
Owner Author

@codex review

Additional verification round 5 of 5. Review current head b94ac81. The previous findings were addressed by persisting host isolation evidence, bounding large Git previews, and accepting valid large review actions within the unchanged plugin transport limit. Please review final regressions and correctness across app reload, checkpoint capture/restore, worktree mutation/retry, and plugin review actions. Report actionable defects; please do not apply changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b94ac815b5

ℹ️ 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".

Comment thread src/main/services/GitCheckpoints.ts Outdated
@BIackFIame

Copy link
Copy Markdown
Owner Author

@codex review

Additional verification round 6: first extra pass to verify the final restore fix. Review current head 189be67. The fifth round found that newly staged files could survive checkpoint restoration. Please verify the corrected restore behavior, staged versus unstaged state, preservation of unrelated untracked files, and the safety snapshot. Also check nearby regressions and error handling. Report actionable defects; please do not apply changes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 189be67d04

ℹ️ 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".

Comment thread src/main/services/GitCheckpoints.ts Outdated
howdeploy and others added 25 commits October 12, 2026 15:31
Prepare CanvasTTY 1.8.0 from the approved main integration, preserving all contributor history and the maintainer's UI/minimap patches.

- Bump package and lockfile versions together and document the user-visible changes in all three changelogs.
- Fix the release-blocking Hermes read path: use an immutable read before opening a stopped WAL database without side files, so collection does not create a WAL file in the source directory. The existing live/stopped/orphaned WAL regression covers this behavior.
- Refuse symlinked or replaced usage-history paths before reading on Windows as well as Unix; retain the existing symlink regression.
- Adapt Windows Codex fixture launches to the existing batch-launch path, restrict chmod-specific assertions to platforms that implement Unix permissions, and update the history overlay assertion for the merged material dialog guard. Cross-platform accounting and symlink checks remain enabled.

Validation: `git diff --check`, matching manifest/lockfile versions, and a clean local Linux AppImage/DEB build passed. The Linux update manifest matches both artifacts' sizes and SHA-512 hashes. CI tests passed on Linux and Windows; Windows and macOS package checks, both mascot-pipeline jobs, and the macOS PTY job passed. A subsequent Linux packaging step failed with HTTP 504 after the application build; that single job is being rerun. The tagged release workflow repeats the required release checks, builds native installers for all three platforms, and verifies the complete signed update artifact set before publishing. No local UI check or real-device update test has been run.

Release notes credit @tab11pm, @SaneSanders, @BIackFIame, @pepechka-kun, @CLOSETTTY, @teo-nex, @Yt4aZaveta, and @theorangecolorv2. The orchestration additions in howdeploy#132–137 remain excluded.
node-pty reports a process killed by a signal as exitCode 0 plus the
signal. The signal was dropped, so an agent killed with SIGKILL (OOM,
kill -9) was recorded as "done, exit 0": wait_for_agent returned done,
get_agent_result said done, and the saved session restored as exited.

Record such an exit the way a shell does, as 128+signal (SIGKILL -> 137),
mark the session failed, and put the signal name first in the failure
details. Every reader of the exit (wait_for_agent, get_agent_result, the
session store, the HOME failure summary) already derives its state from
the exit code, so they now agree. Exits CanvasTTY causes itself (closing a
card, quitting) are unaffected: those sessions are removed before
the kill, and the exit handler ignores them.
The 1.8.0 release build caught a macOS Files reader bug before publication: the descriptor-path check accepted `/dev/fd/<n>` as if it were a resolved filesystem target and rejected every regular file as outside the registered root.

Ignore descriptor-device paths that do not resolve to a file path, and compare the opened file's device/inode with the current canonical path before reading. Root containment, O_NOFOLLOW, regular-file checks, and Linux descriptor-path checks remain in place. A missing path returns the existing unavailable state. Add a regression simulating descriptor devices on every test platform.

Bump to 1.8.1 and update all changelogs. The failed 1.8.0 tag remains unchanged and no 1.8.0 release was published. The accepted contributor history and maintainer patches remain intact.

Validation: the 1.8.0 release tests passed on Linux; its preflight tests and packaging passed on Windows and macOS. The full release macOS unit suite exposed this bug. A new tagged build will repeat native packaging and all required checks on all three operating systems before publication. No local UI check has been run.
…h files

Closing a card started the run cleanup and the removal of runs/<id> at the same time, and a launch still being prepared (plugin prepare, environment wrap) could write run files after the folder was gone. A throw from the environment wrap also skipped the planned and contributed cleanup.

Contributed launches are now tracked. Closing waits for pending launches, then the serialized exit/restart cleanups, then removes the run folders each pipeline created for this card object, and the card folder only when it is empty, so a card brought back under the same id keeps its new run. An unadopted launch releases its planned spawn and contribution in a finally block, and a launch whose card was closed or restarted in the same tick never asks its plugins. Run folders are removed with retries for files Windows still holds open.
A subagent spawned without launch options ran on its CLI's own default
model and sign-in (for OpenCode the hosted OpenCode Zen model), even when
its orchestrator ran on a model the person picked through a plugin's
launch options. A plugin the spawn does not name now keeps the
orchestrator's own values for a subagent of the same CLI; repeating the
person's choice needs no delegable declaration, and then the normalized
values must equal the orchestrator's exactly (missing keys come from the
orchestrator, never from the plugin's defaults). A subagent of another
CLI must name a plugin whose contribution set the orchestrator's model,
and spawn_agent reports servedBy (CLI, requested model, inherited or
explicit options).

- Recognise every form a CLI takes for its model: --model and --model=,
  -m and -mX for OpenCode, Codex, Qwen, Kimi and Grok, and for Codex also
  --profile/-p and -c/--config setting model, model_provider or profile.
- Refuse a second model (an explicit model next to a plugin that sets it,
  or two plugins that both set it; OpenCode crashes at start on two
  --model values) only for a subagent's launch. A person's own launches
  and restores start as before.
- Inherit an empty choice (a plugin without fields) as chosen. Leave out
  an inherited plugin that was turned off since, unless it set the model
  (then refuse); refuse with a clear reason when its fields changed.
- Reset modelSetBy at the start of each launch.
- Describe inheritance, servedBy and the refusals in the orchestrator
  skill and docs.

The core stays plugin-agnostic: it reads the plugin from the launch
options and the contribution, never from a fixed plugin id.
OpenCode drops every key that arrives while its full-screen view starts
(a few seconds). CanvasTTY typed spawn_agent's first prompt, send_to_agent
and plugin sessions.send as soon as the PTY existed, so an OpenCode
subagent sat at an empty prompt until the orchestrator gave up.

deliverInput now waits, within the same bounded wait it already had for
the launch, until OpenCode can take input: the terminal control sequence
that shows its text cursor on the alternate screen once its prompt has
focus, or a lifecycle hook from its plugin, counted only 2 s after the
launch's first one (the plugin can report before the prompt takes keys).
Only DEC private mode sequences are read; human-readable screen text is
never parsed and no status changes. Other CLIs keep getting input when
the PTY exists.

- The wait is used on macOS and Linux. Windows' pseudo console may not
  pass those sequences, so there input is typed when the PTY exists, as
  before.
- spawn_agent (checks, launch and readiness together) and send_to_agent
  wait at most 45 s, so the orchestrator gets the reason and the session
  id, not an MCP client timeout. Nothing is typed then.
- A handoff waits up to 15 s for the CLI to take input instead of
  failing at once; a caller that does not wait is told the state.
- Sends to one card are delivered in the order they were made, a queued
  send never reaches a later launch, and a send cancelled while it waits
  behind another settles at once and types nothing.
wait_for_agent accepted up to 600 seconds, but the MCP clients of several
supported CLIs (OpenCode among them) end a tool call after about 60
seconds, so an orchestrator that asked for a long wait got a failed call
instead of a result.

A call now asks for at most 100 seconds (default 50). Unless the
orchestrator's CLI is known to let a tool call run longer, a call returns
after at most 50 seconds. A timeout carries the status and the masked
output as progress plus a message to call again, and the tool
description in both catalogs, the orchestrator skill and the docs say
that a timeout means the subagent is still running.
Base protection is documented to refuse use of CanvasTTY's private data,
but its hooks only saw shells and file writes: Claude Code's Read, Grep,
Glob or LS, and the Qwen Code and OpenCode equivalents, could read
provider-secrets.bin, the control token or another account's home
without a check.

While base protection is on at launch, the decision hook now also
matches those read-only tools (Claude Code and Qwen Code matchers,
OpenCode's guard through CANVASTTY_RUNTIME_DECISION_READS). With base
protection off, a decision plugin's fail-closed hook still covers only
shells and writes, so reads never wait on the gateway. They map to a
read action that is checked only against the private data, with the
same rules as a shell read: the agent's own account home stays its own,
a search walks its folder, and nothing outside the private data is
refused or asked about. Decision services are not asked about reads.

OpenCode's grep without a path is sent as a search of ".", and glob's
pattern is always sent. Qwen Code's read_many_files is refused when its
paths and include globs together exceed 32, instead of checking only
the first 32, and include globs are checked. A cut read-only call is
refused like a cut shell call. Codex has no read-only file tool in its
hooks; it reads through its shell, which is already checked.

The docs now say base protection is best effort: it does not see paths
built at run time or folder walks started above the app's data, so a
project at the home folder needs agent isolation to keep the data
private.
The private-data rule matched globs one component at a time, so `**`
stood for a single folder and `cat ~/Library/**/provider-secrets.bin`
was allowed; brace expansion was not seen at all, so
`cat <userData>/provider-secrets.{bin,}` was allowed too.

- `**` now matches zero or more folders. A glob that only passes by the
  app's folder on its way to other names (`~/**/*.ts`) is not counted.
- Unquoted braces in a shell word, and braces in a Glob tool pattern,
  are expanded (at most 64 words) before matching.
- On macOS and Windows a glob matches private names in any letter case.

Base protection also had limits that let the rest of a call through
unchecked. A call now is refused ("uncheckable") instead when:

- a path field is not a string, or is longer than 4096 characters after
  folding `./` and `a/..` padding;
- read_many_files names more than 32 paths and include globs, or an
  apply_patch touches more than 256 files or a too-long path;
- the private-path budget (now 1024 checks), the brace-expansion limit,
  the path candidates in one word or heredoc (256), or the nesting of
  shells and substitutions runs out;
- checking a shell, file-edit or read call fails with an error. Before,
  an exception in the analyzer let the call through; other tools still
  get no opinion.

An apply_patch whose command is longer than 4096 characters is read in
full; before, it was treated as touching no file.
bubblewrap bound the shared gateway socket folders writable into every
isolated agent, so an agent could replace a gateway socket and receive
other agents' requests. It also took a socket folder from dirname() of
every CANVASTTY_*_ADDRESS in the launch environment, which holds the
person's variables and plugin contributions: $HOME/foo.sock bound all of
HOME after the tmpfs masks, and an address inside plugin data reopened
every account home. The control grant folder was read from the same
environment.

Socket folders are now bound read-only (connect() needs no write access
to the folder) and come only from the sockets CanvasTTY's own gateways
prepared for the launch; the control grant only from the control
environment the host gave an orchestrator. A socket that must not be
read is masked with /dev/null, and a gateway socket folder that holds a
hidden path is refused. On macOS the seatbelt Unix-socket allow list is
built from the same gateway sockets.
… them

With a project that contains userData (the home folder), an orchestrator
could start a subagent in <userData>/plugin-data/<plugin>/homes. The
isolation layer reopened any project at least two folders deep in plugin
data, so that subagent could read and write every account's sign-in.

A subagent's folder may no longer be inside CanvasTTY's private data,
unless the project itself is a folder inside that same plugin's data (an
environment's worktree). The layer reopens a project inside plugin data
only when the host placed the launch there: a folder an environment
prepared inside its own plugin's data, or the folder the person chose for
the card the launch descends from. Anything else is refused with the
reason.

Also: a gateway socket folder that contains a writable folder is refused
instead of mounted over it; a trailing separator in a CLI home variable
names the same account home; the docs list which variables count as an
account home, in English, Russian and Chinese.
…ct that contains it

When an isolated agent's project contains CanvasTTY's userData folder (a
project at the home folder), the whole project was writable, so the app's
settings.json and the plugins folder were writable to the agent; only the
private parts were hidden.

The isolation paths now carry readOnlyFolders (userData when a writable
folder contains it) and fixedFolders (its parents inside that folder).
Seatbelt denies writes in userData after the project allow, re-allows the
writable folders inside it (a selected account home) and denies renaming
or replacing the parents. bubblewrap binds each parent onto itself (a mount
point cannot be renamed), mounts userData read-only over the project and
binds the writable folders inside it again. A project located inside
userData is unaffected.
…worktrees

Inside the isolation layer only a repository's hooks, info/attributes and
an existing .git/config were protected. .git/commondir (where git reads the
shared config and hooks from), .git/config.worktree and the linked
worktrees' records in .git/worktrees stayed writable, and the path-based
rules could be sidestepped by renaming .git, .git/hooks or .git/info away
and back.

Seatbelt now denies writes to commondir, config.worktree and worktrees/ in
any repository of the project, existing or not, and denies renaming or
removing .git, .git/hooks and .git/info. For an existing repository the
paths list config.worktree and worktrees/ (Linux mounts them read-only,
with neutral placeholders when missing) and an existing commondir; on
Linux the project's .git is bound onto itself so it cannot be renamed.
Objects, refs, the index, logs and HEAD stay writable.
Base protection treated anything inside the working folder (and the
agent's config folder) as the agent's own before it checked
CanvasTTY's private data. With the project at the home folder, or at
any folder that holds the app's userData, an agent's shell command or
file tool could read or overwrite provider-secrets.bin, the control
token, other account homes and the gateway records. A project in a
temporary folder likewise reopened the gateways' socket folders.

A folder is now the agent's own only below the private data: a
private path or socket folder inside the project or config folder
stays private. A folder inside a private path (the account home the
agent was launched with) is still its own, and its neighbours are not.
A recursive walk of a folder in the app's data that holds private
paths is refused wherever the project is.

The per-card Claude Code draft folders (<userData>/claude-draft) are
listed as private data too; nothing creates them yet.
When the agent the person started works at the top of a Git project and
a trusted environments plugin offers the worktree kind for the
subagent's CLI, each subagent (spawn_agent or the control connection's
create) runs in its own worktree from that plugin. The plugin's prepare
now receives the task's project as projectRoot. spawn_agent gains
isolate: "worktree" to ask for one explicitly; it is refused when no
plugin provides it.

The parent and the person's depth, child and live limits are checked
again after the plugin answered, so subagents started together cannot
pass the live limit while their environments resolve.

Under isolation a project inside plugin data is first refused when
CanvasTTY did not place the launch there, then when it is not a linked
worktree validated against the task's repository. An agent card started
in a Git-worktree environment keeps the folder the person chose (saved
with the card as projectRoot, only with an environment); its launches
and its subagents' are validated against that repository, not against
the worktree itself. A folder inside plugin data that the person chose
for a card is opened as placed and need not be a linked worktree.
… immutable

A database without -wal/-shm was always opened with immutable=1, which skips locking and hot-journal checks, so a rollback-journal database could be read while Hermes was writing it. Use the immutable read only when the header says WAL (bytes 18 and 19 are 2) and no side files exist; open other databases with mode=ro and retry SQLITE_BUSY a bounded number of times before skipping the poll.
…edential rules

Held values were masked leftmost-first and the scan jumped past each match, so a second held value overlapping the first showed its suffix. Masking held values before the generic rules could also erase a label such as password or Authorization that a rule needed, leaving the credential visible, and each call allocated text-sized typed arrays. Find held values, JSON values and credential shapes as source ranges on the original text and mask the union of overlapping ranges; search held values with indexOf/KMP streams and a ring buffer instead of text-sized arrays; count entropy characters without regex match arrays. The empty-registry path is unchanged.
…ere read

The Files reader and the usage history reader bind an opened descriptor to
its path by dev/ino. As numbers, 64-bit file ids (NTFS, some network file
systems) lose precision, so two different files could compare equal.
Compare bigint stats instead.

The history reader also compares the pre-open lstat with the descriptor, so
a file swapped between the symlink check and the open is refused rather
than read and then quarantined. When invalid records are dropped, the
original is preserved from the bytes read through the checked handle
(exclusive create, mode 0600) instead of a second copy by path.
A card action that answers with a review now opens it in a native modal
dialog that App renders inside the themed tree, so it reads the theme
tokens and root --ui-scale. Subtasks are a segmented row; the file list
(virtualized, one all-files checkbox) and the diff sit side by side
(stacked below 760px), added and removed lines tinted with the success
and danger tokens. A conflicting file offers Current version / Agent
version above both texts.

- One primary action (Accept selected) and one secondary (Reject
  subtask); the task-wide and whole-subtask variants are in an overflow
  menu (first enabled row focused, Arrow Up/Down between enabled rows).
- Accept and reject run only after an inline confirmation that holds the
  group, files and versions of the moment it opened; confirm re-checks
  them and asks again when they changed.
- A group may carry active: true (its agent is still working): its files
  are not preselected, what the plugin refuses until the agent finishes
  is disabled with the reason, and the plugin's note is an informational
  notice. Disabled buttons stay readable on every theme.
- Failed plugin calls show a localized line, details go to the log; Git
  statuses get localized labels. The orchestrator's own turns no longer
  refresh the review and drop the person's choices.
- Card actions can be declared "menu": false (not listed in the card
  menu, refused without input), and may add "title.ru"/"title.en" (at
  most 40 characters) shown in the app's language. Schema,
  plugin-api.d.ts and docs/plugins (en/ru/zh-CN) document both.
- Bidirectional formatting controls are removed from review text and
  refused in file names.

While the dialog is open the native Browser is hidden and canvas
shortcuts do not fire; focus returns to the card's options button. The
dialog frame is a shared card-dialog base. The host and its Russian and
English texts are a separate chunk, so the startup bundle carries none of
them; App preloads that chunk once it is idle and renders the host
directly, without a Suspense boundary (React holds a suspended reveal for
up to 300 ms), so the dialog opens as soon as the plugin answers. Icons
are the vendored Lucide set.
For a live agent in auto or YOLO whose folder is the top of a Git
project, the host saves a rollback point once per turn: when the
authenticated lifecycle hook reports a turn start (the runtime gateway
holds that hook's answer for at most 750 ms, fail-open, cancelled by a
newer signal, a disconnect or a revoke; the HTTP barrier always answers)
and, when no turn is known, before a permission decision. A turn is the
hook's turn id, or lasts until the agent stops or the person submits
input while the card is not working. A failed attempt is not retried
within its turn.

A point is a git stash create snapshot (or HEAD) plus its index. Inside
the barrier only the snapshot, its ref and the registry row are taken
(~130 ms on a 2000-file, ~33 MiB repository); the trees are copied
afterwards into host packs chained to the previous point (a complete
pack every 16). All data lives in userData/checkpoints, private to the
app and hidden from isolated agents. Retention: 50 per card, 1000 total,
2 GiB of packs; a new point's time is after every registered point's.
Closing a card and cards known not to come back drop their points;
folders that cannot be found (an unmounted volume) keep theirs; an
unreadable registry is set aside, not fatal.

Git runs per repository queue with a stripped environment and hardened
config (--no-replace-objects, no hooks, no external diff, every filter
driver blanked, attributes from a fixed tree). Projects whose files use
a filter attribute (Git LFS, git-crypt) are refused, and the check runs
again after the snapshot.

Restore puts back tracked files and the index; HEAD and the branch stay.
It is refused while any card in the folder runs or launches, for
submodules, untracked files in the way, assume-unchanged/skip-worktree
entries, cards related to an isolated plugin environment, and when the
before-restore safety point cannot be taken. While it runs the folder's
cards take no input (typed or replayed: the turn counters and this check
live in writeInput, the one input write), nothing launches there, closes
wait and card actions on the folder are refused. A case-only rename restores on
case-insensitive file systems; an unset core.ignorecase reads as false.
Previews run in their own lane, are bounded and show the restore
direction (a/ is the file now, b/ after the restore). The main window
reaches list, preview and restore over IPC; refusals are stable codes.
An agent card's options menu gets "Rollback points…" when its folder can
have rollback points (an auto or YOLO agent at the top of a Git project)
or once some exist; the options button shows only when its menu has an
item. It opens a native modal dialog on the shared card-dialog frame,
rendered by App inside the themed tree: the card's points with localized
time and kind, and a preview of the selected one.

- The preview lists changed files and a bounded diff (2000 lines and 500
  file names, then "Show more"), classified by position, with a legend:
  "+" lines come back, "-" lines are removed. Added and removed lines use
  the success and danger tokens, headers are muted. Bidirectional
  controls are removed.
- Restore needs an inline confirmation; the confirmation and the outcome
  line reserve the same three lines, so the dialog keeps its height. The
  main process answers with codes that the dialog shows as localized
  text; the preview is read again after a restore.

While it is open the native Browser is hidden and canvas shortcuts do
not fire; focus returns to the card's options button. The dialog and its
Russian and English texts are a separate chunk (only the menu label is in
lib/i18n.ts), preloaded once the app is idle and rendered directly like
the change review, so the first open is not held back.
wait_for_agent, get_agent_result and observe_agent returned the raw PTY
tail, which for a full-screen TUI such as OpenCode is cursor moves,
colours and spinner redraws, often starting mid-sequence. They now render
the masked scrollback window (the last 65,536 characters) through the
existing headless xterm at the session's size and return the visible
text, masked again and capped as before (observe_agent within its
maxChars). A cut window starts at its first escape or line break, so the
rest of a cut sequence ("18;9H") is never printed as text.

exitLines were the regex-stripped last 16 KiB, so a TUI that redraws only
the cells that changed read as its redraws joined together, and a line
drawn early and never redrawn (the model's status line) was lost. They
are now rendered by the same headless terminal from the same window,
masked before and after, and the three tools return the same exitLines.
An isolated agent could read every plugin's private data folder
(<userData>/plugin-data), including every CLI home and token an
accounts plugin keeps there, not only the account it runs on.

plugin-data is now private app data for the isolation layer. The one
folder reopened is the CLI home a chosen launch contributor started the
agent with: the provider's own home variable set to an exact absolute
folder strictly inside that contributor's host-assigned data folder.
The host derives it from the contribution; a plugin cannot name a grant.
AgentIsolation checks it again when the layer is built (real folders,
no symbolic links from plugin-data down) and refuses the launch
otherwise, giving the reason once. A project inside plugin data is
reopened as that one folder.
Claude Code keeps its Bash tool output and external-editor files in
${CLAUDE_CODE_TMPDIR:-/tmp}/claude-<uid> and ignores TMPDIR. The isolation
layer does not make /tmp/claude-<uid> writable, so every shell call of an
isolated Claude failed with "Operation not permitted".

Set CLAUDE_CODE_TMPDIR to the launch's private temp folder for Claude on
both layers (seatbelt and bubblewrap). The shared /tmp/claude-<uid> is not
granted: all of the person's Claude sessions use it, and an isolated agent
could otherwise rewrite another session's task output.
…card's own account home

An accounts plugin keeps every account's sign-in in CanvasTTY's plugin data
folder. Base protection's private list had account-homes but not
plugin-data, so an agent outside the isolation layer (a person-launched
card, or one with isolation off) could read another account's tokens.

Add plugin-data to the private list. The account home the card's launch
plugin selected (the home the launch pipeline already validates for the
isolation layer) is recorded with the card and counts as one of its own
config folders, as CLAUDE_CONFIG_DIR does, but only while the CLI is still
started with it. Other accounts' homes, other plugins' data and the
listing stay refused; a project CanvasTTY placed inside plugin data keeps
its own files. plugin-data is not added as a name marker, since such a
project names that folder in its own paths.
A card action can now answer with a structured, plain-text review of
file changes grouped per agent, and the app window can call the action
again with a bounded JSON input (pages, selected files, conflict
choices).

The host normalizes the review: at most 512 KiB, 24 groups and 400
files, text masked and stripped of control characters, file paths that
masking would change are refused, and accept/reject action ids must be
the plugin's own declared actions. The input is a JSON object of at
most 1 MiB, copied before dispatch and never used as the session's
identity. A service may declare up to 16 card actions.
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.

7 participants