fix(windows): verify LPAC, edit ACLs exactly, and correct 0.10.1 notes

Hosted Windows on e0a758ff named both remaining causes of its 91 failures.

LPAC verification: the probe token reported TokenIsAppContainer=1, but
GetTokenInformation(TokenIsLessPrivilegedAppContainer) failed with
ERROR_INVALID_PARAMETER on the runner. When that query fails, Codewhale now
reads the token's WIN://NOALLAPPPKG security attribute that the LPAC opt-out
adds (TokenSecurityAttributes; the attribute NtObjectManager uses for
LowPrivilegeAppContainer). Every pointer in the returned structure is checked
for alignment and bounds; anything unreadable or absent still refuses, and the
AppContainer SID and zero-capability checks are unchanged.

ACL editing: SetSecurityInfo re-derived inheritance and turned a file's
inherited ACEs into explicit copies after a grant/retire cycle (the retirement
test's final file ACL assertion). edit_acl now builds the exact DACL (one
explicit allow entry merged or removed, every other ACE copied byte for byte),
keeps SE_DACL_AUTO_INHERITED/SE_DACL_PROTECTED (requesting
SE_DACL_AUTO_INHERIT_REQ so the kernel preserves them), and writes it with
SetKernelObjectSecurity on the pinned handle, which never propagates to
children. Each edit reads back the control bits and, on retirement, fails if
the SID remains.

