mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 17:10:50 -10:00
fix(windows): verify LPAC, edit ACLs exactly, and correct 0.10.1 notes
Hosted Windows one0a758ffnamed 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 HEADcf033337and 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. Hostede0a758ff: 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:
1 parent
e0a758ffed
commit
02fcc3e07d
9 files changed
+516
-121
No files matched your search
+56
-28
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 — …`,而不是被静默忽略。
|
||||
|
||||
#### 设置模型路由
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ ES 模块文件;功能开启时目录及其他后缀会被拒绝。功能关
|
||||
`<workspace>/.codewhale/plugins/`。
|
||||
|
||||
`native.paths` 可声明多个入口,最多 64 个。它们按声明顺序激活,共用一个所有者;
|
||||
任一入口激活失败,整个插件失败,较早入口的注册也会撤销。相同文件重复列出只算一个
|
||||
入口。源文件、权限或已审查内容变更需要重新审查。`/plugin reload` 是显式刷新,
|
||||
某个入口激活失败时,只撤销该入口:已激活的入口保持注册,后续入口仍会激活;只有
|
||||
没有任何入口激活成功时,整个插件才会失败。相同文件重复列出只算一个入口。源文件、权限或已审查内容变更需要重新审查。`/plugin reload` 是显式刷新,
|
||||
不提供自动监视或热重载。停用、撤销信任、所有者故障及宿主退出会撤销对应注册。
|
||||
|
||||
注册返回可重复调用的 disposer,并归属于调用它的 Cordis fiber。可用 `ctx.effect`
|
||||
|
||||
Reference in new issue
Block a user