Shared data directory: because the kernel write cannot propagate, ACL targets
no longer need MAXIMUM_ALLOWED, whose DELETE right collided with any live
host using the directory as its current directory ("grant data directory:
os error 32"; also two sessions sharing a data directory). Targets now request
READ_CONTROL|WRITE_DAC|FILE_LIST_DIRECTORY|FILE_READ_ATTRIBUTES; read-only
sharing still blocks rename, delete and data writes while editing.

macOS: the compiled Bun memory-cap test raced heartbeat supervision (600 ms
hang timeout) against the jetsam limit it exists to prove. Memory-cap tests
now use production hang detection.

Release notes: an independent audit of every claim in the 0.10.1 CHANGELOG
entry, each finding refuted or confirmed by a second reviewer, corrected 13
statements (per-entry plugin activation, router evidence in the Turn
Inspector rather than /status, presets that test, code_mode, the shell tool's
PowerShell flag, Ratatui adoption scope, idle polling, stopship tool_search,
TUI-only audit.log verdicts, SSH fingerprint refusal timing, git helper scope,
verifier env). The Native sandbox sentences now match the source: Linux and
Windows refuse activation when the sandbox cannot be verified. Same fixes in
docs/EXTENSIONS.md, docs/CONFIGURATION.md and their zh_hans copies. Adds
the #6828 MCP discovery entry.

Source14 verification (3,792 rows; owning HEAD cf033337 and index 5983aa54
unchanged):
- npm test and npm run check:web: 1,184 passed / 0 failed / 7 explicit skips.
- macOS: compiled Bun, Bun and Node memory-cap tests 3 runs x 3 cases, 9/9
  passed; compiled and Node containment plus launch plan 3/3 passed, with
  compiled containment, memory and controller-positive/OS-denied network
  markers (compiled host rebuilt from the current bundle f416e2c1).
- Windows: cargo check --target x86_64-pc-windows-gnu (zig as C compiler) of
  codewhale-config and codewhale-tui lib and tests is clean under
  deny(warnings). Two adversarial reviews: the ACL writer (FIX_FIRST finding
  applied) and the LPAC/access changes (SAFE_TO_SHIP, low findings applied).
- Release checks: versions in sync; OHOS deps OK; feature release notes OK
  (38 linked issues); 26 of 28 crates package and verify in a publish dry run.

Hosted e0a758ff: Safety, Lint (runtime-contract and persistence budgets),
Test (ubuntu-latest), CodeQL and npm wrapper smoke passed; macOS 18,741/1;
Windows 17,952/91. Not proven here: Windows runtime of these repairs. Open:
codewhale-tui/cli crates publication needs codewhale-ratatui 0.1.0 on
crates.io first. This is source integration, not release, deploy, provider or
install approval.

Evidence: artifacts/gpt61-0101-takeover-20261002/parent-source14-application,
parent-source14-local, parent-source14-npm-web-actual, windows-gnu-check,
publish-crates-dry-run.

Signed-off-by: CodeWhale Bot <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-03 20:38:58 -07:00
1 parent e0a758ffed
commit 02fcc3e07d
9 files changed
+516 -121

No files matched your search

+56 -28
View File
@@ -31,8 +31,14 @@ note below before upgrading.
reviewed and enabled first. The host is sandboxed on macOS (Seatbelt) and on
Linux with bubblewrap, with no direct network and no reads of the protected
credential locations (Codewhale's secret stores and the Codewhale, Codex and
DSH homes); if bubblewrap is missing or cannot start, it runs unsandboxed and
says so. On Windows it always runs with your user permissions. Other files you
DSH homes). A Native extension starts only after that sandbox is verified at
launch: if bubblewrap is missing or its namespace probe fails, activation is
refused with the concrete error. Codewhale's own pinned built-in host modules
keep a diagnosed exception, and every effect still needs a Rust operation
ticket. On Windows a Native extension starts only in a freshly created Less
Privileged AppContainer with no capabilities, after Codewhale checks its
token and a real file and network probe; otherwise activation is refused.
Other files you
can read, such as project `.env` files, stay readable, and plugins sharing the
host can interfere with each other, so enable only plugins you have reviewed
([docs/EXTENSIONS.md](docs/EXTENSIONS.md)).
@@ -79,9 +85,10 @@ note below before upgrading.
The Rust backend stays the default until platform and rollout gates pass;
its protocol adapters remain through the compatibility window.
- A plugin can declare several `native` entries (`native.paths`, up to 64). They
activate in order as one plugin, so one disable, review change or crash takes
all of them down, and if one entry fails to activate, nothing from the entries
before it stays registered.
activate in order under one owner, so one disable, review change or crash
takes all of them down. If one entry fails to activate, only that entry is
withdrawn: entries that already activated stay registered, and the plugin
fails only when no entry activates.
- Plugin settings and context: `[plugins."<name>".config]` in your own
`config.toml` is passed to the plugin's `apply(ctx, config)` and checked
against its exported `Config` schema; a project's `.codewhale/config.toml`
@@ -163,9 +170,10 @@ note below before upgrading.
([#6580](https://github.com/Hmbown/Codewhale/pull/6580), thanks @gaord).
- Official model routing: `/router` (also `/model router`) sets up the Auto
router with presets: Jev (TypeSafe's decision model, via OpenRouter or a
TypeSafe key), your provider's fast tier, Off, or Custom. Each preset makes
one test call before it saves, `/status` shows the router's choice, cost and
latency, and a failing router is shown as failing
TypeSafe key), your provider's fast tier, Off, or Custom. Jev and Fast each
make one test call before you save, the Turn Inspector (Ctrl+Alt+O or
`/turn inspect`) shows the router's choice, cost and latency, and a failing
router is shown as failing
([#6525](https://github.com/Hmbown/Codewhale/issues/6525)).
- ChatGPT sign-in uses Codewhale-owned protected credentials and a model roster
fetched for the signed-in account. Account, workspace and issuer changes
@@ -176,7 +184,9 @@ note below before upgrading.
`execute_tools` programs can call MCP tools, and each nested call passes the
same approval gate as a direct call, pausing the program for approval when
needed. Every nested call keeps its receipt, including calls that finish
before a deadline, and `code_mode = false` turns it off. `codewhale mcp list`
before a deadline, and `[features] code_mode = false` stops offering
`execute_tools` up front (it stays reachable through tool search).
`codewhale mcp list`
and `codewhale doctor` warn when a user MCP server duplicates the built-in
Computer Use bundle
([#6562](https://github.com/Hmbown/Codewhale/issues/6562),
@@ -189,9 +199,11 @@ note below before upgrading.
### Changed
- On Windows, every PowerShell command Codewhale starts now passes
- On Windows, every PowerShell command the shell tool starts now passes
`-ExecutionPolicy Bypass` for that process only, so a local `Restricted` or
`AllSigned` policy no longer blocks multi-line commands. A policy set by
`AllSigned` policy no longer blocks multi-line commands. (`.ps1` script tools
still start as `powershell -File` without the flag and remain subject to the
local policy.) A policy set by
Group Policy still wins and the command is refused with PowerShell's own
message. Scripts that a command invokes run under the same process-scoped
setting. To let the machine or user policy apply instead, start Codewhale
@@ -242,6 +254,15 @@ note below before upgrading.
### Fixed
- Optional MCP servers can be found before they connect. `tool_search` matches
the query against configured, enabled server names (or an `mcp_<server>_`
prefix), connects up to eight matches within the existing boot wait, and
returns their real tool schemas. Before, a lazily started server exposed no
tools, so the model could never trigger its connection
([#6828](https://github.com/Hmbown/Codewhale/issues/6828)). Known limit: a
query that names neither the server nor `mcp` does not wake it.
`codewhale mcp connect`, `validate` and `tools` run their own connection and
do not attach to a running session.
- `/undo` and `/restore <N>` refuse while a turn is running in the workspace,
instead of rewriting files under it.
- `--resume <id>` after a crash recovers that session's interrupted turn from
@@ -373,9 +394,9 @@ note below before upgrading.
and verification keep their existing delay and cadence, while translation
uses the same earned marker and five-frame-per-second cadence. The Engine
still supplies workflow state, clocks, localized text, custom themes and
terminal adaptation. This is partial adoption: composer,
transcript, dock, posture/metrics, pending input, ocean and whale rendering
retain their existing native implementations.
terminal adaptation. The composer, transcript viewports, dock tabs,
posture/metrics rows, pending-input cards, approval band, Ocean background
and the whale's Braille raster now also render through the pinned kit.
- Calm transcript previews keep the latest three rows of live thought and a
single duration header when it settles. Successful tool headers are quieter;
failed generic and MCP calls keep a bounded head-and-tail excerpt, with full
@@ -391,8 +412,9 @@ note below before upgrading.
file not saved."), in every supported language. The full reason, including the
write error, goes to the transcript once.
- Model-facing guidance names callable tools and explains discovery before a
deferred read. The stopship workflow uses bounded `grep_files` evidence,
optional `tool_search` activation and the current workflow source path
deferred read. The stopship workflow's scout activates the deferred
`grep_files` with one required `tool_search` call, then gathers bounded
`grep_files` evidence from the current workflow source paths
([#6747](https://github.com/Hmbown/Codewhale/issues/6747)).
- Simplified and Traditional Chinese permission, provider and session wording
now follows the current behavior. Chinese guides correct hook receipts,
@@ -421,16 +443,17 @@ note below before upgrading.
cancellation ([#6573](https://github.com/Hmbown/Codewhale/issues/6573),
[#6728](https://github.com/Hmbown/Codewhale/issues/6728)).
- Idle task workers check the queue file's metadata about once a second instead
of five times a second, so a Codewhale left open with nothing to run no longer
polls the disk. An in-process notification still wakes a worker at once; a
of five times a second, and no longer reloads the store when nothing changed.
An in-process notification still wakes a worker at once; a
write by another Codewhale process that shares the data directory is noticed
within about a second instead of about 200 ms. A worker with pending work, a
retry deadline or a failed claim keeps the short interval
([#6728](https://github.com/Hmbown/Codewhale/issues/6728),
[#6573](https://github.com/Hmbown/Codewhale/issues/6573)).
- After five seconds without session activity, the UI loop polls every 250 ms
instead of 48 ms, the automation-panel scan backs off, and the Git probe uses
one `git status` every 15 seconds instead of the full probe every two seconds.
instead of 48 ms and the automation-panel scan backs off to 15 seconds. After
thirty quiet seconds, the Git probe runs one `git status` every 15 seconds
instead of the full probe every two seconds, unless the Git panel is showing.
User input and engine events restore the active cadence. This reduces idle
work; it does not remove every cost that grows with session history
([#6728](https://github.com/Hmbown/Codewhale/issues/6728)).
@@ -613,8 +636,10 @@ note below before upgrading.
- Network audit lines now go to the same `audit.log` as every other audit
event (`$CODEWHALE_HOME` included), and test runs no longer append to your
real one.
- Auto-Review verdicts now reach `audit.log`, as `/permissions` said they
did. They were written only when `CODEWHALE_TOOL_AUDIT_LOG` was set.
- In the terminal UI, Auto-Review verdicts now reach `audit.log`, as
`/permissions` said they did; before, they were written only when
`CODEWHALE_TOOL_AUDIT_LOG` was set. Headless `exec` and Runtime API sessions
do not write them yet.
- A turn that stops producing output now reports itself: the turn loop records
its phase and last progress, and an overdue phase surfaces instead of
hanging silently until the stream idle timeout. A delegated agent's final result is
@@ -763,7 +788,8 @@ note below before upgrading.
reloads and new tabs, and stream tickets retry after transient failures.
Fleet SSH known-host checks support OpenSSH 7.x and later. SSH host configs
using `host_key_fingerprint` must migrate to `known_hosts` with verified host
keys; the unsupported fingerprint field now fails at configuration load.
keys; the unsupported fingerprint field is now refused when an SSH worker
starts, before any connection is made, instead of being silently ignored.
- Harden workspace instruction, note, and anchor file access with shared
no-follow reads and writes. Compaction loads pinned anchors only from
trusted workspaces. Validate registry skill names before selecting cache paths.
@@ -871,9 +897,10 @@ note below before upgrading.
read-only plan, because the completion gates run workspace build and test
scripts.
- Git commands no longer hand provider credentials to programs that a
workspace's git config can start (`core.fsmonitor`, hooks, filters): every
git child starts from the scrubbed environment plus the ssh agent, global
config location and author identity. The read-only `git_status`,
workspace's git config can start (`core.fsmonitor`, hooks, filters) when
they run through Codewhale's shared git helper: those start from the scrubbed
environment plus the ssh agent, global config location and author identity.
Sub-agent worktree provisioning and delivery do not use it yet. The read-only `git_status`,
`git_diff`, history and `verify` tools also disable the workspace's
fsmonitor, hooks and clean/process filters.
- Language servers started for post-edit diagnostics, and every Python and
@@ -886,8 +913,9 @@ note below before upgrading.
`RUSTFLAGS`, `RUST_LOG`, `RUST_BACKTRACE`, `VIRTUAL_ENV`, `JAVA_HOME`, Go
paths, `NVM_*`/`NODE_OPTIONS` and CA-bundle variables — so `run_tests` and
gates keep the user's target dir, job limit and toolchain. Connection
strings such as `DATABASE_URL` are still dropped; declare them in a
verifier gate's `env` or the project's own config when a build needs them.
strings such as `DATABASE_URL` are still dropped; set them in the project's
own config, or wrap the command in a script that sets them, when a build
needs them.
- Deny rules in the permission settings now hold for these ways of hiding a
command word until the shell runs it: a variable (`$v`), a substitution, a
glob or brace list, escaped ANSI-C quoting, or a shell reading its script
+56 -28
View File
@@ -31,8 +31,14 @@ note below before upgrading.
reviewed and enabled first. The host is sandboxed on macOS (Seatbelt) and on
Linux with bubblewrap, with no direct network and no reads of the protected
credential locations (Codewhale's secret stores and the Codewhale, Codex and
DSH homes); if bubblewrap is missing or cannot start, it runs unsandboxed and
says so. On Windows it always runs with your user permissions. Other files you
DSH homes). A Native extension starts only after that sandbox is verified at
launch: if bubblewrap is missing or its namespace probe fails, activation is
refused with the concrete error. Codewhale's own pinned built-in host modules
keep a diagnosed exception, and every effect still needs a Rust operation
ticket. On Windows a Native extension starts only in a freshly created Less
Privileged AppContainer with no capabilities, after Codewhale checks its
token and a real file and network probe; otherwise activation is refused.
Other files you
can read, such as project `.env` files, stay readable, and plugins sharing the
host can interfere with each other, so enable only plugins you have reviewed
([docs/EXTENSIONS.md](docs/EXTENSIONS.md)).
@@ -79,9 +85,10 @@ note below before upgrading.
The Rust backend stays the default until platform and rollout gates pass;
its protocol adapters remain through the compatibility window.
- A plugin can declare several `native` entries (`native.paths`, up to 64). They
activate in order as one plugin, so one disable, review change or crash takes
all of them down, and if one entry fails to activate, nothing from the entries
before it stays registered.
activate in order under one owner, so one disable, review change or crash
takes all of them down. If one entry fails to activate, only that entry is
withdrawn: entries that already activated stay registered, and the plugin
fails only when no entry activates.
- Plugin settings and context: `[plugins."<name>".config]` in your own
`config.toml` is passed to the plugin's `apply(ctx, config)` and checked
against its exported `Config` schema; a project's `.codewhale/config.toml`
@@ -163,9 +170,10 @@ note below before upgrading.
([#6580](https://github.com/Hmbown/Codewhale/pull/6580), thanks @gaord).
- Official model routing: `/router` (also `/model router`) sets up the Auto
router with presets: Jev (TypeSafe's decision model, via OpenRouter or a
TypeSafe key), your provider's fast tier, Off, or Custom. Each preset makes
one test call before it saves, `/status` shows the router's choice, cost and
latency, and a failing router is shown as failing
TypeSafe key), your provider's fast tier, Off, or Custom. Jev and Fast each
make one test call before you save, the Turn Inspector (Ctrl+Alt+O or
`/turn inspect`) shows the router's choice, cost and latency, and a failing
router is shown as failing
([#6525](https://github.com/Hmbown/Codewhale/issues/6525)).
- ChatGPT sign-in uses Codewhale-owned protected credentials and a model roster
fetched for the signed-in account. Account, workspace and issuer changes
@@ -176,7 +184,9 @@ note below before upgrading.
`execute_tools` programs can call MCP tools, and each nested call passes the
same approval gate as a direct call, pausing the program for approval when
needed. Every nested call keeps its receipt, including calls that finish
before a deadline, and `code_mode = false` turns it off. `codewhale mcp list`
before a deadline, and `[features] code_mode = false` stops offering
`execute_tools` up front (it stays reachable through tool search).
`codewhale mcp list`
and `codewhale doctor` warn when a user MCP server duplicates the built-in
Computer Use bundle
([#6562](https://github.com/Hmbown/Codewhale/issues/6562),
@@ -189,9 +199,11 @@ note below before upgrading.
### Changed
- On Windows, every PowerShell command Codewhale starts now passes
- On Windows, every PowerShell command the shell tool starts now passes
`-ExecutionPolicy Bypass` for that process only, so a local `Restricted` or
`AllSigned` policy no longer blocks multi-line commands. A policy set by
`AllSigned` policy no longer blocks multi-line commands. (`.ps1` script tools
still start as `powershell -File` without the flag and remain subject to the
local policy.) A policy set by
Group Policy still wins and the command is refused with PowerShell's own
message. Scripts that a command invokes run under the same process-scoped
setting. To let the machine or user policy apply instead, start Codewhale
@@ -242,6 +254,15 @@ note below before upgrading.
### Fixed
- Optional MCP servers can be found before they connect. `tool_search` matches
the query against configured, enabled server names (or an `mcp_<server>_`
prefix), connects up to eight matches within the existing boot wait, and
returns their real tool schemas. Before, a lazily started server exposed no
tools, so the model could never trigger its connection
([#6828](https://github.com/Hmbown/Codewhale/issues/6828)). Known limit: a
query that names neither the server nor `mcp` does not wake it.
`codewhale mcp connect`, `validate` and `tools` run their own connection and
do not attach to a running session.
- `/undo` and `/restore <N>` refuse while a turn is running in the workspace,
instead of rewriting files under it.
- `--resume <id>` after a crash recovers that session's interrupted turn from
@@ -373,9 +394,9 @@ note below before upgrading.
and verification keep their existing delay and cadence, while translation
uses the same earned marker and five-frame-per-second cadence. The Engine
still supplies workflow state, clocks, localized text, custom themes and
terminal adaptation. This is partial adoption: composer,
transcript, dock, posture/metrics, pending input, ocean and whale rendering
retain their existing native implementations.
terminal adaptation. The composer, transcript viewports, dock tabs,
posture/metrics rows, pending-input cards, approval band, Ocean background
and the whale's Braille raster now also render through the pinned kit.
- Calm transcript previews keep the latest three rows of live thought and a
single duration header when it settles. Successful tool headers are quieter;
failed generic and MCP calls keep a bounded head-and-tail excerpt, with full
@@ -391,8 +412,9 @@ note below before upgrading.
file not saved."), in every supported language. The full reason, including the
write error, goes to the transcript once.
- Model-facing guidance names callable tools and explains discovery before a
deferred read. The stopship workflow uses bounded `grep_files` evidence,
optional `tool_search` activation and the current workflow source path
deferred read. The stopship workflow's scout activates the deferred
`grep_files` with one required `tool_search` call, then gathers bounded
`grep_files` evidence from the current workflow source paths
([#6747](https://github.com/Hmbown/Codewhale/issues/6747)).
- Simplified and Traditional Chinese permission, provider and session wording
now follows the current behavior. Chinese guides correct hook receipts,
@@ -421,16 +443,17 @@ note below before upgrading.
cancellation ([#6573](https://github.com/Hmbown/Codewhale/issues/6573),
[#6728](https://github.com/Hmbown/Codewhale/issues/6728)).
- Idle task workers check the queue file's metadata about once a second instead
of five times a second, so a Codewhale left open with nothing to run no longer
polls the disk. An in-process notification still wakes a worker at once; a
of five times a second, and no longer reloads the store when nothing changed.
An in-process notification still wakes a worker at once; a
write by another Codewhale process that shares the data directory is noticed
within about a second instead of about 200 ms. A worker with pending work, a
retry deadline or a failed claim keeps the short interval
([#6728](https://github.com/Hmbown/Codewhale/issues/6728),
[#6573](https://github.com/Hmbown/Codewhale/issues/6573)).
- After five seconds without session activity, the UI loop polls every 250 ms
instead of 48 ms, the automation-panel scan backs off, and the Git probe uses
one `git status` every 15 seconds instead of the full probe every two seconds.
instead of 48 ms and the automation-panel scan backs off to 15 seconds. After
thirty quiet seconds, the Git probe runs one `git status` every 15 seconds
instead of the full probe every two seconds, unless the Git panel is showing.
User input and engine events restore the active cadence. This reduces idle
work; it does not remove every cost that grows with session history
([#6728](https://github.com/Hmbown/Codewhale/issues/6728)).
@@ -613,8 +636,10 @@ note below before upgrading.
- Network audit lines now go to the same `audit.log` as every other audit
event (`$CODEWHALE_HOME` included), and test runs no longer append to your
real one.
- Auto-Review verdicts now reach `audit.log`, as `/permissions` said they
did. They were written only when `CODEWHALE_TOOL_AUDIT_LOG` was set.
- In the terminal UI, Auto-Review verdicts now reach `audit.log`, as
`/permissions` said they did; before, they were written only when
`CODEWHALE_TOOL_AUDIT_LOG` was set. Headless `exec` and Runtime API sessions
do not write them yet.
- A turn that stops producing output now reports itself: the turn loop records
its phase and last progress, and an overdue phase surfaces instead of
hanging silently until the stream idle timeout. A delegated agent's final result is
@@ -763,7 +788,8 @@ note below before upgrading.
reloads and new tabs, and stream tickets retry after transient failures.
Fleet SSH known-host checks support OpenSSH 7.x and later. SSH host configs
using `host_key_fingerprint` must migrate to `known_hosts` with verified host
keys; the unsupported fingerprint field now fails at configuration load.
keys; the unsupported fingerprint field is now refused when an SSH worker
starts, before any connection is made, instead of being silently ignored.
- Harden workspace instruction, note, and anchor file access with shared
no-follow reads and writes. Compaction loads pinned anchors only from
trusted workspaces. Validate registry skill names before selecting cache paths.
@@ -871,9 +897,10 @@ note below before upgrading.
read-only plan, because the completion gates run workspace build and test
scripts.
- Git commands no longer hand provider credentials to programs that a
workspace's git config can start (`core.fsmonitor`, hooks, filters): every
git child starts from the scrubbed environment plus the ssh agent, global
config location and author identity. The read-only `git_status`,
workspace's git config can start (`core.fsmonitor`, hooks, filters) when
they run through Codewhale's shared git helper: those start from the scrubbed
environment plus the ssh agent, global config location and author identity.
Sub-agent worktree provisioning and delivery do not use it yet. The read-only `git_status`,
`git_diff`, history and `verify` tools also disable the workspace's
fsmonitor, hooks and clean/process filters.
- Language servers started for post-edit diagnostics, and every Python and
@@ -886,8 +913,9 @@ note below before upgrading.
`RUSTFLAGS`, `RUST_LOG`, `RUST_BACKTRACE`, `VIRTUAL_ENV`, `JAVA_HOME`, Go
paths, `NVM_*`/`NODE_OPTIONS` and CA-bundle variables — so `run_tests` and
gates keep the user's target dir, job limit and toolchain. Connection
strings such as `DATABASE_URL` are still dropped; declare them in a
verifier gate's `env` or the project's own config when a build needs them.
strings such as `DATABASE_URL` are still dropped; set them in the project's
own config, or wrap the command in a script that sets them, when a build
needs them.
- Deny rules in the permission settings now hold for these ways of hiding a
command word until the shell runs it: a variable (`$v`), a substitution, a
glob or brace list, escaped ANSI-C quoting, or a shell reading its script
+6
View File
@@ -3967,6 +3967,12 @@ fn memory_cap_supervision() -> super::SupervisionOptions {
} else {
super::supervisor::HOST_MEMORY_CAP
},
// Production hang detection. With the fast 600 ms hang timeout, a host
// stalled in GC near its cap was killed for a missed heartbeat and
// restarted before the memory limit itself stopped it, so the test
// never observed the enforcement it exists to prove.
ping_timeout: Duration::from_secs(3),
hang_timeout: super::supervisor::PING_DEADLINE,
..fast_supervision()
}
}
+375 -52
View File
@@ -23,8 +23,7 @@ use windows_sys::Win32::Foundation::{
WAIT_TIMEOUT,
};
use windows_sys::Win32::Security::Authorization::{
EXPLICIT_ACCESS_W, GRANT_ACCESS, GetSecurityInfo, REVOKE_ACCESS, SE_FILE_OBJECT,
SetEntriesInAclW, SetSecurityInfo, TRUSTEE_IS_SID, TRUSTEE_IS_UNKNOWN, TRUSTEE_W,
GRANT_ACCESS, GetSecurityInfo, REVOKE_ACCESS, SE_FILE_OBJECT,
};
use windows_sys::Win32::Security::Isolation::{
CreateAppContainerProfile, DeleteAppContainerProfile,
@@ -248,8 +247,8 @@ fn retire_scope(root: &Path, scope: &GrantScope, sid: PSID) -> io::Result<()> {
));
}
// Remove the parent's inheritable grant before walking, so newly created
// children cannot inherit this retired SID. MAXIMUM_ALLOWED prevents SDK
// propagation; every child is independently fenced and updated.
// children cannot inherit this retired SID. The kernel write never
// propagates; every child is independently fenced and updated.
edit_acl(file, sid, 0, 0, REVOKE_ACCESS)?;
if !scope.tree {
return Ok(());
@@ -288,8 +287,9 @@ fn retire_scope(root: &Path, scope: &GrantScope, sid: PSID) -> io::Result<()> {
return Err(io::Error::other("grant left its pinned parent"));
}
// Retiring this exact SID may overlap a new host's data
// writes. The physical handle stays pinned, but allow those
// legitimate writers/renames; never restore an old whole ACL.
// writes. The handle edits this exact object while the pinned
// parent chain fences its path, so allow those legitimate
// writers/renames; never restore an old whole ACL.
let file = acl_file_with_share(&path, 1 | 2 | 4)?;
edit_acl(&file, sid, 0, 0, REVOKE_ACCESS)?;
Ok(())
@@ -504,7 +504,7 @@ impl NativeSandbox {
}
// The owning grant entrypoint holds ACL_EDITS. Tree files reuse the parent
// chain rather than reopen MAXIMUM_ALLOWED directory objects. The
// chain rather than reopen pinned directory objects. The
// child_path comparison is only a lexical invariant; the protection is the
// held no-write/no-delete parent chain plus acl_file's no-follow,
// regular, single-link checks.
@@ -826,20 +826,16 @@ fn verify_token(process: &OwnedHandle, sid: PSID) -> io::Result<()> {
return Err(io::Error::last_os_error());
}
let token = unsafe { OwnedHandle::from_raw_handle(token) };
// Both flags must be exactly 1. Report each observation, so a refusal
// says which property the kernel did not confirm.
let mut observed = Vec::new();
let mut confirmed = true;
for (name, class) in [
("TokenIsAppContainer", TokenIsAppContainer),
(
"TokenIsLessPrivilegedAppContainer",
TokenIsLessPrivilegedAppContainer,
),
] {
// AppContainer must be confirmed, and LPAC through the token flag or, when
// that query fails (hosted Windows Server returned ERROR_INVALID_PARAMETER;
// the cause is unknown), through the `WIN://NOALLAPPPKG` security
// attribute the LPAC opt-out adds (ntdoc; NtObjectManager's
// LowPrivilegeAppContainer). Report each observation, so a refusal says
// which property the kernel did not confirm.
let query = |class| -> io::Result<u32> {
let mut value = 0_u32;
let mut read = 0;
let queried = unsafe {
if unsafe {
GetTokenInformation(
token.as_raw_handle(),
class,
@@ -847,18 +843,49 @@ fn verify_token(process: &OwnedHandle, sid: PSID) -> io::Result<()> {
size_of::<u32>() as u32,
&mut read,
)
} != 0;
if queried {
observed.push(format!("{name}={value}"));
} else {
observed.push(format!(
"{name} query failed: {}",
io::Error::last_os_error()
));
} == 0
{
return Err(io::Error::last_os_error());
}
confirmed &= queried && value == 1;
}
if !confirmed {
Ok(value)
};
let mut observed = Vec::new();
let app_container = match query(TokenIsAppContainer) {
Ok(value) => {
observed.push(format!("TokenIsAppContainer={value}"));
value == 1
}
Err(error) => {
observed.push(format!("TokenIsAppContainer query failed: {error}"));
false
}
};
let less_privileged = match query(TokenIsLessPrivilegedAppContainer) {
Ok(value) => {
observed.push(format!("TokenIsLessPrivilegedAppContainer={value}"));
value == 1
}
Err(error) => {
observed.push(format!(
"TokenIsLessPrivilegedAppContainer query failed: {error}"
));
match token_has_lpac_attribute(&token) {
Ok(true) => {
observed.push("WIN://NOALLAPPPKG=1".to_string());
true
}
Ok(false) => {
observed.push("WIN://NOALLAPPPKG absent".to_string());
false
}
Err(error) => {
observed.push(format!("WIN://NOALLAPPPKG unreadable: {error}"));
false
}
}
}
};
if !(app_container && less_privileged) {
return Err(io::Error::other(format!(
"Windows refused the required LPAC token ({})",
observed.join(", ")
@@ -903,9 +930,136 @@ fn verify_token(process: &OwnedHandle, sid: PSID) -> io::Result<()> {
Ok(())
}
/// Whether the token carries the LPAC opt-out attribute `WIN://NOALLAPPPKG`
/// with a nonzero integer value. `TokenSecurityAttributes` (Windows 8+)
/// returns TOKEN_SECURITY_ATTRIBUTES_INFORMATION whose pointers are absolute
/// addresses into the returned buffer; each is checked to be aligned and to
/// stay inside it before it is read.
fn token_has_lpac_attribute(token: &OwnedHandle) -> io::Result<bool> {
use windows_sys::Win32::Security::TokenSecurityAttributes;
#[repr(C)]
struct UnicodeString {
length: u16,
maximum_length: u16,
buffer: *const u16,
}
#[repr(C)]
struct AttributeV1 {
name: UnicodeString,
value_type: u16,
reserved: u16,
flags: u32,
value_count: u32,
values: *const u64,
}
#[repr(C)]
struct AttributesInformation {
version: u16,
reserved: u16,
attribute_count: u32,
attributes: *const AttributeV1,
}
const VERSION_V1: u16 = 1;
const TYPE_INT64: u16 = 1;
const TYPE_UINT64: u16 = 2;
let mut size = 0;
unsafe {
GetTokenInformation(
token.as_raw_handle(),
TokenSecurityAttributes,
null_mut(),
0,
&mut size,
);
}
if (size as usize) < size_of::<AttributesInformation>() || size > 64 * 1024 {
return Err(io::Error::other("invalid token security attribute size"));
}
let mut buffer = vec![0_usize; (size as usize).div_ceil(size_of::<usize>())];
if unsafe {
GetTokenInformation(
token.as_raw_handle(),
TokenSecurityAttributes,
buffer.as_mut_ptr().cast(),
size,
&mut size,
)
} == 0
{
return Err(io::Error::last_os_error());
}
let start = buffer.as_ptr() as usize;
let end = start + buffer.len() * size_of::<usize>();
let inside = |pointer: usize, bytes: usize, align: usize| {
pointer % align == 0
&& pointer >= start
&& pointer.checked_add(bytes).is_some_and(|last| last <= end)
};
let information = unsafe { &*buffer.as_ptr().cast::<AttributesInformation>() };
if information.version != VERSION_V1 {
return Err(io::Error::other("unknown token security attribute version"));
}
let count = information.attribute_count as usize;
if count == 0 {
return Ok(false);
}
if !count
.checked_mul(size_of::<AttributeV1>())
.is_some_and(|bytes| {
inside(
information.attributes as usize,
bytes,
align_of::<AttributeV1>(),
)
})
{
return Err(io::Error::other(
"token security attributes exceed their buffer",
));
}
for index in 0..count {
let attribute = unsafe { &*information.attributes.add(index) };
let name_bytes = usize::from(attribute.name.length);
if name_bytes % 2 != 0
|| !inside(
attribute.name.buffer as usize,
name_bytes,
align_of::<u16>(),
)
{
return Err(io::Error::other(
"token security attribute name exceeds its buffer",
));
}
let name = unsafe { std::slice::from_raw_parts(attribute.name.buffer, name_bytes / 2) };
if !String::from_utf16_lossy(name).eq_ignore_ascii_case("WIN://NOALLAPPPKG") {
continue;
}
let values = attribute.value_count as usize;
if !matches!(attribute.value_type, TYPE_INT64 | TYPE_UINT64)
|| values == 0
|| !values
.checked_mul(size_of::<u64>())
.is_some_and(|bytes| inside(attribute.values as usize, bytes, align_of::<u64>()))
{
return Ok(false);
}
return Ok(unsafe { *attribute.values } != 0);
}
Ok(false)
}
fn set_acl(file: &File, sid: PSID, access: u32, inheritance: u32) -> io::Result<()> {
edit_acl(file, sid, access, inheritance, GRANT_ACCESS)
}
/// Edit exactly one SID's explicit allow entry and write the resulting DACL
/// through the kernel on this pinned handle. `SetSecurityInfo` re-derives
/// inheritance: it turned a file's inherited ACEs into explicit copies after a
/// grant/retire cycle, and it would propagate to children. Writing the exact
/// DACL with `SetKernelObjectSecurity` keeps every other ACE byte-for-byte,
/// including inherited ones, and never touches a child or reparse target.
/// Microsoft advises against this API for files precisely because it does not
/// propagate; here that is the required property.
fn edit_acl(
file: &File,
sid: PSID,
@@ -913,8 +1067,21 @@ fn edit_acl(
inheritance: u32,
mode: windows_sys::Win32::Security::Authorization::ACCESS_MODE,
) -> io::Result<()> {
use windows_sys::Win32::Security::{
ACCESS_ALLOWED_ACE, ACE_HEADER, AddAccessAllowedAceEx, AddAce, GetAce, GetLengthSid,
GetSecurityDescriptorControl, INHERITED_ACE, InitializeAcl, InitializeSecurityDescriptor,
SE_DACL_AUTO_INHERIT_REQ, SE_DACL_AUTO_INHERITED, SE_DACL_PROTECTED, SECURITY_DESCRIPTOR,
SetKernelObjectSecurity, SetSecurityDescriptorControl, SetSecurityDescriptorDacl,
};
use windows_sys::Win32::System::SystemServices::{
ACCESS_ALLOWED_ACE_TYPE, SECURITY_DESCRIPTOR_REVISION,
};
// The owning grant/retirement entrypoint holds ACL_EDITS while opening
// and editing its exact objects, including all shared pinned ancestors.
let grant = mode == GRANT_ACCESS;
if !grant && mode != REVOKE_ACCESS {
return Err(io::Error::other("unsupported ACL edit mode"));
}
let mut old_acl: *mut ACL = null_mut();
let mut descriptor = null_mut();
let result = unsafe {
@@ -936,40 +1103,196 @@ fn edit_acl(
if old_acl.is_null() {
return Err(io::Error::other("refusing to replace an unrestricted DACL"));
}
let entry = EXPLICIT_ACCESS_W {
grfAccessPermissions: access,
grfAccessMode: mode,
grfInheritance: inheritance,
Trustee: TRUSTEE_W {
pMultipleTrustee: null_mut(),
MultipleTrusteeOperation: 0,
TrusteeForm: TRUSTEE_IS_SID,
TrusteeType: TRUSTEE_IS_UNKNOWN,
ptstrName: sid.cast(),
},
};
let mut acl = null_mut();
let result = unsafe { SetEntriesInAclW(1, &entry, old_acl, &mut acl) };
if result != 0 {
return Err(io::Error::from_raw_os_error(result as i32));
let mut control = 0_u16;
let mut revision = 0_u32;
if unsafe { GetSecurityDescriptorControl(descriptor.0, &mut control, &mut revision) } == 0 {
return Err(io::Error::last_os_error());
}
let acl = LocalAllocation(acl.cast());
let preserved = control & (SE_DACL_AUTO_INHERITED | SE_DACL_PROTECTED);
let (acl_revision, acl_size, ace_count) = unsafe {
let acl = &*old_acl;
(
u32::from(acl.AclRevision),
usize::from(acl.AclSize),
acl.AceCount,
)
};
let added = if grant {
size_of::<ACCESS_ALLOWED_ACE>() - size_of::<u32>() + unsafe { GetLengthSid(sid) } as usize
} else {
0
};
let size = acl_size + added;
if size > 0xFFFC {
return Err(io::Error::other("edited DACL exceeds the ACL size limit"));
}
// ACLs must be DWORD aligned.
let mut storage = vec![0_u32; size.div_ceil(size_of::<u32>())];
let new_acl = storage.as_mut_ptr().cast::<ACL>();
if unsafe {
InitializeAcl(
new_acl,
(storage.len() * size_of::<u32>()) as u32,
acl_revision,
)
} == 0
{
return Err(io::Error::last_os_error());
}
let mut merged = access;
let mut inserted = !grant;
for index in 0..u32::from(ace_count) {
let mut ace = null_mut();
if unsafe { GetAce(old_acl, index, &mut ace) } == 0 {
return Err(io::Error::last_os_error());
}
let header = unsafe { &*ace.cast::<ACE_HEADER>() };
let flags = u32::from(header.AceFlags);
let inherited = flags & INHERITED_ACE != 0;
// Only ACCESS_ALLOWED_ACE carries its SID at SidStart.
let ours = u32::from(header.AceType) == ACCESS_ALLOWED_ACE_TYPE
&& usize::from(header.AceSize) >= size_of::<ACCESS_ALLOWED_ACE>()
&& unsafe {
EqualSid(
std::ptr::addr_of_mut!((*ace.cast::<ACCESS_ALLOWED_ACE>()).SidStart).cast(),
sid,
)
} != 0;
if ours && grant && !inherited && flags == inheritance {
// GRANT_ACCESS semantics: combine with the existing explicit grant.
merged |= unsafe { (*ace.cast::<ACCESS_ALLOWED_ACE>()).Mask };
continue;
}
if ours && !grant {
// Retirement removes explicit entries and the stale inherited
// copies left after the parent's grant was revoked first.
continue;
}
if !inserted && inherited {
// Explicit entries precede inherited ones.
if unsafe { AddAccessAllowedAceEx(new_acl, acl_revision, inheritance, merged, sid) }
== 0
{
return Err(io::Error::last_os_error());
}
inserted = true;
}
if unsafe {
AddAce(
new_acl,
acl_revision,
u32::MAX,
ace,
u32::from(header.AceSize),
)
} == 0
{
return Err(io::Error::last_os_error());
}
}
if !inserted
&& unsafe { AddAccessAllowedAceEx(new_acl, acl_revision, inheritance, merged, sid) } == 0
{
return Err(io::Error::last_os_error());
}
let mut security: SECURITY_DESCRIPTOR = unsafe { zeroed() };
let security_ptr = (&mut security as *mut SECURITY_DESCRIPTOR).cast();
// NTFS stores this descriptor as given, with no inheritance merge. The
// kernel keeps SE_DACL_AUTO_INHERITED only when SE_DACL_AUTO_INHERIT_REQ
// accompanies it, so request it exactly when the object already had it;
// otherwise the object would silently become a legacy DACL.
let requested = preserved
| if preserved & SE_DACL_AUTO_INHERITED != 0 {
SE_DACL_AUTO_INHERIT_REQ
} else {
0
};
if unsafe { InitializeSecurityDescriptor(security_ptr, SECURITY_DESCRIPTOR_REVISION) } == 0
|| unsafe { SetSecurityDescriptorDacl(security_ptr, 1, new_acl, 0) } == 0
|| unsafe {
SetSecurityDescriptorControl(
security_ptr,
SE_DACL_AUTO_INHERIT_REQ | SE_DACL_AUTO_INHERITED | SE_DACL_PROTECTED,
requested,
)
} == 0
|| unsafe {
SetKernelObjectSecurity(
file.as_raw_handle(),
DACL_SECURITY_INFORMATION,
security_ptr,
)
} == 0
{
return Err(io::Error::last_os_error());
}
drop(descriptor);
let (still_granted, control) = read_back_dacl(file, sid)?;
if control & (SE_DACL_AUTO_INHERITED | SE_DACL_PROTECTED) != preserved {
return Err(io::Error::other(
"edited DACL changed its inheritance control bits",
));
}
if !grant && still_granted {
return Err(io::Error::other(
"retired profile SID is still present after ACL retirement",
));
}
Ok(())
}
/// Read back the object's DACL: whether any allow entry names `sid`, and the
/// descriptor's control bits.
fn read_back_dacl(file: &File, sid: PSID) -> io::Result<(bool, u16)> {
use windows_sys::Win32::Security::{
ACCESS_ALLOWED_ACE, ACE_HEADER, GetAce, GetSecurityDescriptorControl,
};
use windows_sys::Win32::System::SystemServices::ACCESS_ALLOWED_ACE_TYPE;
let mut acl: *mut ACL = null_mut();
let mut descriptor = null_mut();
let result = unsafe {
SetSecurityInfo(
GetSecurityInfo(
file.as_raw_handle(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
null_mut(),
null_mut(),
acl.0.cast(),
null(),
&mut acl,
null_mut(),
&mut descriptor,
)
};
drop((acl, descriptor));
if result != 0 {
return Err(io::Error::from_raw_os_error(result as i32));
}
Ok(())
let descriptor = LocalAllocation(descriptor);
let mut control = 0_u16;
let mut revision = 0_u32;
if unsafe { GetSecurityDescriptorControl(descriptor.0, &mut control, &mut revision) } == 0 {
return Err(io::Error::last_os_error());
}
if acl.is_null() {
return Ok((false, control));
}
for index in 0..u32::from(unsafe { (*acl).AceCount }) {
let mut ace = null_mut();
if unsafe { GetAce(acl, index, &mut ace) } == 0 {
return Err(io::Error::last_os_error());
}
let header = unsafe { &*ace.cast::<ACE_HEADER>() };
if u32::from(header.AceType) == ACCESS_ALLOWED_ACE_TYPE
&& usize::from(header.AceSize) >= size_of::<ACCESS_ALLOWED_ACE>()
&& unsafe {
EqualSid(
std::ptr::addr_of_mut!((*ace.cast::<ACCESS_ALLOWED_ACE>()).SidStart).cast(),
sid,
)
} != 0
{
return Ok((true, control));
}
}
Ok((false, control))
}
struct LocalAllocation(*mut core::ffi::c_void);
+14 -6
View File
@@ -526,10 +526,18 @@ impl WindowsDirectory {
let mut options = std::fs::OpenOptions::new();
options.read(true).share_mode(1).custom_flags(0x0220_0000);
if acl_target {
// SetSecurityInfo must not recursively follow/modify descendants:
// SDK-documented MAXIMUM_ALLOWED disables propagation. Each child
// is fenced/granted explicitly, without reopening pinned ancestors.
options.access_mode(windows_sys::Win32::System::SystemServices::MAXIMUM_ALLOWED);
// The Native ACL writer uses SetKernelObjectSecurity, which never
// propagates to descendants; each child is fenced and granted
// explicitly. So the target needs only READ_CONTROL|WRITE_DAC plus a
// read right that records share access: read-only sharing still
// blocks rename, delete and data writes while it is edited. Unlike
// MAXIMUM_ALLOWED, which includes DELETE, it does not collide with a
// live host whose current directory is this directory.
use windows_sys::Win32::Storage::FileSystem::{
FILE_LIST_DIRECTORY, FILE_READ_ATTRIBUTES, READ_CONTROL, WRITE_DAC,
};
options
.access_mode(READ_CONTROL | WRITE_DAC | FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES);
}
let file = options.open(path)?;
let metadata = file.metadata()?;
@@ -547,8 +555,8 @@ impl WindowsDirectory {
Ok(self.directory.join(name))
}
/// Extend the actual held direct-parent chain. Reopening a MAXIMUM_ALLOWED
/// ancestor would conflict with its deliberate no-write/no-delete sharing.
/// Extend the actual held direct-parent chain instead of reopening pinned
/// ancestors by path.
pub(crate) fn open_acl_child(&self, name: &std::ffi::OsStr) -> io::Result<Self> {
let directory = self.child_path(name)?;
let file = Self::open_component(&directory, true)?;
+2 -1
View File
@@ -725,7 +725,8 @@ A classifier call happens only when `[auto.router]` names both `provider` and
`ModelInventory::from_config` (`crates/tui/src/model_inventory.rs`). If either
condition fails, or the classifier call errors or times out, the local
fallback decides: the default model, or the fast sibling under `cost_saving`.
The turn's route receipt (`/status` → Auto) records which path was taken, and a
The turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, "Model
route + tokens/cost") records which path was taken, and a
router you configured that cannot run or fails (missing key, HTTP error,
timeout, invalid answer) is shown as `Auto router: failing — …` rather than
silently ignored.
+4 -3
View File
@@ -41,10 +41,11 @@ A plugin may split its code across several entries with `native.paths` (up to
64, alone or beside `path`). They are activated in the order declared, as the
fibers of one plugin: they share one owner, so one disable, review change or
crash tears all of them down together. Each entry is a separate module with its
own `apply`, and tool and command names must be unique across them. If any entry
own `apply`, and tool and command names must be unique across them. If an entry
fails to activate (throws, requires a service the host does not provide, or has
a registration refused), the whole plugin fails and nothing from the entries
before it stays registered. Listing the same file twice is one entry.
a registration refused), only that entry is withdrawn: entries that already
activated stay registered, later entries still activate, and the plugin fails
only when no entry activates. Listing the same file twice is one entry.
## Runtime
+1 -1
View File
@@ -350,7 +350,7 @@ thinking = "off" # optional; defaults to off
timeout_secs = 4 # optional; default 4, 0 = default, capped at 300
```
只有当 `[auto.router]` 同时指定了 `provider` 和 `model`,**并且**该 provider 有 key 时,才会发生分类器调用:`ModelInventory::from_config`(`crates/tui/src/model_inventory.rs`)中的 `router_available = router_configured && has_api_key_for(...)`。任一条件不满足,或分类器调用出错/超时时,由本地回退决定:默认模型,或在 `cost_saving` 下的快速同系模型。回合的路由回执(`/status` → Auto)会记录走了哪条路径;你配置了但无法运行或失败的路由器(缺 key、HTTP 错误、超时、无效回答)会显示为 `Auto router: failing — …`,而不是被静默忽略。
只有当 `[auto.router]` 同时指定了 `provider` 和 `model`,**并且**该 provider 有 key 时,才会发生分类器调用:`ModelInventory::from_config`(`crates/tui/src/model_inventory.rs`)中的 `router_available = router_configured && has_api_key_for(...)`。任一条件不满足,或分类器调用出错/超时时,由本地回退决定:默认模型,或在 `cost_saving` 下的快速同系模型。回合的路由回执(回合检查器:Ctrl+Alt+O 或 `/turn inspect` 的“Model route + tokens/cost”)会记录走了哪条路径;你配置了但无法运行或失败的路由器(缺 key、HTTP 错误、超时、无效回答)会显示为 `Auto router: failing — …`,而不是被静默忽略。
#### 设置模型路由
+2 -2
View File
@@ -36,8 +36,8 @@ ES 模块文件;功能开启时目录及其他后缀会被拒绝。功能关
`<workspace>/.codewhale/plugins/`。
`native.paths` 可声明多个入口,最多 64 个。它们按声明顺序激活,共用一个所有者;
任一入口激活失败,整个插件失败,较早入口的注册也会撤销。相同文件重复列出只算一个
入口。源文件、权限或已审查内容变更需要重新审查。`/plugin reload` 是显式刷新,
某个入口激活失败时,只撤销该入口:已激活的入口保持注册,后续入口仍会激活;只有
没有任何入口激活成功时,整个插件才会失败。相同文件重复列出只算一个入口。源文件、权限或已审查内容变更需要重新审查。`/plugin reload` 是显式刷新,
不提供自动监视或热重载。停用、撤销信任、所有者故障及宿主退出会撤销对应注册。
注册返回可重复调用的 disposer,并归属于调用它的 Cordis fiber。可用 `ctx.effect`