mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-10 17:10:18 -10:00
Compare commits
133
Commits
No files matched your search
@@ -750,18 +750,10 @@ assert.equal(
|
||||
2,
|
||||
"both glibc recovery branches must name codewhale-cli",
|
||||
);
|
||||
// The archive installer never overwrites an existing command: it validates the
|
||||
// retired TUI path against the consolidated bytes and leaves upgrades to
|
||||
// `codewhale update`, which migrates `codewhale-tui` beside the canonical pair.
|
||||
assert.match(
|
||||
archiveInstaller,
|
||||
/legacy_tui="\$BIN_DIR\/codewhale-tui"[\s\S]*check_destination "\$SCRIPT_DIR\/codewhale" "\$legacy_tui"/,
|
||||
"archive installs must validate the retired TUI path against consolidated bytes",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
archiveInstaller,
|
||||
/install_binary "\$SCRIPT_DIR\/codewhale" "\$legacy_tui"/,
|
||||
"archive installs must not overwrite an existing retired TUI command",
|
||||
/install_binary "\$SCRIPT_DIR\/[^"]+" "\$BIN_DIR\/codewhale-tui"/,
|
||||
"archive installs must not write the retired codewhale-tui command",
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
cliDispatcher,
|
||||
|
||||
@@ -320,21 +320,6 @@ jobs:
|
||||
(cd "integrations/${bridge}" && npm test)
|
||||
done
|
||||
|
||||
- name: Build computer-use test desktop
|
||||
# Spawn assertions have short request deadlines; keep the cold image
|
||||
# build outside those deadlines and fail image preparation explicitly.
|
||||
timeout-minutes: 10
|
||||
run: >-
|
||||
docker build --tag codewhale-cu-linux
|
||||
--file crates/tui/plugins/computer-use/docker/Dockerfile
|
||||
crates/tui/plugins/computer-use
|
||||
|
||||
- name: Run computer-use plugin suites
|
||||
# The bundled plugin is dependency-free too; its suites cover the
|
||||
# manifest contract, the registry, the exec/ssh transport, the four
|
||||
# platform backends, and the MCP stdio protocol. No GUI input runs.
|
||||
run: (cd crates/tui/plugins/computer-use && npm test)
|
||||
|
||||
- name: Run extension host suites and check the committed bundle
|
||||
# The TypeScript extension host (experimental, [features]
|
||||
# extension_host). Tests exercise both the committed bundle and
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
id: review
|
||||
uses: ./
|
||||
with:
|
||||
version: ${{ vars.CODEWHALE_REVIEW_VERSION || 'v0.10.0' }}
|
||||
version: ${{ vars.CODEWHALE_REVIEW_VERSION || 'v0.10.2' }}
|
||||
provider: codewhale
|
||||
model: ${{ vars.CODEWHALE_REVIEW_MODEL }}
|
||||
pr-number: ${{ inputs.pr-number }}
|
||||
|
||||
@@ -45,11 +45,10 @@ jobs:
|
||||
run: node marketplace/scripts/check-marketplace.mjs
|
||||
- name: Check bundled catalog snapshot
|
||||
run: python3 core/scripts/sync-marketplace.py --marketplace marketplace --check
|
||||
- name: Check current skill and Computer Use mirrors
|
||||
- name: Check current skill mirrors
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
node marketplace/scripts/check-marketplace.mjs --core core
|
||||
node marketplace/scripts/check-cu-sync.mjs --core core
|
||||
- name: Explain a drift failure
|
||||
if: failure()
|
||||
run: |
|
||||
|
||||
@@ -87,9 +87,8 @@ jobs:
|
||||
# Both cold macOS artifact builds on 0.10.1 exhausted 90 minutes inside
|
||||
# compilation. Keep the full CI build budget; packaging stays capped below.
|
||||
timeout-minutes: 210
|
||||
# FreeBSD is a source-build target validated via `cargo check --target x86_64-unknown-freebsd -p codewhale-cli --locked`
|
||||
# (see packaging/freebsd/README.md and docs/INSTALL.md#freebsd). The 7×1 prebuilt matrix stays 7 targets;
|
||||
# FreeBSD has no prebuilt asset, no npm binary, and no matrix bloat — it builds from source.
|
||||
# FreeBSD is a source-build target with no prebuilt asset, no npm binary and no CI check
|
||||
# (see packaging/freebsd/README.md and docs/INSTALL.md#freebsd).
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -39,7 +39,6 @@ jobs:
|
||||
- { name: web, directory: web }
|
||||
- { name: telemetry-ingest, directory: telemetry-ingest }
|
||||
- { name: extension-host, directory: crates/tui/extension-host }
|
||||
- { name: embedded-computer-use, directory: crates/tui/plugins/computer-use }
|
||||
- { name: vscode, directory: extensions/vscode }
|
||||
- { name: feishu-bridge, directory: integrations/feishu-bridge }
|
||||
- { name: telegram-bridge, directory: integrations/telegram-bridge }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Build artifacts
|
||||
/target
|
||||
/target-*
|
||||
/vendor/codewhale-ratatui/target/
|
||||
.entire/
|
||||
.cursor/
|
||||
.plans/
|
||||
|
||||
@@ -183,7 +183,7 @@ casually it reads as taking the work even when credit is preserved.
|
||||
KV-cache effect: frozen prefix vs. append-only history. Never splice a
|
||||
volatile fact into the prefix; append it as a user-role message.
|
||||
- These active modules are repeatedly misidentified as dead; verify consumers
|
||||
before removal: `runtime/src/context_budget.rs`, `tui/src/model_registry.rs`,
|
||||
before removal: `runtime/src/context_budget.rs`,
|
||||
`runtime/src/prompt_zones.rs`, `tui/src/tools/remember.rs`, and
|
||||
`config/src/route/`. Native memory lives in `runtime/src/native_memory.rs`;
|
||||
`tools/remember.rs` is its capture path.
|
||||
|
||||
+188
-1
@@ -7,19 +7,206 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- **Crate split S24a (internal):** the workshop byte-budget slot moves to `codewhale-runtime`, `AutoRouteDecisionEvidence` to `codewhale-models`, and cost-status session persistence to a tui `usage_persistence` module, so billing no longer reaches the session manager or model routing. No behavior change. (CW-6129)
|
||||
- **Nested and deletable threads (Runtime API):** a fork (including a backtrack fork) records `parent_thread_id`, `POST /v1/threads` accepts one, and `GET /v1/threads/summary` returns it so clients can nest child conversations. `DELETE /v1/threads/{id}` removes an idle thread with its turns, items, events and goal, and its saved session unless another thread shares it or a terminal holds it open; a running thread answers 409.
|
||||
- **Hunk actions in Review (Runtime API):** `POST /v1/git/stage`, `/v1/git/unstage` and `/v1/git/discard` take `hunks` (the `@@ … @@` headers from `GET /v1/diff`) with one path and apply only those hunks through `git apply`, honouring the same `expect` preconditions as whole-path writes. A hunk that no longer exists answers `409 git_state_changed` with `stale_hunks`, and nothing is written unless git accepts the whole patch.
|
||||
- **Provider pre-cuts B (internal):** client wire helpers, reasoning-effort mapping and the Ollama catalog parsers move into their own client modules, and test-only provider gates use the shared test-isolation predicate. No behavior change. (CW-6129)
|
||||
- **`codewhale-dependencies`, `codewhale-snapshot`, `codewhale-lsp`, `codewhale-repl` crates:** external-tool resolution, workspace snapshots, the language-server client and the Python REPL runtime moved out of `codewhale-tui` into their own crates; `write_context_file` now lives in `codewhale-repl`. No behavior change. (CW-6129)
|
||||
- **`codewhale-skill-install` crate:** the community-skill installer, package content digest and `SKILL.md` frontmatter reader move out of `codewhale-tui` into a new crate; the install SSRF guard and timeouts are unchanged in production builds. (CW-6129)
|
||||
- **`codewhale-util` crate:** shared utilities, confined workspace file access, link and file-identity checks, bundle file readers, the `.git` name rule and the owner file lock move out of `codewhale-tui` into a new crate; no behavior change. (CW-6129)
|
||||
- **Crate-split contract hygiene:** the `canonical_action` policy table now lives in `codewhale-execpolicy`, `required_action`, the file path-alias folding and the `apply_patch` preflight parser in `codewhale-tools`, and `DEFAULT_SPAWN_DEPTH`, `PermissionCeiling` and `ShellCeiling` in `codewhale_protocol::fleet`, so `codewhale-agent-contract` no longer depends on `codewhale-config` or `codewhale-workflow`. Approval and authority behavior is unchanged. (CW-6129)
|
||||
- **Provider pre-cuts (internal):** schema sanitizing, prompt-turn classification, image-window capping, tool-projection warning names and the reasoning-effort tier moved into `codewhale-models`; stall records and route-label sanitizing moved into `codewhale-runtime`. No behavior change. (CW-6129)
|
||||
- **Change mode and permissions while the agent works:** Tab, Shift+Tab, `/mode`, the hotbar and `/config mode` no longer refuse while a turn is running. A response that is already streaming is not restarted; the change applies at the agent's next step in the same turn (before its next tool batch, at every approval gate, and before its next model request, which carries the new mode notice). The status line says so until it applies. A call still waiting for approval goes back to the model under the new posture, and a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends. (CW-7552)
|
||||
- **Steering interrupts thinking:** a steer (Ctrl+Enter, or Enter twice after queueing) now closes the in-flight model request at once, whether the model is waiting on headers, reasoning, writing text or emitting a tool call, and continues the same turn with your message. Text already shown is kept as interrupted output; unfinished reasoning and partial tool calls are discarded, never run. (CW-7552)
|
||||
- **Switching provider lands on its current model:** `/provider stepfun` now selects Step 5 Preview instead of the older Step 3.7 Flash; a model you saved for a provider is still kept. `/model` now lists the current provider's own models (bundled and live, newest first, up to 20) instead of only the selected one. Search also matches ids typed without separators (`step5`). While you search, Shift+P and Shift+F type their letters rather than pinning or adding to Fleet, so `Step-5-Preview` can be typed in full; Ctrl+P and Ctrl+F pin or add the highlighted row at any time.
|
||||
- **Remote control works again:** `codewhale rc` (`/rc`) can attach to the hosted control plane for web Chat again. The control plane had only accepted the retired 0.9.13 runtime identity, so every 0.10.x Engine was refused with `runner_runtime_identity_unavailable`; it now accepts the exact released runtime identity (commit, image and digest) and fails closed when they are not configured.
|
||||
- **Modern MCP client:** MCP servers now connect through the official MCP SDK (2.4) behind Codewhale's Rust broker by default (`mcp_backend = "host"`; `"rust"` stays available for one release). Servers that speak the 2026-07-28 revision are discovered with `server/discover`; other stdio and HTTP servers fall back to the legacy `initialize` handshake, and configured SSE servers keep theirs. Rust keeps process, network, credential, catalog, approval and cancellation authority. (CW-6713)
|
||||
- **MCP compatibility fixes:** older servers that answer the discovery probe with an error or an unrelated result now connect through the legacy handshake instead of being refused after the probe. A server that rejects sign-in reports its real 401 and the `/mcp login` hint rather than the probe's "method not found". A stale OAuth token is refreshed once when the probe itself is rejected. A server whose command cannot start now says why (for example, command not found) instead of a generic broker refusal.
|
||||
- **DeepSeek Harness plugins:** reviewed DSH plugins (definition API pinned to DSH 0.2.1-rc.2 in a shared compatibility table) run in the Native extension host. Tool output is validated against its declared schema, unsupported options are refused instead of ignored, and plugin commands can submit a prompt with visible plugin attribution. Terminal command receipts are kept in thread snapshots without entering model history. Changing the compatibility table invalidates earlier trust. A newly enabled plugin's tools are offered on the first turn: the turn waits up to 10 seconds for the plugin host to finish loading instead of adding them only from the next turn. Plugin UI views stay disabled, and the import receipt says why: WebKit's network restrictions do not stop WebRTC, so plugin UI waits for an isolated renderer. A plugin tool can call `ctx.sessions.flush(exec.agent.session)` to have the Engine save the conversation so far before it continues (Runtime, terminal and `exec` sessions); it reports failure instead of claiming a save. Installing a plugin whose bytes changed after review now returns a 409 conflict instead of a server error. A plugin tool can ask you a question with `ctx.userQuestions.ask` and gets back the options you picked, your own text, or a skip; the question shows like any other Codewhale question in the terminal and through the Runtime API. (CW-7054, CW-7549)
|
||||
- **MCP catalogs stay fresh:** a server's tools, resources, prompts and skills are rediscovered on their own connection when an advertised `ttlMs` expires or the server sends a list-changed notice. A tool resolved against the old catalog is refused with "retry the call", a removed tool cannot run, and a failed refresh reconnects instead of serving stale entries. Catalogs are never shared across servers, credentials or reconnects. (CW-7547)
|
||||
- **MCP Skills extension:** servers that advertise the published Skills extension have their skills listed in `list_mcp_resources`. Listing never activates a skill, and reading a skill file is refused unless its size and sha256 digest match the listing. MCP skills do not yet join `/skills`. (CW-7545)
|
||||
- **Computer Use is installed, not bundled:** the Computer Use plugin is no longer vendored into the binary. Install it from the `codewhale` marketplace, which points at its independent release; its consent tools load on first use, and decisions still require a human approval card. (CW-6732)
|
||||
- **Tool image validation:** an image returned by a tool must decode before it is sent, so truncated or corrupt files are omitted with a receipt even when they are already within the delivery budget.
|
||||
|
||||
- **Image-heavy session recovery:** ordinary image reads use a 256 KiB delivery budget. Requests measure serialized bytes, omit older images from an outbound copy with receipts, and retry an initial HTTP 413 once only with a smaller body. Saved images and executed tool actions remain intact. (CW-7550)
|
||||
|
||||
- **Uninstall on every install route:** `codewhale uninstall` previews receipt-owned release files and `--yes` removes only checked files, with the running command last. Both Unix installers and the Windows archive installer record command ownership; self-update renews existing receipts on both. On Windows the running `codewhale.exe` is renamed and deleted once the command exits. Package-managed copies are left alone, and the command prints their exact removal command: npm with its prefix, Cargo with its root, the Homebrew formula, `scoop uninstall codewhale`, `winget uninstall --id HunterBown.CodeWhale`, or the Windows setup's `Uninstall.exe`. Every other `codewhale`/`codew` still on PATH is listed. User data is kept unless you add `--purge-data`, which previews and then deletes only `~/.codewhale` (or a `CODEWHALE_HOME` named `.codewhale`) and `~/.deepseek/snapshots`, refusing symlinks, home ancestors and directories owned by another user. Windows removal is compile-checked but not yet exercised on a Windows host. (Refs #7091)
|
||||
|
||||
- **GitHub Action needs an explicit provider:** the Action's `provider` input no longer defaults to the `codewhale` account relay. Name your BYOK provider (or `codewhale` for a separately verified account machine key); a missing provider stops with `configuration_missing` before any download or model call. The repository's own (disabled) review workflow now pins CLI `v0.10.2` by default instead of `v0.10.0`.
|
||||
- **Composer history honors `CODEWHALE_HOME`:** with `CODEWHALE_HOME` set, prompt history reads and writes `$CODEWHALE_HOME/composer_history.jsonl` instead of sharing `~/.codewhale` across isolated homes; an invalid `CODEWHALE_HOME` keeps no history rather than falling back to your real home. (#7069)
|
||||
- **Slow tools are not reported missing:** a tool whose `--version` probe is still running at its 3-second deadline (for example the `cargo` rustup shim on a busy machine) now counts as installed instead of being cached as missing for the rest of the session.
|
||||
- **GitHub Action reviews:** analysis can export a structured Engine review for a separate trusted publisher job. The publisher checks the current PR revision and line anchors, deduplicates its findings, and keeps uncertain or preexisting claims out of inline comments.
|
||||
- **Telegram recovery:** prompts and their initiating identity are durable before the polling cursor advances. Restart recovery reuses the original Engine operation, questions preserve human ownership, and uncertain final replies remain available through `/status` without an automatic resend.
|
||||
- **DingTalk bridge:** an optional internal-app Stream adapter reuses the same Engine, shared admission and approval helpers. Tenant/staff allowlists, durable acceptance before acknowledgement and retained uncertain deliveries protect recovery. Managed account linking is separate from this advanced self-hosted bridge.
|
||||
|
||||
- **Answerless output-limit recovery:** opt into `[reasoning_only] on_output_limit = true` to request an answer again after a length stop with no answer or tool call, including hidden reasoning and ordinary Fleet workers. Retries share `max_reprompts`, retain existing output and control limits, and record request-scoped nudge receipts without adding them to resumed conversation context. RLM keeps its existing stop protocol. Thanks [@M-Maciej](https://github.com/M-Maciej) for the long-proof Fleet report. (#6990)
|
||||
|
||||
- **Linux hardening tests:** the NoNewPrivs child matrices now respect a parent that already holds the irreversible flag, keeping the full assertions on unpinned hosts and checking the kernel ratchet on pinned hosts. Thanks [@asto18089](https://github.com/asto18089) for diagnosing the hardened-container failure. (#7021)
|
||||
|
||||
- **OrcaRouter recommendation:** `/provider` and `/model` offer a shared partner recommendation with the DeepSeek V4.1 Flash 30% top-up-match link, public referral disclosure and localized offer details. F7 or a click opens the existing pager while retaining the current route.
|
||||
|
||||
- **Per-scope instruction budgets:** global and project standing instructions now have separate token budgets (100,000 estimated tokens each, capped at 25% of the usable context window), every trim, skip or refusal reaches the model and `/context report`, and `[instruction_budget] policy = "refuse"` stops a request before dispatch. Other tools' instruction files stay opt-in through `project_instruction_imports` or `claude_fallback`, and the table is user config only. Thanks [@7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) for the design and implementation. (#6526, #7086)
|
||||
|
||||
- Long drafts now show a localized external-editor hint when the actual composer
|
||||
viewport clips their text. The hint uses the shared F4 binding and preserves
|
||||
queue, running-turn, history, and menu hints (#7049). Thanks @Lstarsky0.
|
||||
- Archived context markdown now uses the active theme's palette when rendered
|
||||
in the transcript, preventing unreadable dark text on dark backgrounds (#7043).
|
||||
Thanks @Statter.
|
||||
- Plan-to-Work handoff now names the seeded To-do item in a localized reminder
|
||||
to keep it on the list and complete it after the plan (#7068).
|
||||
- Localized question footers, approval and plan-handoff chrome, tool waiting
|
||||
status, and /color receipts use the shared locale packs (#7071).
|
||||
|
||||
- Clearing a conversation with no goal no longer shows a misleading "Goal cleared."
|
||||
receipt. Clearing an existing or configured goal still reports it (#7089).
|
||||
Thanks @Lstarsky0.
|
||||
|
||||
- Removed the internal config re-export facade and its model-constant wrapper;
|
||||
provider types, shared constants, credential helpers, and notification types
|
||||
now come directly from their shared owners. Configuration and saved settings
|
||||
keep their existing behavior.
|
||||
|
||||
- Removed superseded home-directory and Fleet-role re-export shims, unused
|
||||
tool-context sandbox/MCP fields, and the redundant core tool-parser facade.
|
||||
Consumers now call their existing owners directly; tool denial rules, approval
|
||||
posture, strict tool schemas, home resolution, and legacy saved-role migration
|
||||
retain their behavior. The cleanup removes three dead-code suppressions and
|
||||
keeps the engine as the sole owner of human approval attestations.
|
||||
- Removed unused plan/journal accessors, an unused plan-status glyph helper,
|
||||
and a cancellation case that no execution path issued. Active mailbox and
|
||||
cancellation wiring stays in place; its stale unused-code suppressions are
|
||||
removed. No supported command or feature is retired by this cleanup.
|
||||
|
||||
- Local Ollama defaults now come from chat-capable tags, and `/provider ollama`
|
||||
refreshes the live catalog before switching. Exec and Runtime API defaults use
|
||||
the same discovery path. Fresh context observations come from loaded models'
|
||||
served windows; unloaded models retain an explicitly unverified fallback (#5848).
|
||||
- The Runtime API's `turn.usage` receipts now carry each model call's priced
|
||||
cost on the route that served it and the prompt size the provider billed, so
|
||||
a client can follow a long turn while it runs. The mobile control page shows
|
||||
live token usage and a partial cost estimate in its header, keeps the
|
||||
estimate after the turn settles, starts fresh for the next turn, and counts a
|
||||
replayed receipt once (#7074). Thanks @gaord.
|
||||
- A fatal engine error no longer turns on offline queueing by itself. When the
|
||||
error came from a step that never starts a turn (a finished background
|
||||
shell, a goal continuation or a child completion that cannot resolve its
|
||||
route), every later message used to be queued behind it; it is now sent
|
||||
normally. An environment-only authentication failure still reopens provider
|
||||
setup and queues until you fix it (#6561).
|
||||
- `codewhale --provider <name> resume <id>` now moves the saved session to the
|
||||
named provider, as `exec --resume` already did, so a session whose provider
|
||||
table was deleted can be reopened on a provider you still have. It takes
|
||||
that provider's default model unless `--model` is also given. Without
|
||||
`--provider` the resume still refuses rather than fall back, and the refusal,
|
||||
now also on the `codewhale resume` and `fork` mount path, names this command
|
||||
beside `codewhale sessions export <id>` (#6996). Thanks @luestr.
|
||||
- The model's skills index now says when it shortens skill descriptions.
|
||||
When a description hits the 400-char row cap, or rows are cut to fit the
|
||||
context budget, the index ends with "descriptions shortened for N of M
|
||||
skills" and points to `load_skill` with `name="list"` for the full text;
|
||||
`/context report` and `/context json` show the same count on the skills entry.
|
||||
`/skills inspect` now
|
||||
flags a description by the same rule the index uses (#7073).
|
||||
- The TUI Terminal dock now also shows the model's shell commands for the
|
||||
session: each running or recent command is a read-only tab with its live
|
||||
output, beside the interactive shells. "No live terminals" appears only when
|
||||
there are neither.
|
||||
- A shell command that spans several lines (a heredoc, a loop, an `&&` chain
|
||||
split across lines) now shows those lines on its Live card while it runs and
|
||||
after it succeeds, within the card's usual line limit, instead of only the
|
||||
first line. Transcript shows the whole command. Single-line cards are
|
||||
unchanged (#6436, #7082).
|
||||
- `codewhale review` no longer accepts leaked tool-call markup as a review: a
|
||||
pass whose reply is a DeepSeek DSML (or other) tool-call wrapper outside code
|
||||
now fails as incomplete, with no tally and nothing posted (#7072). Reviews no
|
||||
longer have fixed per-risk output caps (a Low-risk review used to get 4,000
|
||||
tokens, which a reasoning pass could spend entirely on thinking): every pass
|
||||
may use the route's own output limit, and the step, tool and money budgets
|
||||
still stop a review early as a named partial review rather than truncating
|
||||
it. The `review` tool bounds its reasoning level to leave room for the review,
|
||||
and an exhausted pass reports the allowance and reserve actually sent and
|
||||
says "cut off" when visible text came back (#7067).
|
||||
- `--full-access` is now the documented flag for Full Access (`--yolo` stays an
|
||||
alias), and `serve` accepts it after the subcommand: `codewhale serve --acp
|
||||
--yolo` used to fail with "unexpected argument". `serve --acp` also writes one
|
||||
stderr line naming the permission and sandbox it runs with, and the ACP
|
||||
permission option's Full Access hint names the working command. A configured
|
||||
`sandbox_mode = "danger-full-access"` still never widens Ask or Auto-Review
|
||||
(#6992).
|
||||
(#6992, #4955). Thanks @eugenicum for the request.
|
||||
- Preview: `dispatch --computer` starts an account cloud conversation that can
|
||||
also be opened in desktop. `--thread` continues or inspects it; `--stop`,
|
||||
`--approve` and `--deny` control its current task. Explicit `--key-stdin`
|
||||
installs provider access in the running computer's memory. Retries retain
|
||||
one operation key and the account/VM binding. Existing Agent Work dispatch
|
||||
remains available when no computer conversation is selected.
|
||||
- A delegated agent still marked running that has reported no progress for 10
|
||||
minutes no longer holds its parent turn open: the turn's stall watchdogs can
|
||||
recover again, and the stall record names the agent. A child's running time
|
||||
stays unlimited; only silence counts (#6184).
|
||||
- A long draft or paste now grows the composer past its density row limit, up
|
||||
to half the screen (never less than that limit) while leaving room for the
|
||||
transcript, so the text can be read and edited before sending. While a
|
||||
slash, mention or history-search menu is open the composer keeps its usual
|
||||
height (#7049).
|
||||
- Code blocks inside reasoning cells now take their syntax colours from the
|
||||
active theme instead of the terminal's detected light or dark mode, so a dark
|
||||
theme such as Shoreline in a light-profile terminal no longer paints
|
||||
near-black code on its dark reasoning surface. The pager and clipboard copies
|
||||
still follow terminal detection (#7043).
|
||||
- `/balance` and the balance status chip work on the default DeepSeek route
|
||||
again. The balance request now goes to the DeepSeek API root instead of the
|
||||
`/beta` inference path, which failed with "Could not fetch DeepSeek account
|
||||
balance". A failed balance request now names its reason, such as HTTP 401
|
||||
Unauthorized (#7055).
|
||||
- An `AGENTS.md` or global instruction file over 100 KiB is no longer
|
||||
skipped (which could let a sibling file load in its place): Codewhale loads
|
||||
its first 100 KiB, ends it with a marker giving the bytes omitted, and names
|
||||
the file in `/context report`. The shared 48 KiB instruction budget still
|
||||
applies after that, and the marker it leaves when it drops broader scopes
|
||||
now gives the bytes dropped and the total (#6526).
|
||||
- Ctrl+C on an idle composer that holds a draft now clears the draft instead
|
||||
of arming exit, and Ctrl+Z restores it. The "press Ctrl+C again" exit prompt
|
||||
arms only once the composer is empty, and Ctrl+C during a running turn still
|
||||
cancels the turn first. Clearing a draft with Ctrl+C or Esc also resets a
|
||||
pending exit prompt (#6237, #7084).
|
||||
- While a background job runs, the git status probe now backs off after 30
|
||||
seconds without input or engine events, as it already did in an idle
|
||||
session, instead of starting about a dozen git processes every 2 seconds for
|
||||
as long as the job lives (#6728).
|
||||
- An idle session wakes less often. While an approval, elevation or question
|
||||
prompt waits on you, while a background job runs, or while only a
|
||||
once-a-second label ticks, the event loop now relaxes to its 250 ms quiet
|
||||
poll instead of polling every 24 to 48 ms; animations and the prompt's
|
||||
timeout still wake it on their own deadlines. The terminal input thread
|
||||
polls every 200 ms instead of 50 ms after 5 seconds without input. Measured
|
||||
on macOS with release builds: an approval wait went from 29 to 12 timer
|
||||
wakeups per second, a background job from 32 to 16 and an idle composer
|
||||
from 25 to 11 (#6728).
|
||||
- Saving a session no longer copies every message body: journal entries
|
||||
share their content, so the snapshot taken on each update copies ids and
|
||||
pointers. Resuming a session compares a leftover crash checkpoint against
|
||||
the saved document's metadata instead of parsing the whole document first,
|
||||
so `--resume` and attach read the document once (#6842).
|
||||
- `write` over an existing file now says when it kept that file's CRLF or LF
|
||||
line endings, so the agent no longer reports a line-ending conversion that
|
||||
did not happen (#7070).
|
||||
- The To-do that a Plan-to-Work handoff seeds is now titled with the plan's
|
||||
first non-heading line outside code fences, without list or emphasis
|
||||
markdown, instead of the whole plan run together
|
||||
(#7068).
|
||||
- Right-click "Open in editor" on a transcript cell now also offers a bare
|
||||
workspace file path with no line number, opening it at line 1 (#2342).
|
||||
- Composer history is no longer rewritten from a partial read: when the
|
||||
history file cannot be read, new entries are not saved over it and a
|
||||
warning is logged. An empty declarative workflow is refused ("workflow must
|
||||
contain at least one node") instead of succeeding with no work (#6559).
|
||||
- A `SKILL.md` that is not a regular file is reported as a skill load warning
|
||||
instead of being read, so a FIFO in a skills directory can no longer stall
|
||||
discovery (#6554).
|
||||
- `app-server`: a turn whose `response_start` could not be written to stdio
|
||||
is now interrupted instead of running on with nothing able to stop it
|
||||
(#6558).
|
||||
|
||||
## [0.10.2] - 2026-10-22
|
||||
|
||||
|
||||
+1
-1
@@ -555,7 +555,7 @@ A well-structured PR follows a consistent pattern. Recent exemplars include:
|
||||
|
||||
- **#386** — `/init` command: new `crates/tui/src/commands/groups/project/init.rs` module, project-type detection,
|
||||
AGENTS.md generation, command registration in `commands/mod.rs`, localization strings.
|
||||
- **#389** — Inline LSP diagnostics: LSP subsystem in `crates/tui/src/lsp/`, engine hooks in
|
||||
- **#389** — Inline LSP diagnostics: LSP subsystem in `crates/lsp/src/`, engine hooks in
|
||||
`crates/tui/src/core/engine/lsp_hooks.rs`, config toggle, test coverage.
|
||||
- **#387** — Self-update: new `crates/cli/src/update.rs` module, CLI subcommand registration,
|
||||
HTTP download + SHA256 verification + atomic binary replacement.
|
||||
|
||||
Generated
+223
-7
@@ -843,10 +843,8 @@ dependencies = [
|
||||
name = "codewhale-agent-contract"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"codewhale-config",
|
||||
"codewhale-execpolicy",
|
||||
"codewhale-protocol",
|
||||
"codewhale-workflow",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -903,6 +901,7 @@ dependencies = [
|
||||
"codewhale-lane",
|
||||
"codewhale-paths",
|
||||
"codewhale-release",
|
||||
"codewhale-runtime",
|
||||
"codewhale-secrets",
|
||||
"codewhale-state",
|
||||
"codewhale-telemetry",
|
||||
@@ -978,6 +977,7 @@ dependencies = [
|
||||
"libc",
|
||||
"ring",
|
||||
"rust_decimal",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -986,6 +986,7 @@ dependencies = [
|
||||
"toml 1.1.4+spec-1.1.0",
|
||||
"toml_edit 0.25.15+spec-1.1.0",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -1007,6 +1008,24 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-dependencies"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codewhale-config",
|
||||
"codewhale-paths",
|
||||
"codewhale-runtime",
|
||||
"codewhale-util",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-execpolicy"
|
||||
version = "0.10.2"
|
||||
@@ -1015,6 +1034,7 @@ dependencies = [
|
||||
"codewhale-protocol",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shlex 2.0.1",
|
||||
"toml 1.1.4+spec-1.1.0",
|
||||
]
|
||||
@@ -1066,6 +1086,24 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-lsp"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"codewhale-config",
|
||||
"codewhale-runtime",
|
||||
"codewhale-util",
|
||||
"libc",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-mcp"
|
||||
version = "0.10.2"
|
||||
@@ -1095,8 +1133,47 @@ version = "0.10.2"
|
||||
dependencies = [
|
||||
"codewhale-config",
|
||||
"codewhale-core",
|
||||
"codewhale-sanitize",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-net"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"codewhale-command-contract",
|
||||
"codewhale-config",
|
||||
"codewhale-release",
|
||||
"codewhale-runtime",
|
||||
"codewhale-tools",
|
||||
"http",
|
||||
"hyper-util",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"toml 1.1.4+spec-1.1.0",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-notify"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"codewhale-config",
|
||||
"codewhale-runtime",
|
||||
"codewhale-secrets",
|
||||
"dirs 7.0.0",
|
||||
"regex",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1185,41 +1262,95 @@ dependencies = [
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-repl"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"codewhale-dependencies",
|
||||
"codewhale-runtime",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-runtime"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"aws-lc-rs",
|
||||
"base64 0.23.1",
|
||||
"chrono",
|
||||
"codewhale-cloud-facts",
|
||||
"codewhale-command-contract",
|
||||
"codewhale-config",
|
||||
"codewhale-core",
|
||||
"codewhale-execpolicy",
|
||||
"codewhale-lane",
|
||||
"codewhale-localization",
|
||||
"codewhale-memory",
|
||||
"codewhale-models",
|
||||
"codewhale-palette",
|
||||
"codewhale-paths",
|
||||
"codewhale-protocol",
|
||||
"codewhale-release",
|
||||
"codewhale-secrets",
|
||||
"colored",
|
||||
"dirs 7.0.0",
|
||||
"fd-lock",
|
||||
"futures-util",
|
||||
"jsonwebtoken",
|
||||
"libc",
|
||||
"lru",
|
||||
"parking_lot",
|
||||
"portable-pty",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.1",
|
||||
"shellexpand",
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml 1.1.4+spec-1.1.0",
|
||||
"toml_edit 0.25.15+spec-1.1.0",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"wait-timeout",
|
||||
"windows",
|
||||
"windows-sys 0.61.2",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-sandbox"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"codewhale-app-server",
|
||||
"codewhale-execpolicy",
|
||||
"codewhale-paths",
|
||||
"codewhale-release",
|
||||
"codewhale-runtime",
|
||||
"dirs 7.0.0",
|
||||
"libc",
|
||||
"portable-pty",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1250,6 +1381,46 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-skill-install"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"codewhale-net",
|
||||
"codewhale-paths",
|
||||
"codewhale-util",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.1",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
"yaml-rust2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-snapshot"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"codewhale-config",
|
||||
"codewhale-dependencies",
|
||||
"codewhale-paths",
|
||||
"codewhale-runtime",
|
||||
"codewhale-util",
|
||||
"fd-lock",
|
||||
"ignore",
|
||||
"libc",
|
||||
"serde",
|
||||
"sha2 0.11.1",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-state"
|
||||
version = "0.10.2"
|
||||
@@ -1309,7 +1480,6 @@ dependencies = [
|
||||
"arboard",
|
||||
"async-stream",
|
||||
"async-trait",
|
||||
"aws-lc-rs",
|
||||
"axum",
|
||||
"base64 0.23.1",
|
||||
"chrono",
|
||||
@@ -1323,23 +1493,32 @@ dependencies = [
|
||||
"codewhale-commands",
|
||||
"codewhale-config",
|
||||
"codewhale-core",
|
||||
"codewhale-dependencies",
|
||||
"codewhale-execpolicy",
|
||||
"codewhale-hooks",
|
||||
"codewhale-lane",
|
||||
"codewhale-localization",
|
||||
"codewhale-lsp",
|
||||
"codewhale-mcp",
|
||||
"codewhale-memory",
|
||||
"codewhale-models",
|
||||
"codewhale-net",
|
||||
"codewhale-notify",
|
||||
"codewhale-palette",
|
||||
"codewhale-paths",
|
||||
"codewhale-protocol",
|
||||
"codewhale-ratatui",
|
||||
"codewhale-release",
|
||||
"codewhale-repl",
|
||||
"codewhale-runtime",
|
||||
"codewhale-sandbox",
|
||||
"codewhale-sanitize",
|
||||
"codewhale-secrets",
|
||||
"codewhale-skill-install",
|
||||
"codewhale-snapshot",
|
||||
"codewhale-telemetry",
|
||||
"codewhale-tools",
|
||||
"codewhale-util",
|
||||
"codewhale-workflow",
|
||||
"codewhale-workflow-js",
|
||||
"colored",
|
||||
@@ -1353,11 +1532,9 @@ dependencies = [
|
||||
"futures-util",
|
||||
"globset",
|
||||
"htmd",
|
||||
"hyper-util",
|
||||
"ignore",
|
||||
"image",
|
||||
"jsonschema",
|
||||
"jsonwebtoken",
|
||||
"libc",
|
||||
"liblzma",
|
||||
"lru",
|
||||
@@ -1415,10 +1592,37 @@ dependencies = [
|
||||
"yaml-rust2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-util"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"codewhale-config",
|
||||
"codewhale-models",
|
||||
"codewhale-paths",
|
||||
"codewhale-protocol",
|
||||
"codewhale-runtime",
|
||||
"codewhale-telemetry",
|
||||
"futures-util",
|
||||
"ignore",
|
||||
"libc",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"sha2 0.11.1",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"windows",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codewhale-workflow"
|
||||
version = "0.10.2"
|
||||
dependencies = [
|
||||
"codewhale-protocol",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.11.1",
|
||||
@@ -1640,6 +1844,7 @@ dependencies = [
|
||||
"crossterm_winapi",
|
||||
"derive_more",
|
||||
"document-features",
|
||||
"filedescriptor",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
@@ -3982,6 +4187,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
||||
|
||||
[[package]]
|
||||
name = "peg"
|
||||
version = "0.6.3"
|
||||
@@ -4772,17 +4983,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "3.5.1"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab5023804cbe0304cbe06197ac16e645a0a0d0ffa2953bf735c15c2fa0213350"
|
||||
checksum = "ece64e19631fc38758181e637c15509a2c2709ee53bfde1e1d58f9dc991f919e"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"futures",
|
||||
"indexmap",
|
||||
"libc",
|
||||
"oauth2",
|
||||
"pastey",
|
||||
"pin-project-lite",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
@@ -4791,6 +5005,7 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5086,6 +5301,7 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
|
||||
@@ -17,17 +17,26 @@ members = [
|
||||
"crates/mcp",
|
||||
"crates/memory",
|
||||
"crates/models",
|
||||
"crates/net",
|
||||
"crates/notify",
|
||||
"crates/palette",
|
||||
"crates/paths",
|
||||
"crates/protocol",
|
||||
"crates/release",
|
||||
"crates/runtime",
|
||||
"crates/sandbox",
|
||||
"crates/secrets",
|
||||
"crates/skill-install",
|
||||
"crates/sanitize",
|
||||
"crates/state",
|
||||
"crates/telemetry",
|
||||
"crates/tools",
|
||||
"crates/tui",
|
||||
"crates/util",
|
||||
"crates/dependencies",
|
||||
"crates/snapshot",
|
||||
"crates/lsp",
|
||||
"crates/repl",
|
||||
"crates/workflow",
|
||||
"crates/workflow-js",
|
||||
"tests/portable-session",
|
||||
|
||||
@@ -191,10 +191,10 @@ Connect [MCP servers](docs/MCP.md), add [skills](docs/SKILLS.md) and
|
||||
[plugins](docs/PLUGINS.md), and run [hooks](docs/HOOKS.md) on session and tool
|
||||
events. Code mode (experimental, on by default) lets the model write one
|
||||
`execute_tools` program that calls several MCP and plugin tools; each call
|
||||
still meets your approval rules and keeps its receipt. The bundled Computer
|
||||
still meets your approval rules and keeps its receipt. The independently installed Computer
|
||||
Use plugin (experimental, macOS only) can observe
|
||||
and operate other apps. It stays off until you review its access and enable it
|
||||
([guide](crates/tui/plugins/computer-use/README.md)).
|
||||
([guide](https://github.com/codewhale-hq/codewhale-cu-plugin)).
|
||||
|
||||
## Ways to run it
|
||||
|
||||
|
||||
+10
-5
@@ -102,12 +102,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
## DeepSeek Harness utility packages (`dsh-util-values`, `dsh-tools`) — MIT
|
||||
## DeepSeek Harness utility packages (`dsh-util-values`, `dsh-tools`, message compatibility) — MIT
|
||||
|
||||
The same bundle contains `@deepseek-ai/dsh-util-values` 0.1.7-alpha.2, and
|
||||
`crates/tui/extension-host/src/dsh/dsh-tools-compat.js` contains verbatim
|
||||
excerpts of `@deepseek-ai/dsh-tools` 0.1.7-alpha.2 (`lib/index.js`, lines 8-888
|
||||
and 2528-2558), under the same licence text.
|
||||
excerpts of `@deepseek-ai/dsh-tools` 0.2.1-rc.2 (`lib/index.js`), under the
|
||||
same licence text. `crates/tui/extension-host/src/dsh/dsh-message-compat.js`
|
||||
contains verbatim excerpts of `@deepseek-ai/dsh-brand` 0.2.1-rc.2,
|
||||
`@deepseek-ai/dsh-llm` 0.2.1-rc.2, `@deepseek-ai/dsh-session` 0.2.1-rc.2,
|
||||
`@deepseek-ai/dsh-system-prompt` 0.2.1-rc.2,
|
||||
`@deepseek-ai/dsh-user-questions` 0.2.1-rc.2 and
|
||||
`@deepseek-ai/dsh-util-crypto` 0.2.1-rc.2, under the same licence text.
|
||||
|
||||
Upstream: <https://github.com/deepseek-ai/deepseek-harness>
|
||||
(`dsh-util-values` is published from `packages/util/values`)
|
||||
@@ -148,7 +153,7 @@ carry the upstream licensing-transition statement and full Apache-2.0 text
|
||||
reproduced below. The other packages are labeled MIT. Their exact distributed
|
||||
notices are preserved.
|
||||
|
||||
### `@modelcontextprotocol/client` 2.3.1
|
||||
### `@modelcontextprotocol/client` 2.4.0
|
||||
|
||||
```text
|
||||
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
|
||||
@@ -369,7 +374,7 @@ CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
|
||||
the full license text.
|
||||
```
|
||||
|
||||
### `@modelcontextprotocol/core` 2.3.1
|
||||
### `@modelcontextprotocol/core` 2.4.0
|
||||
|
||||
```text
|
||||
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
|
||||
|
||||
+7
-4
@@ -5,13 +5,13 @@ inputs:
|
||||
description: Exact released CLI tag (vX.Y.Z). Must support review --json.
|
||||
required: true
|
||||
provider:
|
||||
description: codewhale for an account machine key, or an explicit BYOK provider.
|
||||
default: codewhale
|
||||
description: Explicit BYOK provider (orcarouter, deepseek, anthropic, openrouter, zai, modelstudio-token-plan), or codewhale for a separately verified account machine key.
|
||||
required: true
|
||||
model:
|
||||
description: Exact model ID. Account mode requires provider/model from the account catalog.
|
||||
required: true
|
||||
github-token:
|
||||
description: GitHub token with contents read and pull-requests write.
|
||||
description: GitHub token with contents read and pull-requests read; legacy post:true also requires pull-requests write.
|
||||
default: ${{ github.token }}
|
||||
pr-number:
|
||||
description: PR number for workflow_dispatch; pull_request events use the event snapshot.
|
||||
@@ -27,7 +27,7 @@ inputs:
|
||||
description: Model and publication deadline, in seconds (30–1200).
|
||||
default: '600'
|
||||
post:
|
||||
description: Publish a COMMENT review; never approve or request changes.
|
||||
description: Legacy direct COMMENT publication. Set false to export findings for a separate publisher job.
|
||||
default: 'true'
|
||||
outputs:
|
||||
outcome:
|
||||
@@ -36,6 +36,9 @@ outputs:
|
||||
receipt:
|
||||
description: Absolute path to the sanitized JSON execution receipt (also written on failures).
|
||||
value: ${{ steps.review.outputs.receipt }}
|
||||
findings:
|
||||
description: Absolute path to validated findings JSON for a complete post:false review; contains repository-sensitive review text.
|
||||
value: ${{ steps.review.outputs.findings }}
|
||||
pr-url:
|
||||
description: URL of the pull request, when resolved.
|
||||
value: ${{ steps.review.outputs.pr-url }}
|
||||
|
||||
+31
-19
@@ -168,11 +168,25 @@ sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-acc
|
||||
# "muse", or "all". Env: CODEWHALE_PROJECT_INSTRUCTION_IMPORTS (comma-separated).
|
||||
# project_instruction_imports = ["claude"]
|
||||
#
|
||||
# Everything that reaches the model as standing project instruction authority —
|
||||
# the repository-root -> workspace AGENTS.md chain, the global fallback layer,
|
||||
# .codewhale/rules/*.md, and any imported foreign files — shares one 48 KiB
|
||||
# aggregate budget. Instructions claim it first and are trimmed from the
|
||||
# broadest scope inward, so the nearest-scope file is the last thing dropped.
|
||||
# Standing instructions are budgeted per scope, in estimated tokens, and every
|
||||
# trim or skip is reported to the model and in /context.
|
||||
# `global` covers ~/.codewhale/AGENTS.md (or ~/.agents/AGENTS.md), the global
|
||||
# instructions.md fallbacks, ~/.claude/CLAUDE.md (last resort, claude_fallback
|
||||
# only) and the `instructions = [...]` array. `project` covers the
|
||||
# repository-root -> workspace AGENTS.md chain, co-read foreign files, and
|
||||
# .codewhale/rules/*.md. A scope is capped at the smaller of its absolute token
|
||||
# budget and its share of the usable window (context window minus the output
|
||||
# reservation and a 10% safety reserve). Each file keeps at most its first
|
||||
# 100 KiB. User config only: a cloned repository cannot change it.
|
||||
# [instruction_budget]
|
||||
# global_budget_tokens = 100000 # positive; absolute cap for the global scope
|
||||
# project_budget_tokens = 100000 # positive; absolute cap for the project scope
|
||||
# global_budget_percent = 25 # 1..=100, share of the usable window
|
||||
# project_budget_percent = 25 # 1..=100, share of the usable window
|
||||
# static_prefix_guard_percent = 80 # 1..=100; guard must leave >=10 points
|
||||
# file_max_bytes = 8388608 # 1..=8388608; larger files are skipped
|
||||
# policy = "trim" # trim | refuse
|
||||
# claude_fallback = false # opt in: AGENTS.md wins; else CLAUDE.md
|
||||
# prompt_suggestion = true # opt-in: show ghost-text follow-up question in composer after each turn
|
||||
|
||||
# Optional tab/window title shown as `[title] …` in front of the terminal
|
||||
@@ -1267,20 +1281,18 @@ code_mode = true # execute_tools composes MCP/plugin/native calls; false defers
|
||||
# a prompt under Full Access, under Bypass, or with a
|
||||
# matching session grant, which is bound to the
|
||||
# plugin's reviewed build (an update asks again).
|
||||
# Sandbox: on macOS the host runs under Seatbelt, and
|
||||
# on Linux under bubblewrap (/usr/bin/bwrap) when a
|
||||
# launch-time probe shows it works. Either way: no
|
||||
# direct network, no reads of ~/.codewhale secrets/
|
||||
# credentials/config/sessions, and writes only to its
|
||||
# data dir and temp dirs (plus, on macOS, the Darwin
|
||||
# user cache, ~/.cargo/registry, ~/.cargo/git and the
|
||||
# npm cache, which every Seatbelt-sandboxed command
|
||||
# gets). If bwrap is missing or cannot start (for
|
||||
# example blocked unprivileged user namespaces), the
|
||||
# host runs UNSANDBOXED with your user permissions and
|
||||
# /plugin, `codewhale doctor` and the start diagnostic
|
||||
# say so with bwrap's own error. On Windows it always
|
||||
# runs with your user permissions.
|
||||
# Native code requires a verified OS sandbox: Seatbelt
|
||||
# on macOS, bubblewrap on Linux, and LPAC/Job on Windows.
|
||||
# An unavailable Native sandbox refuses activation.
|
||||
# Unix reads are limited to the selected runtime and
|
||||
# libraries, canonical host bundle, captured Core-managed
|
||||
# plugin stores and Native data. Writes stay in Native
|
||||
# data and private scratch; repository reads and direct
|
||||
# network are denied. Native plugins share one trusted
|
||||
# cohort, not separate per-plugin OS sandboxes.
|
||||
# On macOS a detached child can outlive process-group
|
||||
# cancellation while retaining the sandbox restrictions.
|
||||
# Builtin host policy is separate and diagnosed separately.
|
||||
# Toggling this changes the plugin activation policy,
|
||||
# so every plugin must be re-reviewed afterwards.
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ workspace = true
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
codewhale-config = { path = "../config", version = "0.10.2" }
|
||||
codewhale-execpolicy = { path = "../execpolicy", version = "0.10.2" }
|
||||
codewhale-protocol = { path = "../protocol", version = "0.10.2" }
|
||||
codewhale-workflow = { path = "../workflow", version = "0.10.2" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -20,8 +20,8 @@
|
||||
//! ledger / executor / worker_runtime / roster / profile / identity /
|
||||
//! members, and nothing from tools.
|
||||
|
||||
use codewhale_protocol::fleet::FleetEffectivePermissions;
|
||||
use codewhale_workflow::{PermissionCeiling, ShellCeiling};
|
||||
use codewhale_protocol::fleet::{FleetEffectivePermissions, migrate_legacy_role_token};
|
||||
use codewhale_protocol::fleet::{PermissionCeiling, ShellCeiling};
|
||||
|
||||
use crate::worker_profile::{ShellPolicy, ToolScope, WorkerRuntimeProfile};
|
||||
|
||||
@@ -40,7 +40,7 @@ pub const FLEET_ROLE_SCHEMA_VALUES: [&str; 8] = [
|
||||
"custom",
|
||||
];
|
||||
|
||||
/// Human-readable hint listing every token [`FleetRole::from_str`] accepts,
|
||||
/// Human-readable hint listing every token [`FleetRole::parse`] accepts,
|
||||
/// for spawn-time error messages. Keep in sync with
|
||||
/// [`migrate_legacy_role_token`] and the `from_str` match arms; those two are
|
||||
/// the only role parser (#2649 was a second table in the spawn tool drifting
|
||||
@@ -114,38 +114,18 @@ impl<'de> serde::Deserialize<'de> for FleetRole {
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw = String::deserialize(deserializer)?;
|
||||
Self::from_str(&raw)
|
||||
Self::parse(&raw)
|
||||
.ok_or_else(|| serde::de::Error::unknown_variant(&raw, &FLEET_ROLE_SCHEMA_VALUES))
|
||||
}
|
||||
}
|
||||
|
||||
/// Explicit boundary migration for pre-Fleet serialized role tokens.
|
||||
///
|
||||
/// Call this only at load / parse edges. Runtime code must use Fleet role
|
||||
/// names via [`FleetRole::as_str`]. Returns `None` for tokens that are
|
||||
/// already canonical or unknown — callers should prefer [`FleetRole::from_str`]
|
||||
/// for full acceptance (canonical + legacy).
|
||||
#[must_use]
|
||||
pub fn migrate_legacy_role_token(token: &str) -> Option<&'static str> {
|
||||
match token.trim().to_ascii_lowercase().as_str() {
|
||||
"worker" | "general-purpose" | "general_purpose" | "default" => Some("general"),
|
||||
"scout" | "exploration" | "explorer" => Some("explore"),
|
||||
"plan" | "planning" | "awaiter" => Some("planner"),
|
||||
"review" | "code-review" | "code_review" => Some("reviewer"),
|
||||
"builder" | "implementer" | "implementation" => Some("implement"),
|
||||
"verifier" | "verify" | "verification" | "validator" | "tester" => Some("test"),
|
||||
"consultant" | "oracle" => Some("advisor"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl FleetRole {
|
||||
/// Parse a Fleet role from user input or a serialized boundary.
|
||||
///
|
||||
/// Accepts Fleet role names and, at this parse boundary only, legacy
|
||||
/// aliases (`scout` → explore, `plan` → planner, …).
|
||||
#[must_use]
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
let normalized = s.trim().to_ascii_lowercase();
|
||||
// Boundary migration first, then canonical Fleet names.
|
||||
let token = migrate_legacy_role_token(&normalized).unwrap_or(normalized.as_str());
|
||||
@@ -222,7 +202,7 @@ impl FleetRole {
|
||||
/// anything else passes through trimmed.
|
||||
#[must_use]
|
||||
pub fn public_role_label(token: &str) -> String {
|
||||
FleetRole::from_str(token).map_or_else(
|
||||
FleetRole::parse(token).map_or_else(
|
||||
|| token.trim().to_string(),
|
||||
|role| role.as_str().to_string(),
|
||||
)
|
||||
@@ -288,7 +268,7 @@ pub const NETWORK_TOOL_DENYLIST: &[&str] = &[
|
||||
// contract was considered and rejected: `rlm_open` chooses its source from
|
||||
// *input fields* (`file_path` / `content` / `url` / `session_object`), not
|
||||
// from the action name, and the action-policy seam
|
||||
// (`crate::tools::canonical_action`) resolves names, not field shapes —
|
||||
// (`codewhale_execpolicy::canonical_action`) resolves names, not field shapes —
|
||||
// it cannot prove a source is local before execution. So this fails closed.
|
||||
// A network-denied member loses `rlm` loading and evaluation entirely,
|
||||
// including the purely local `file_path` form, and keeps only the bounded
|
||||
@@ -659,7 +639,7 @@ impl ChildAuthority {
|
||||
/// model-facing spawn vocabulary.
|
||||
///
|
||||
/// Deliberately *not* folded into [`migrate_legacy_role_token`]. That table
|
||||
/// feeds [`FleetRole::from_str`], which is also the closed vocabulary the
|
||||
/// feeds [`FleetRole::parse`], which is also the closed vocabulary the
|
||||
/// `agent` tool validates its `type` against, and — decisively — the test the
|
||||
/// spawn parser uses to decide whether a `role` string is a posture alias or a
|
||||
/// roster profile key. Teaching `from_str` about `manager` or `smoke-runner`
|
||||
@@ -705,7 +685,7 @@ fn member_role_alias(token: &str) -> Option<FleetRole> {
|
||||
/// Resolution order, and nothing else:
|
||||
/// 1. an empty label means *unspecified*, which is the documented general
|
||||
/// default a Fleet task without a `role` has always had;
|
||||
/// 2. [`FleetRole::from_str`] — the declared closed vocabulary plus its
|
||||
/// 2. [`FleetRole::parse`] — the declared closed vocabulary plus its
|
||||
/// documented legacy aliases ([`VALID_ROLE_ALIASES`]);
|
||||
/// 3. `member_role_alias` — the org-chart/slot spellings above;
|
||||
/// 4. **fail closed.**
|
||||
@@ -723,7 +703,7 @@ pub fn runtime_role_for_member(role: &str) -> FleetRole {
|
||||
if token.is_empty() {
|
||||
return FleetRole::Worker;
|
||||
}
|
||||
FleetRole::from_str(&token)
|
||||
FleetRole::parse(&token)
|
||||
.or_else(|| member_role_alias(&token))
|
||||
.unwrap_or(FleetRole::Scout)
|
||||
}
|
||||
|
||||
@@ -463,7 +463,7 @@ impl WorkerRuntimeProfile {
|
||||
// wins via `derive_child`, same as every other role.
|
||||
reasoning_effort: matches!(role, FleetRole::Consultant).then(|| "high".to_string()),
|
||||
denied_tools: Vec::new(),
|
||||
max_spawn_depth: codewhale_config::DEFAULT_SPAWN_DEPTH,
|
||||
max_spawn_depth: codewhale_protocol::fleet::DEFAULT_SPAWN_DEPTH,
|
||||
spawn_depth: 0,
|
||||
wall_time_secs: None,
|
||||
wall_deadline_ms: None,
|
||||
|
||||
@@ -1131,7 +1131,7 @@ mod tests {
|
||||
|
||||
for (provider, id) in [
|
||||
(ProviderKind::Zai, "GLM-5.2"),
|
||||
(ProviderKind::Stepfun, "step-3.7-flash"),
|
||||
(ProviderKind::Stepfun, "step-5-preview"),
|
||||
(ProviderKind::Minimax, "MiniMax-M2.1"),
|
||||
(ProviderKind::MinimaxAnthropic, "MiniMax-M3"),
|
||||
(ProviderKind::Openmodel, "deepseek-v4-flash"),
|
||||
@@ -1307,7 +1307,7 @@ mod tests {
|
||||
|
||||
let stepfun = registry.resolve_ok(None, Some(ProviderKind::Stepfun));
|
||||
assert_eq!(stepfun.resolved.provider, ProviderKind::Stepfun);
|
||||
assert_eq!(stepfun.resolved.id, "step-3.7-flash");
|
||||
assert_eq!(stepfun.resolved.id, "step-5-preview");
|
||||
|
||||
for (alias, expected) in [
|
||||
("minimax", "MiniMax-M3"),
|
||||
|
||||
@@ -2519,14 +2519,31 @@ impl RuntimeBridge {
|
||||
});
|
||||
}
|
||||
|
||||
emit_stdio_event(
|
||||
if let Err(error) = emit_stdio_event(
|
||||
writer,
|
||||
json!({
|
||||
"type": "response_start",
|
||||
"response_id": response_id,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
{
|
||||
let orphan = InFlightTurn {
|
||||
base_url: self.base_url.clone(),
|
||||
auth_token: self.auth_token.clone(),
|
||||
runtime_thread_id: thread_id.to_string(),
|
||||
turn_id: turn_id.clone(),
|
||||
};
|
||||
if let Err(interrupt) = interrupt_turn_request(&orphan).await {
|
||||
tracing::warn!(
|
||||
thread_id,
|
||||
turn_id = %turn_id,
|
||||
"failed to interrupt a turn whose response_start could not be written: {}",
|
||||
interrupt.message
|
||||
);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// Publish the turn only for the streaming window, and take it back
|
||||
// before any `?` below: a turn that has already finished must never
|
||||
|
||||
@@ -111,59 +111,22 @@ fn cached_build_scripts_classify_the_current_manifest_and_require_it() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn macos_helper_compilation_follows_the_relocated_manifest() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
fn tui_build_does_not_compile_or_embed_computer_use() {
|
||||
let fixture = Fixture::new();
|
||||
let original = fixture.0.path().join("original manifest");
|
||||
let relocated = fixture.0.path().join("relocated manifest");
|
||||
let source = Path::new("plugins/computer-use/src/backends/darwin-accessibility.m");
|
||||
std::fs::create_dir_all(original.join(source).parent().unwrap()).unwrap();
|
||||
std::fs::write(original.join(source), "fixture source").unwrap();
|
||||
let executable = fixture.compile("tui", include_str!("../../tui/build.rs"), &original);
|
||||
std::fs::rename(&original, &relocated).unwrap();
|
||||
let manifest = fixture.0.path().join("manifest");
|
||||
std::fs::create_dir(&manifest).unwrap();
|
||||
let executable = fixture.compile("tui", include_str!("../../tui/build.rs"), &manifest);
|
||||
let out = fixture.0.path().join("out");
|
||||
let bin = fixture.0.path().join("bin");
|
||||
std::fs::create_dir(&out).unwrap();
|
||||
std::fs::create_dir(&bin).unwrap();
|
||||
// Exercise the real build-script command arguments without requiring a macOS
|
||||
// SDK or accessing a signing identity. A missing source fails as clang did.
|
||||
for (name, script) in [
|
||||
(
|
||||
"xcrun",
|
||||
"#!/bin/sh\nset -eu\nsource=\noutput=\nwhile [ \"$#\" -gt 0 ]; do\ncase \"$1\" in *.m) source=$1 ;; -o) shift; output=$1 ;; esac\nshift\ndone\ntest -f \"$source\"\nprintf %s \"$source\" > \"$output\"\n",
|
||||
),
|
||||
(
|
||||
"codesign",
|
||||
"#!/bin/sh\nset -eu\nfor last in \"$@\"; do :; done\ntest -s \"$last\"\n",
|
||||
),
|
||||
] {
|
||||
let path = bin.join(name);
|
||||
std::fs::write(&path, script).unwrap();
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
let mut paths = vec![bin];
|
||||
paths.extend(std::env::split_paths(
|
||||
&std::env::var_os("PATH").unwrap_or_default(),
|
||||
));
|
||||
let output = checked(
|
||||
run_script(&executable, &relocated, fixture.0.path())
|
||||
run_script(&executable, &manifest, fixture.0.path())
|
||||
.env("CARGO_CFG_TARGET_OS", "macos")
|
||||
.env("CARGO_CFG_TARGET_ARCH", "aarch64")
|
||||
.env("OUT_DIR", &out)
|
||||
.env("PATH", std::env::join_paths(paths).unwrap())
|
||||
.env_remove("CODEWHALE_CU_SIGN_IDENTITY"),
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(out.join("computer-use-accessibility")).unwrap(),
|
||||
relocated.join(source).to_string_lossy()
|
||||
.env("OUT_DIR", &out),
|
||||
);
|
||||
assert_eq!(std::fs::read_dir(out).unwrap().count(), 0);
|
||||
let directives = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(directives.contains(&format!(
|
||||
"cargo:rerun-if-changed={}\n",
|
||||
relocated.join(source).display()
|
||||
)));
|
||||
assert!(!directives.contains(original.to_string_lossy().as_ref()));
|
||||
assert!(!directives.contains("computer-use"));
|
||||
assert!(!directives.contains("CODEWHALE_CU_SIGN_IDENTITY"));
|
||||
}
|
||||
@@ -24,6 +24,7 @@ name = "codewhale"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
clap_complete.workspace = true
|
||||
|
||||
@@ -314,15 +314,14 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
} else {
|
||||
Value::Null
|
||||
};
|
||||
if let Some(id) = &options.thread {
|
||||
if thread["id"].as_str() != Some(id)
|
||||
if let Some(id) = &options.thread
|
||||
&& (thread["id"].as_str() != Some(id)
|
||||
|| thread["userId"].as_str() != Some(&account.account)
|
||||
|| thread["archivedAt"]
|
||||
.as_str()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
{
|
||||
bail!("This account conversation is unavailable.");
|
||||
}
|
||||
.is_some_and(|value| !value.is_empty()))
|
||||
{
|
||||
bail!("This account conversation is unavailable.");
|
||||
}
|
||||
let whale_id = options.whale.clone().or_else(|| {
|
||||
thread["agentId"]
|
||||
@@ -537,6 +536,65 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
"key": local.api_key.as_deref(), "expectedBaseUrl": local.base_url }),
|
||||
).context("The computer could not confirm your model connection. Check that its provider endpoint matches your configured model")?;
|
||||
}
|
||||
let mut member_connections = Vec::new();
|
||||
if use_local_model {
|
||||
let lead = thread["agentId"].as_str().unwrap_or("");
|
||||
let mut connected = vec![provider.to_string()];
|
||||
for participant in thread["group"]["participants"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|row| row["state"] == "active" && row["agentId"].as_str() != Some(lead))
|
||||
{
|
||||
let id = participant["agentId"]
|
||||
.as_str()
|
||||
.context("A group member has no whale ID. Refresh the group.")?;
|
||||
validate_resource_id(id, "Whale")?;
|
||||
let name = printable(participant["name"].as_str().unwrap_or("A member"));
|
||||
let agent = account.request(HttpMethod::Get, &format!("/api/agents/{id}"), None)?;
|
||||
let route = &agent["agent"]["configuration"]["model"];
|
||||
let (Some(member_provider), Some(member_model)) =
|
||||
(route["provider"].as_str(), route["model"].as_str())
|
||||
else {
|
||||
bail!("{name} has no saved model. Give it a configured model in Codewhale.");
|
||||
};
|
||||
if agent["agent"]["id"].as_str() != Some(id) {
|
||||
bail!("The account returned a different whale for {name}.");
|
||||
}
|
||||
validate_model_route(member_provider, member_model)?;
|
||||
if connected.iter().any(|done| done == member_provider) {
|
||||
continue;
|
||||
}
|
||||
let kind =
|
||||
codewhale_config::ProviderKind::parse(member_provider).with_context(|| {
|
||||
format!("Configure the {member_provider} provider in Codewhale for {name}.")
|
||||
})?;
|
||||
let member_local = config.config.resolve_runtime_options_with_secrets(
|
||||
&codewhale_config::CliRuntimeOverrides {
|
||||
provider: Some(kind),
|
||||
model: Some(member_model.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
secrets,
|
||||
);
|
||||
portable_model(&member_local, config)
|
||||
.with_context(|| format!("{name} uses {member_provider}"))?;
|
||||
account
|
||||
.operation(
|
||||
thread_id,
|
||||
&computer,
|
||||
&runtime,
|
||||
"key",
|
||||
json!({ "provider": member_provider,
|
||||
"key": member_local.api_key.as_deref(), "expectedBaseUrl": member_local.base_url }),
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("The computer could not confirm the {member_provider} connection for {name}")
|
||||
})?;
|
||||
connected.push(member_provider.to_string());
|
||||
member_connections.push(json!({ "agentId": id, "provider": member_provider }));
|
||||
}
|
||||
}
|
||||
if let Some(key) = key {
|
||||
validate_api_key(&key)?;
|
||||
account.operation(
|
||||
@@ -579,14 +637,15 @@ pub(super) fn run<T: CloudTransport, W: Write>(
|
||||
}
|
||||
reply
|
||||
} else if options.prepare_model {
|
||||
json!({ "prepared": true, "provider": provider, "model": model, "backend": "memory" })
|
||||
json!({ "prepared": true, "provider": provider, "model": model, "backend": "memory",
|
||||
"members": member_connections })
|
||||
} else {
|
||||
detail
|
||||
};
|
||||
if let Some(turn) = result.get("turn") {
|
||||
if turn["thread_id"].as_str() != Some(&runtime) {
|
||||
bail!("The computer returned a different task");
|
||||
}
|
||||
if let Some(turn) = result.get("turn")
|
||||
&& turn["thread_id"].as_str() != Some(&runtime)
|
||||
{
|
||||
bail!("The computer returned a different task");
|
||||
}
|
||||
if options.json {
|
||||
writeln!(
|
||||
|
||||
+53
-17
@@ -465,6 +465,21 @@ The command prints the completion script to stdout; redirect it to a path your s
|
||||
after_help = "GitHub Releases is the default source. Supported mirrors are explicit overrides or manifest-failure fallbacks. Checksums are required; older releases never replace a newer build.\n\nThe command prints the executable it will update. If you have multiple installs, run the intended binary by its full path.\n\nNew macOS/Linux install: curl -fsSL https://codewhale.net/install.sh | sh\nInstallation and PATH help: https://github.com/codewhale-hq/CodeWhale/blob/main/docs/INSTALL.md"
|
||||
)]
|
||||
Update(UpdateArgs),
|
||||
#[command(
|
||||
about = "Remove this receipt-owned release install, or print its package manager's removal command; user data is kept unless --purge-data"
|
||||
)]
|
||||
Uninstall {
|
||||
#[arg(
|
||||
long,
|
||||
help = "Remove the preflighted files; without this flag, print the plan"
|
||||
)]
|
||||
yes: bool,
|
||||
#[arg(
|
||||
long,
|
||||
help = "Also delete Codewhale's own data directory (config, secrets file, sessions); previewed unless --yes"
|
||||
)]
|
||||
purge_data: bool,
|
||||
},
|
||||
/// Export the route catalog (`providers export --json`).
|
||||
Providers(ProvidersArgs),
|
||||
}
|
||||
@@ -2282,6 +2297,17 @@ fn run() -> Result<()> {
|
||||
|| matches!(cli.command.as_ref(), Some(Commands::AppServer(args)) if args.http),
|
||||
"An inherited Runtime listener requires serve --http or app-server --http"
|
||||
);
|
||||
if let Some(Commands::Uninstall { yes, purge_data }) = cli.command.as_ref() {
|
||||
#[cfg(any(all(unix, not(target_env = "ohos")), windows))]
|
||||
return update::uninstall::run(*yes, *purge_data);
|
||||
#[cfg(not(any(all(unix, not(target_env = "ohos")), windows)))]
|
||||
{
|
||||
let _ = (yes, purge_data);
|
||||
bail!(
|
||||
"Automatic uninstall is supported for receipt-owned macOS, Linux and Windows release installs. Use the installer or package manager that owns this copy; user data is preserved."
|
||||
);
|
||||
}
|
||||
}
|
||||
preserve_exec_separator(&mut cli, &argv);
|
||||
capture_exec_startup_options(&mut cli)?;
|
||||
capture_serve_full_access(&mut cli);
|
||||
@@ -2773,6 +2799,9 @@ fn run() -> Result<()> {
|
||||
finish_cli_telemetry(session, &outcome, cli.verbose);
|
||||
outcome
|
||||
}
|
||||
Some(Commands::Uninstall { .. }) => {
|
||||
unreachable!("uninstall dispatches before config loading")
|
||||
}
|
||||
Some(Commands::Providers(args)) => run_providers_command(args),
|
||||
None => {
|
||||
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
|
||||
@@ -2993,7 +3022,11 @@ fn capture_serve_full_access(cli: &mut Cli) {
|
||||
return;
|
||||
};
|
||||
let before = args.args.len();
|
||||
let separator = args.args.iter().position(|arg| arg == "--").unwrap_or(before);
|
||||
let separator = args
|
||||
.args
|
||||
.iter()
|
||||
.position(|arg| arg == "--")
|
||||
.unwrap_or(before);
|
||||
let mut index = 0;
|
||||
args.args.retain(|arg| {
|
||||
index += 1;
|
||||
@@ -4400,7 +4433,7 @@ fn owned_oauth_account(store: &ConfigStore, provider: ProviderKind) -> Result<Op
|
||||
.oauth_credential_generation
|
||||
.as_deref()
|
||||
.context("no sign-in generation configured")?;
|
||||
codewhale_tui::owned_oauth_account_label(provider, generation)
|
||||
codewhale_runtime::oauth::owned_oauth_account_label(provider, generation)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -5411,8 +5444,8 @@ fn run_config_command(
|
||||
}
|
||||
// A settings.toml key is answered from settings.toml, even when a
|
||||
// stale config.toml copy that nothing reads is still present.
|
||||
if codewhale_tui::config_keys::config_key_home(&key)
|
||||
== codewhale_tui::config_keys::ConfigKeyHome::SettingsToml
|
||||
if codewhale_runtime::config_keys::config_key_home(&key)
|
||||
== codewhale_runtime::config_keys::ConfigKeyHome::SettingsToml
|
||||
{
|
||||
let value = settings_key_value(store, &key)?;
|
||||
note_unread_config_copy(store, &key);
|
||||
@@ -5420,7 +5453,7 @@ fn run_config_command(
|
||||
return Ok(());
|
||||
}
|
||||
if key == "stream" || key.starts_with("stream.") {
|
||||
let stream = codewhale_tui::config_keys::resolved_stream_config(&store.config)?;
|
||||
let stream = codewhale_runtime::config_keys::resolved_stream_config(&store.config)?;
|
||||
let value = if key == "stream" {
|
||||
&stream
|
||||
} else {
|
||||
@@ -5462,11 +5495,11 @@ fn run_config_command(
|
||||
}
|
||||
// Refuse a key nothing reads, and send settings.toml keys to
|
||||
// settings.toml, before config.toml is touched (#6563).
|
||||
match codewhale_tui::config_keys::config_key_home(&key) {
|
||||
codewhale_tui::config_keys::ConfigKeyHome::ConfigToml => {
|
||||
match codewhale_runtime::config_keys::config_key_home(&key) {
|
||||
codewhale_runtime::config_keys::ConfigKeyHome::ConfigToml => {
|
||||
// A value typed or validated by its config.toml reader.
|
||||
if let Some(typed) =
|
||||
codewhale_tui::config_keys::config_toml_value(&key, &value)?
|
||||
codewhale_runtime::config_keys::config_toml_value(&key, &value)?
|
||||
{
|
||||
store.config.extras.insert(key.trim().to_string(), typed);
|
||||
store.save()?;
|
||||
@@ -5474,15 +5507,17 @@ fn run_config_command(
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
codewhale_tui::config_keys::ConfigKeyHome::SettingsToml => {
|
||||
codewhale_runtime::config_keys::ConfigKeyHome::SettingsToml => {
|
||||
refuse_workspace_scoped_settings_key(store, &key)?;
|
||||
let path = codewhale_tui::config_keys::set_settings_value(&key, &value)?;
|
||||
let path = codewhale_runtime::config_keys::set_settings_value(&key, &value)?;
|
||||
println!("set {key} in {}", path.display());
|
||||
note_unread_config_copy(store, &key);
|
||||
return Ok(());
|
||||
}
|
||||
codewhale_tui::config_keys::ConfigKeyHome::Unknown => {
|
||||
bail!(codewhale_tui::config_keys::unknown_config_key_message(&key));
|
||||
codewhale_runtime::config_keys::ConfigKeyHome::Unknown => {
|
||||
bail!(codewhale_runtime::config_keys::unknown_config_key_message(
|
||||
&key
|
||||
));
|
||||
}
|
||||
}
|
||||
store.config.set_value(&key, &value)?;
|
||||
@@ -5591,7 +5626,7 @@ fn run_config_command(
|
||||
.context("config must be a TOML table")?
|
||||
.insert(
|
||||
"stream".to_string(),
|
||||
codewhale_tui::config_keys::resolved_stream_config(&store.config)?,
|
||||
codewhale_runtime::config_keys::resolved_stream_config(&store.config)?,
|
||||
);
|
||||
print!("{}", toml::to_string_pretty(&document)?);
|
||||
Ok(())
|
||||
@@ -5681,7 +5716,8 @@ fn refuse_workspace_scoped_settings_key(store: &ConfigStore, key: &str) -> Resul
|
||||
/// never a config.toml copy that nothing reads.
|
||||
fn settings_key_value(store: &ConfigStore, key: &str) -> Result<String> {
|
||||
refuse_workspace_scoped_settings_key(store, key)?;
|
||||
codewhale_tui::config_keys::settings_value(key)?.ok_or_else(|| anyhow!("key not found: {key}"))
|
||||
codewhale_runtime::config_keys::settings_value(key)?
|
||||
.ok_or_else(|| anyhow!("key not found: {key}"))
|
||||
}
|
||||
|
||||
/// Point at a config.toml copy of a settings.toml key: nothing reads it.
|
||||
@@ -5720,7 +5756,7 @@ fn apply_per_run_overrides(store: &mut ConfigStore, specs: &[String]) -> Result<
|
||||
fn run_config_doctor(store: &ConfigStore) -> Result<()> {
|
||||
println!("# {}", store.path().display());
|
||||
let mut errors: Vec<String> = Vec::new();
|
||||
let unread = codewhale_tui::config_keys::unread_config_keys(
|
||||
let unread = codewhale_runtime::config_keys::unread_config_keys(
|
||||
store.config.extras.keys().map(String::as_str),
|
||||
);
|
||||
for finding in &unread {
|
||||
@@ -7221,7 +7257,7 @@ mod tests {
|
||||
&overlays,
|
||||
)
|
||||
.unwrap();
|
||||
let stream = codewhale_tui::config_keys::resolved_stream_config(&store.config).unwrap();
|
||||
let stream = codewhale_runtime::config_keys::resolved_stream_config(&store.config).unwrap();
|
||||
assert_eq!(stream["open_timeout_secs"].as_integer(), Some(120));
|
||||
assert_eq!(stream["max_resumes"].as_integer(), Some(6));
|
||||
assert_eq!(
|
||||
@@ -7243,7 +7279,7 @@ mod tests {
|
||||
"zzz_unknown = 1\ncalm_mode = \"false\"\nmax_subagents = 4\n",
|
||||
);
|
||||
let store = ConfigStore::load(Some(path)).expect("load fixture");
|
||||
let unread = codewhale_tui::config_keys::unread_config_keys(
|
||||
let unread = codewhale_runtime::config_keys::unread_config_keys(
|
||||
store.config.extras.keys().map(String::as_str),
|
||||
);
|
||||
assert_eq!(unread.len(), 2, "{unread:#?}");
|
||||
|
||||
@@ -29,6 +29,8 @@ use std::time::Duration;
|
||||
use crate::hashing::sha256_hex;
|
||||
|
||||
mod compiled_host;
|
||||
#[cfg(any(unix, windows))]
|
||||
pub(crate) mod uninstall;
|
||||
|
||||
const GITHUB_LATEST_RELEASE_PAGE_URL: &str =
|
||||
"https://github.com/codewhale-hq/CodeWhale/releases/latest";
|
||||
@@ -62,10 +64,6 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
let channel = ReleaseChannel::from_beta_flag(beta);
|
||||
let current_version = env!("CARGO_PKG_VERSION");
|
||||
let build_version = env!("CODEWHALE_BUILD_VERSION");
|
||||
// The build script marks an unstamped checkout with `(dev)` and nothing
|
||||
// else: a release artifact carries its short SHA and a Cargo source
|
||||
// package carries the plain version. Only the checkout marker means the
|
||||
// installed file has no published artifact behind it.
|
||||
let unreleased_checkout_build = build_version.ends_with(" (dev)");
|
||||
let proxy = proxy_arg
|
||||
.as_deref()
|
||||
@@ -209,6 +207,10 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
// Step 4: Replace command paths only after the download and the running
|
||||
// executable identity verify. The preflight happens before a colocated
|
||||
// compatibility path can change, then the identity is checked just in time.
|
||||
#[cfg(unix)]
|
||||
let mut command_receipt = uninstall::CommandReceipt::capture(¤t_exe)?;
|
||||
#[cfg(windows)]
|
||||
let had_receipt = uninstall::CommandReceipt::capture(¤t_exe)?.is_some();
|
||||
let mut host_update = compiled_host::prepare(
|
||||
&download,
|
||||
latest_tag,
|
||||
@@ -226,13 +228,38 @@ pub fn run_update(beta: bool, check_only: bool, proxy_arg: Option<String>) -> Re
|
||||
validate_update_target(target, &executable_identity)
|
||||
})
|
||||
})();
|
||||
#[cfg(unix)]
|
||||
let receipt_renewal = command_receipt
|
||||
.as_mut()
|
||||
.map(|receipt| receipt.renew(&bytes))
|
||||
.transpose();
|
||||
#[cfg(windows)]
|
||||
let receipt_renewal = if had_receipt {
|
||||
uninstall::CommandReceipt::capture_accepting(¤t_exe, Some(&sha256_hex(&bytes)))
|
||||
.and_then(|receipt| {
|
||||
receipt
|
||||
.context("command ownership receipt disappeared during update")?
|
||||
.renew(&bytes)
|
||||
})
|
||||
.map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
if let Err(error) = replaced {
|
||||
if let Some(host) = &mut host_update {
|
||||
host.rollback()
|
||||
.context("compiled-host rollback failed; retained backup path is reported below")?;
|
||||
}
|
||||
#[cfg(any(unix, windows))]
|
||||
if let Err(renewal) = receipt_renewal {
|
||||
return Err(error.context(format!(
|
||||
"command ownership receipt could not be renewed after partial update: {renewal:#}"
|
||||
)));
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
#[cfg(any(unix, windows))]
|
||||
receipt_renewal.context("binaries updated, but command ownership receipt renewal failed; preserved receipt/recovery paths require attention")?;
|
||||
if let Some(host) = &host_update {
|
||||
println!(
|
||||
"Updated the qualified compiled image, notices and relink source beside this CLI; Node remains default."
|
||||
@@ -1304,7 +1331,10 @@ fn parse_checksum_manifest(text: &str) -> Result<HashMap<String, String>> {
|
||||
bail!("invalid SHA256 manifest line {}: {trimmed}", index + 1);
|
||||
}
|
||||
|
||||
let (hash, rest) = trimmed.split_at(64);
|
||||
let (hash, rest) = trimmed
|
||||
.get(..64)
|
||||
.zip(trimmed.get(64..))
|
||||
.with_context(|| format!("invalid SHA256 manifest line {}: {trimmed}", index + 1))?;
|
||||
if !hash.chars().all(|ch| ch.is_ascii_hexdigit())
|
||||
|| rest.is_empty()
|
||||
|| !rest.chars().next().is_some_and(char::is_whitespace)
|
||||
|
||||
@@ -265,6 +265,45 @@ impl Prepared {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(unix, windows))]
|
||||
pub(super) fn owned_for_uninstall(directory: &Path, target: &str) -> Result<Vec<GuardedFile>> {
|
||||
let Some(mut receipt) = GuardedFile::open_bounded(directory, RECEIPT, 64 * 1024)? else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
receipt.verify_owned()?;
|
||||
let catalog = parse(&receipt.read_bounded(64 * 1024)?, env!("CARGO_PKG_VERSION"))?;
|
||||
let host = catalog
|
||||
.hosts
|
||||
.iter()
|
||||
.find(|host| host.target == target)
|
||||
.context("installed compiled-host receipt has no matching target")?;
|
||||
let mut files = Vec::new();
|
||||
let host_name = format!("codewhale-extension-host{}", std::env::consts::EXE_SUFFIX);
|
||||
for (name, expected) in [
|
||||
(host_name.as_str(), host.sha256.as_str()),
|
||||
(
|
||||
"codewhale-extension-host.LICENSES.txt",
|
||||
host.notices_sha256.as_str(),
|
||||
),
|
||||
(
|
||||
"codewhale-extension-host.relink-source.tar.gz",
|
||||
host.source_sha256.as_str(),
|
||||
),
|
||||
] {
|
||||
if let Some(file) = GuardedFile::open(directory, name)? {
|
||||
if file.sha256() != expected {
|
||||
bail!(
|
||||
"Preserved changed compiled-host file: {}",
|
||||
directory.join(name).display()
|
||||
);
|
||||
}
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
files.push(receipt);
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
pub(super) fn required_for(current_exe: &Path) -> Result<bool> {
|
||||
let directory = current_exe
|
||||
.parent()
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
use super::*;
|
||||
use codewhale_tui::delivery_files::GuardedFile;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const RECEIPT: &str = ".codewhale-install.sha256";
|
||||
const MAX_RECEIPT: usize = 512;
|
||||
#[cfg(windows)]
|
||||
const COMMANDS: &[&str] = &["codewhale.exe", "codew.exe", "codewhale.bat"];
|
||||
#[cfg(not(windows))]
|
||||
const COMMANDS: &[&str] = &["codewhale", "codew"];
|
||||
|
||||
pub(super) struct CommandReceipt {
|
||||
directory: PathBuf,
|
||||
receipt: GuardedFile,
|
||||
hashes: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
impl CommandReceipt {
|
||||
pub(super) fn capture(executable: &Path) -> Result<Option<Self>> {
|
||||
Self::capture_accepting(executable, None)
|
||||
}
|
||||
|
||||
pub(super) fn capture_accepting(
|
||||
executable: &Path,
|
||||
updated: Option<&str>,
|
||||
) -> Result<Option<Self>> {
|
||||
let directory = executable.parent().context("executable has no parent")?;
|
||||
#[cfg(not(windows))]
|
||||
let directory = directory.canonicalize()?;
|
||||
#[cfg(windows)]
|
||||
let directory = directory.to_path_buf();
|
||||
let Some(mut receipt) = GuardedFile::open_bounded(&directory, RECEIPT, MAX_RECEIPT)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
receipt.verify_owned()?;
|
||||
let bytes = receipt.read_bounded(MAX_RECEIPT)?;
|
||||
let text = std::str::from_utf8(&bytes).context("command receipt is not UTF-8")?;
|
||||
let hashes = parse_checksum_manifest(text)?;
|
||||
if hashes.is_empty()
|
||||
|| hashes.len() > COMMANDS.len()
|
||||
|| text.lines().count() != hashes.len()
|
||||
|| hashes.keys().any(|name| !COMMANDS.contains(&name.as_str()))
|
||||
{
|
||||
bail!(
|
||||
"Invalid command ownership receipt {}; no files were changed",
|
||||
directory.join(RECEIPT).display()
|
||||
);
|
||||
}
|
||||
for (name, digest) in &hashes {
|
||||
if let Some(mut file) = GuardedFile::open(&directory, name)? {
|
||||
file.verify_owned()?;
|
||||
if file.sha256() != digest.as_str() && Some(file.sha256()) != updated {
|
||||
bail!(
|
||||
"Preserved modified command {}; receipt no longer matches",
|
||||
directory.join(name).display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(Self {
|
||||
directory,
|
||||
receipt,
|
||||
hashes: hashes.into_iter().collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn renew(&mut self, verified: &[u8]) -> Result<()> {
|
||||
self.receipt.verify_owned()?;
|
||||
let new_hash = sha256_hex(verified);
|
||||
let mut text = String::new();
|
||||
for (name, old_hash) in &self.hashes {
|
||||
if let Some(mut file) = GuardedFile::open(&self.directory, name)? {
|
||||
if file.sha256() != old_hash.as_str() && file.sha256() != new_hash.as_str() {
|
||||
bail!(
|
||||
"Cannot renew ownership of modified command {}",
|
||||
self.directory.join(name).display()
|
||||
);
|
||||
}
|
||||
file.verify_owned()?;
|
||||
text.push_str(&format!("{} {name}\n", file.sha256()));
|
||||
}
|
||||
}
|
||||
if text.is_empty() {
|
||||
bail!("No receipt-owned commands remain; ownership receipt was preserved");
|
||||
}
|
||||
if self.receipt.read_bounded(MAX_RECEIPT)? == text.as_bytes() {
|
||||
return Ok(());
|
||||
}
|
||||
self.receipt.verify_owned()?;
|
||||
let mut staged = GuardedFile::stage(&self.directory, text.as_bytes(), false)?;
|
||||
staged.verify_owned()?;
|
||||
let backup = GuardedFile::recovery_name();
|
||||
self.receipt.move_to_vacant(&backup).with_context(|| {
|
||||
format!(
|
||||
"receipt retirement failed; recovery at {}",
|
||||
self.directory.join(self.receipt.name()).display()
|
||||
)
|
||||
})?;
|
||||
if let Err(error) = staged.move_to_vacant(RECEIPT) {
|
||||
let restored = self.receipt.move_to_vacant(RECEIPT);
|
||||
bail!(
|
||||
"receipt publication failed: {error}; restoration: {restored:?}; previous receipt {}, staged receipt {}",
|
||||
self.directory.join(self.receipt.name()).display(),
|
||||
self.directory.join(staged.name()).display()
|
||||
);
|
||||
}
|
||||
println!(
|
||||
"Renewed command ownership receipt; previous receipt retained at {}",
|
||||
self.directory.join(backup).display()
|
||||
);
|
||||
self.receipt = staged;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run(yes: bool, purge_data: bool) -> Result<()> {
|
||||
let selected = std::env::current_exe().ok();
|
||||
let result = remove(yes);
|
||||
report_path_copies(selected.as_deref());
|
||||
result?;
|
||||
if purge_data {
|
||||
purge(yes)
|
||||
} else {
|
||||
println!(
|
||||
"User data was not touched. To also delete Codewhale's own data directory, add --purge-data."
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(yes: bool) -> Result<()> {
|
||||
let identity = update_executable_identity()?;
|
||||
let method = InstallMethod::detect(&identity.path);
|
||||
println!("Selected executable: {}", identity.path.display());
|
||||
println!(
|
||||
"Program removal preserves user state, credentials, sessions, workspaces, shell profiles and services."
|
||||
);
|
||||
if !method.supports_self_update() {
|
||||
println!(
|
||||
"This copy belongs to {}. Remove it with: {}",
|
||||
method.label(),
|
||||
manager_command(method, &identity.path)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(uninstaller) = windows_installer_uninstaller(&identity.path) {
|
||||
println!(
|
||||
"This copy belongs to the Codewhale Windows installer. Remove it from Settings > Apps > CodeWhale, or run: \"{}\"",
|
||||
uninstaller.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if protected_update_path(&identity.path) {
|
||||
bail!(
|
||||
"System/package path was preserved. Identify its owning package manager and remove it through that manager."
|
||||
);
|
||||
}
|
||||
let primary_name = identity
|
||||
.path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.context("executable basename is not UTF-8")?
|
||||
.to_string();
|
||||
if !COMMANDS.contains(&primary_name.as_str()) {
|
||||
bail!(
|
||||
"This command name has no installer ownership contract; preserved {}",
|
||||
identity.path.display()
|
||||
);
|
||||
}
|
||||
let ownership = CommandReceipt::capture(&identity.path)?.with_context(|| format!(
|
||||
"No command ownership receipt beside {}. This legacy/manual copy is preserved. Inspect its exact path and provenance before removing it manually; reinstalling into a fresh directory creates a receipted install.", identity.path.display()))?;
|
||||
if ownership.hashes.get(&primary_name) != Some(&identity.file_hash) {
|
||||
bail!("The receipt does not own the selected executable; nothing was removed");
|
||||
}
|
||||
let directory = ownership.directory.clone();
|
||||
let mut files = Vec::new();
|
||||
let mut primary = None;
|
||||
for name in ownership.hashes.keys() {
|
||||
if let Some(file) = GuardedFile::open(&directory, name)? {
|
||||
if ownership.hashes.get(name).map(String::as_str) != Some(file.sha256()) {
|
||||
bail!(
|
||||
"Command changed during uninstall preflight; preserved {}",
|
||||
directory.join(name).display()
|
||||
);
|
||||
}
|
||||
if name == &primary_name {
|
||||
primary = Some(file);
|
||||
} else {
|
||||
files.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
let target =
|
||||
release_asset_stem_for_prefix("codewhale", std::env::consts::OS, std::env::consts::ARCH);
|
||||
let companions =
|
||||
compiled_host::owned_for_uninstall(&directory, target.trim_start_matches("codewhale-"))?;
|
||||
if companions.is_empty() {
|
||||
println!("Any unreceipted compiled-host files are preserved.");
|
||||
}
|
||||
files.extend(companions);
|
||||
let mut receipt = ownership.receipt;
|
||||
let receipt_bytes = receipt.read_bounded(MAX_RECEIPT)?;
|
||||
files.push(receipt);
|
||||
files.push(primary.context("selected executable disappeared during preflight")?);
|
||||
for file in &mut files {
|
||||
file.verify_owned()?;
|
||||
println!(
|
||||
"{} {}",
|
||||
if yes { "Remove" } else { "Would remove" },
|
||||
directory.join(file.name()).display()
|
||||
);
|
||||
}
|
||||
validate_primary_update_identity(&identity)?;
|
||||
if !yes {
|
||||
println!(
|
||||
"No files were changed. Run this executable again with uninstall --yes to remove only this plan."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let preflight = tempfile::Builder::new()
|
||||
.prefix(".codewhale-uninstall-preflight-")
|
||||
.tempdir_in(&directory)?
|
||||
.keep();
|
||||
std::fs::remove_dir(&preflight).with_context(|| {
|
||||
format!(
|
||||
"uninstall directory preflight failed at {}",
|
||||
preflight.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let mut removed = Vec::new();
|
||||
for file in &mut files {
|
||||
let name = file.name().to_string();
|
||||
let mut unlinked = false;
|
||||
let outcome = validate_primary_update_identity(&identity)
|
||||
.and_then(|()| file.remove_checked(&mut unlinked).map_err(Into::into));
|
||||
#[cfg(windows)]
|
||||
let outcome = match outcome {
|
||||
Err(error) if unlinked && name == primary_name => {
|
||||
let retired = directory.join(file.name());
|
||||
delete_after_exit(&retired)
|
||||
.map(|()| {
|
||||
println!(
|
||||
"The running executable was retired to {}; it is deleted after this process exits. Delete it by hand if it remains.",
|
||||
retired.display()
|
||||
)
|
||||
})
|
||||
.map_err(|schedule| {
|
||||
error.context(format!(
|
||||
"deferred removal of {} could not be scheduled: {schedule:#}",
|
||||
retired.display()
|
||||
))
|
||||
})
|
||||
}
|
||||
other => other,
|
||||
};
|
||||
if unlinked {
|
||||
removed.push(name.clone());
|
||||
}
|
||||
if let Err(error) = outcome {
|
||||
if removed.iter().any(|name| name == RECEIPT)
|
||||
&& !removed.iter().any(|name| name == &primary_name)
|
||||
{
|
||||
let restore = (|| {
|
||||
let mut staged = GuardedFile::stage(&directory, &receipt_bytes, false)?;
|
||||
staged.verify_owned()?;
|
||||
staged.move_to_vacant(RECEIPT).with_context(|| {
|
||||
format!(
|
||||
"receipt recovery bytes retained at {}",
|
||||
directory.join(staged.name()).display()
|
||||
)
|
||||
})
|
||||
})();
|
||||
if let Err(restore) = restore {
|
||||
eprintln!("Receipt recovery needs attention: {restore:#}");
|
||||
}
|
||||
}
|
||||
bail!(
|
||||
"Uninstall stopped at {name}: {error:#}. Already unlinked: {}. Any retained recovery locations are reported above; no unknown destination is overwritten.",
|
||||
removed.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
for entry in std::fs::read_dir(&directory)?.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if name.starts_with("codewhale.old-") || name.starts_with("codew.old-") {
|
||||
println!(
|
||||
"Preserved a copy left by an earlier self-update; delete it by hand: {}",
|
||||
entry.path().display()
|
||||
);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"Removed this receipted release installation. Data and other installations are unchanged. {}",
|
||||
if cfg!(windows) {
|
||||
"Remove its directory from your user PATH if you added it, then open a new terminal."
|
||||
} else {
|
||||
"Run hash -r in your shell."
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn quoted(path: &Path) -> String {
|
||||
format!("\"{}\"", path.display())
|
||||
}
|
||||
|
||||
fn manager_command(method: InstallMethod, executable: &Path) -> String {
|
||||
let real = std::fs::canonicalize(executable).unwrap_or_else(|_| executable.to_path_buf());
|
||||
let names: Vec<_> = real.components().map(|part| part.as_os_str()).collect();
|
||||
let position = |marker: &str| {
|
||||
names
|
||||
.iter()
|
||||
.position(|name| name.eq_ignore_ascii_case(marker))
|
||||
};
|
||||
let prefix = |count: usize| names[..count].iter().collect::<PathBuf>();
|
||||
match method {
|
||||
InstallMethod::Cargo => match real.parent().and_then(Path::parent) {
|
||||
Some(root) => format!("cargo uninstall codewhale-cli --root {}", quoted(root)),
|
||||
None => method.uninstall_command().to_string(),
|
||||
},
|
||||
InstallMethod::Homebrew => {
|
||||
match position("Cellar").and_then(|index| names.get(index + 1)) {
|
||||
Some(formula) => format!("brew uninstall {}", formula.to_string_lossy()),
|
||||
None => method.uninstall_command().to_string(),
|
||||
}
|
||||
}
|
||||
InstallMethod::Npm => match position("node_modules") {
|
||||
Some(index) if index > 0 => {
|
||||
let mut root = prefix(index);
|
||||
if !cfg!(windows) && root.file_name().is_some_and(|name| name == "lib") {
|
||||
root.pop();
|
||||
}
|
||||
format!("npm uninstall -g codewhale --prefix {}", quoted(&root))
|
||||
}
|
||||
_ => method.uninstall_command().to_string(),
|
||||
},
|
||||
_ => method.uninstall_command().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn windows_installer_uninstaller(executable: &Path) -> Option<PathBuf> {
|
||||
let bin = executable.parent()?;
|
||||
if !bin
|
||||
.file_name()
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case("bin"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let root = bin.parent()?;
|
||||
let uninstaller = root.join("Uninstall.exe");
|
||||
[&uninstaller, &root.join("update-user-path.ps1")]
|
||||
.iter()
|
||||
.all(|path| std::fs::symlink_metadata(path).is_ok_and(|meta| meta.is_file()))
|
||||
.then_some(uninstaller)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn delete_after_exit(path: &Path) -> Result<()> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
const DETACHED_PROCESS: u32 = 0x0000_0008;
|
||||
let system = std::env::var_os("SystemRoot").context("SystemRoot is not set")?;
|
||||
let powershell = Path::new(&system).join(r"System32\WindowsPowerShell\v1.0\powershell.exe");
|
||||
let literal = path.display().to_string().replace('\'', "''");
|
||||
std::process::Command::new(powershell)
|
||||
.args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"])
|
||||
.arg(format!(
|
||||
"Wait-Process -Id {} -ErrorAction SilentlyContinue; Remove-Item -LiteralPath '{literal}' -Force",
|
||||
std::process::id()
|
||||
))
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.creation_flags(DETACHED_PROCESS)
|
||||
.spawn()
|
||||
.context("failed to start the deferred deletion helper")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn purge(yes: bool) -> Result<()> {
|
||||
let home = codewhale_config::codewhale_home()?;
|
||||
let explicit = codewhale_config::codewhale_home_is_explicit();
|
||||
let mut targets = vec![home.clone()];
|
||||
if !explicit {
|
||||
targets.push(codewhale_config::legacy_deepseek_home()?.join("snapshots"));
|
||||
}
|
||||
let user_home = dirs::home_dir().context("failed to resolve home directory")?;
|
||||
println!(
|
||||
"Data purge scope: Codewhale's config, plaintext secrets file, sessions, logs, skills, plugins, tasks and caches in the directories below. Nothing else is searched for."
|
||||
);
|
||||
let mut present = Vec::new();
|
||||
for target in targets {
|
||||
let metadata = match std::fs::symlink_metadata(&target) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
|
||||
println!("Already absent: {}", target.display());
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(error)
|
||||
.with_context(|| format!("failed to inspect {}", target.display()));
|
||||
}
|
||||
};
|
||||
if !metadata.is_dir() || user_home.starts_with(&target) || target.parent().is_none() {
|
||||
bail!(
|
||||
"Preserved {}: it is not a real directory, or it is your home or one of its parents; no data was deleted",
|
||||
target.display()
|
||||
);
|
||||
}
|
||||
if explicit && target == home && target.file_name().is_none_or(|name| name != ".codewhale")
|
||||
{
|
||||
bail!(
|
||||
"Preserved explicit CODEWHALE_HOME {}: review it and delete it by hand; no data was deleted",
|
||||
target.display()
|
||||
);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
if metadata.uid()
|
||||
!= codewhale_config::private_directory::PrivateDirectory::current_user_id()
|
||||
{
|
||||
bail!(
|
||||
"Preserved {}: it belongs to another user; no data was deleted",
|
||||
target.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{} {}",
|
||||
if yes { "Delete" } else { "Would delete" },
|
||||
target.display()
|
||||
);
|
||||
present.push(target);
|
||||
}
|
||||
println!(
|
||||
"Not covered: OS keychain/keyring entries (run codewhale auth clear --provider <id> before removing the program), per-workspace .codewhale directories, shell profile lines and completion files."
|
||||
);
|
||||
if !yes {
|
||||
println!("No data was changed. Add --yes to delete exactly these directories.");
|
||||
return Ok(());
|
||||
}
|
||||
for target in present {
|
||||
std::fs::remove_dir_all(&target).with_context(|| {
|
||||
format!(
|
||||
"data purge stopped at {}; inspect what remains",
|
||||
target.display()
|
||||
)
|
||||
})?;
|
||||
println!("Deleted {}", target.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_path_copies(selected: Option<&Path>) {
|
||||
let extensions: &[&str] = if cfg!(windows) {
|
||||
&[".exe", ".cmd", ".bat", ".ps1"]
|
||||
} else {
|
||||
&[""]
|
||||
};
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut remaining = false;
|
||||
for directory in std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()) {
|
||||
for name in [
|
||||
"codewhale",
|
||||
"codew",
|
||||
"codewhale-tui",
|
||||
"deepseek",
|
||||
"deepseek-tui",
|
||||
] {
|
||||
for extension in extensions {
|
||||
let path = directory.join(format!("{name}{extension}"));
|
||||
if Some(path.as_path()) == selected {
|
||||
continue;
|
||||
}
|
||||
if seen.insert(path.clone()) && std::fs::symlink_metadata(&path).is_ok() {
|
||||
remaining = true;
|
||||
println!(
|
||||
"Another copy remains on PATH (preserved): {}. Run it with uninstall to see who owns it and how to remove it.",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !remaining {
|
||||
println!("No other Codewhale command remains on PATH.");
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ fn passthrough_help_leaves_configuration_and_legacy_credentials_untouched() {
|
||||
.expect("seed synthetic legacy credential");
|
||||
let legacy_bytes = fs::read(&legacy).expect("read legacy fixture");
|
||||
|
||||
for subcommand in ["exec", "init", "setup", "mcp", "review", "rc"] {
|
||||
for subcommand in ["exec", "init", "setup", "mcp", "review", "rc", "uninstall"] {
|
||||
for flag in ["--help", "-h"] {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_codewhale"))
|
||||
.env_clear()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! rewrite legacy child-server definitions. The active MCP client is separate.
|
||||
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
@@ -67,23 +67,35 @@ impl Fixture {
|
||||
.spawn()
|
||||
.expect("spawn codewhale mcp-server");
|
||||
|
||||
{
|
||||
let stdin = child.stdin.as_mut().expect("mcp-server stdin");
|
||||
for request in requests {
|
||||
writeln!(stdin, "{request}").expect("write request");
|
||||
let mut stdin = child.stdin.take().expect("mcp-server stdin");
|
||||
let mut stdout = BufReader::new(child.stdout.take().expect("mcp-server stdout"));
|
||||
let mut responses = Vec::new();
|
||||
for request in requests {
|
||||
writeln!(stdin, "{request}").expect("write request");
|
||||
stdin.flush().expect("flush request");
|
||||
if let Some(id) = request.get("id") {
|
||||
let mut line = String::new();
|
||||
assert!(stdout.read_line(&mut line).expect("read response") > 0);
|
||||
let response: Value = serde_json::from_str(&line).expect("stdout is only JSON-RPC");
|
||||
assert_eq!(response.get("id"), Some(id));
|
||||
responses.push(response);
|
||||
}
|
||||
}
|
||||
|
||||
drop(stdin);
|
||||
let mut remainder = String::new();
|
||||
stdout
|
||||
.read_to_string(&mut remainder)
|
||||
.expect("read remaining output");
|
||||
assert!(
|
||||
remainder.trim().is_empty(),
|
||||
"unexpected response: {remainder}"
|
||||
);
|
||||
let output = child.wait_with_output().expect("mcp-server output");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"native MCP server failed: {stderr}"
|
||||
);
|
||||
let responses = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.map(|line| serde_json::from_str::<Value>(line).expect("stdout is only JSON-RPC"))
|
||||
.collect();
|
||||
(responses, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,7 @@ pub struct ConfigStatusView {
|
||||
pub cache_miss_tokens: u32,
|
||||
pub cost: f64,
|
||||
pub currency: CommandCurrency,
|
||||
pub cost_label: Option<String>,
|
||||
pub metrics: StatusMetrics,
|
||||
pub ascii_safe: bool,
|
||||
pub tool_outputs: StatusToolOutputs,
|
||||
|
||||
@@ -48,9 +48,10 @@ impl CommandCapabilities {
|
||||
/// basic session capability.
|
||||
pub const SESSION_LIFECYCLE: Self = Self(1 << 13);
|
||||
/// Session-control host data (FEAT-024 D3), the next non-conflicting bit
|
||||
/// after `SESSION_LIFECYCLE`. Required only by the six host-dependent
|
||||
/// after `SESSION_LIFECYCLE`. Required only by the seven host-dependent
|
||||
/// control commands (`/relay`, `/rename`, `/resume`, `/rc`, `/remote-env`,
|
||||
/// `/title`); `/remote-env` also declares `PRESENTATION`. Bit 14 fit the
|
||||
/// `/title`, `/color`); `/remote-env` and `/color` also declare
|
||||
/// `PRESENTATION`. Bit 14 fit the
|
||||
/// original `u16` backing without speculative widening in FEAT-023.
|
||||
pub const SESSION_CONTROL: Self = Self(1 << 14);
|
||||
/// Session-export host data (FEAT-025 D1), the next non-conflicting bit
|
||||
|
||||
@@ -10,6 +10,10 @@ description = "Config schema and precedence model for Codewhale"
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
schema = ["dep:schemars"]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
codewhale-protocol = { path = "../protocol", version = "0.10.2" }
|
||||
anyhow.workspace = true
|
||||
@@ -18,6 +22,7 @@ chrono.workspace = true
|
||||
ring = "0.17.14"
|
||||
rust_decimal = { version = "1.43.0", default-features = false, features = ["std"] }
|
||||
semver.workspace = true
|
||||
schemars = { version = "1.2.1", optional = true, features = ["derive", "preserve_order"] }
|
||||
codewhale-execpolicy = { path = "../execpolicy", version = "0.10.2" }
|
||||
codewhale-paths = { path = "../paths", version = "0.10.2" }
|
||||
codewhale-secrets = { path = "../secrets", version = "0.10.2" }
|
||||
@@ -26,6 +31,7 @@ libc = "0.2"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
url = "2.5"
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
toml_edit.workspace = true
|
||||
|
||||
@@ -6102,7 +6102,6 @@
|
||||
"knowledge": "2026-03-01",
|
||||
"release_date": "2026-05-29",
|
||||
"last_updated": "2026-05-29",
|
||||
"default": true,
|
||||
"attachment": true,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [
|
||||
@@ -6150,6 +6149,7 @@
|
||||
"description": "StepFun's next-generation flagship base model for coding and professional knowledge work, with native text, image, and video input and a 1M-token context window",
|
||||
"release_date": "2026-09-16",
|
||||
"last_updated": "2026-09-20",
|
||||
"default": true,
|
||||
"attachment": true,
|
||||
"reasoning": true,
|
||||
"reasoning_options": [
|
||||
|
||||
@@ -1133,7 +1133,7 @@
|
||||
"id": "stepfun",
|
||||
"label": "StepFun / StepFlash",
|
||||
"base_url": "https://api.stepfun.ai/v1",
|
||||
"default_model": "step-3.7-flash",
|
||||
"default_model": "step-5-preview",
|
||||
"env_vars": [
|
||||
"STEPFUN_API_KEY",
|
||||
"STEP_API_KEY"
|
||||
|
||||
@@ -312,7 +312,7 @@ impl CatalogOffering {
|
||||
.filter(|provider| *provider != crate::ProviderKind::Custom)
|
||||
.map(|provider| provider.provider().default_base_url())
|
||||
});
|
||||
match (&self.source, &lower.source) {
|
||||
let endpoint_facts = match (&self.source, &lower.source) {
|
||||
(
|
||||
CatalogSource::Live {
|
||||
base_url_fingerprint: current,
|
||||
@@ -323,7 +323,10 @@ impl CatalogOffering {
|
||||
..
|
||||
},
|
||||
) if current == prior
|
||||
&& base_url.is_none_or(|url| base_url_fingerprint(url) == *current) => {}
|
||||
&& base_url.is_none_or(|url| base_url_fingerprint(url) == *current) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(
|
||||
CatalogSource::Live {
|
||||
base_url_fingerprint: current,
|
||||
@@ -333,8 +336,9 @@ impl CatalogOffering {
|
||||
| CatalogSource::ModelsDevLive { .. }
|
||||
| CatalogSource::CloudFacts { .. }
|
||||
| CatalogSource::CodewhaleBundled { .. },
|
||||
) if published_base.is_some_and(|url| base_url_fingerprint(url) == *current)
|
||||
&& base_url.is_none_or(|url| base_url_fingerprint(url) == *current) => {}
|
||||
) if base_url.is_none_or(|url| base_url_fingerprint(url) == *current) => {
|
||||
published_base.is_some_and(|url| base_url_fingerprint(url) == *current)
|
||||
}
|
||||
(
|
||||
CatalogSource::ConfigOverride | CatalogSource::UserOverride,
|
||||
CatalogSource::Bundled
|
||||
@@ -345,13 +349,20 @@ impl CatalogOffering {
|
||||
.zip(published_base)
|
||||
.is_some_and(|(current, prior)| {
|
||||
base_url_fingerprint(current) == base_url_fingerprint(prior)
|
||||
}) => {}
|
||||
}) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
self.complete_facts_from(lower)
|
||||
};
|
||||
self.complete_facts(lower, endpoint_facts)
|
||||
}
|
||||
|
||||
pub(crate) fn complete_facts_from(&mut self, lower: &Self) -> bool {
|
||||
self.complete_facts(lower, true)
|
||||
}
|
||||
|
||||
fn complete_facts(&mut self, lower: &Self, endpoint_facts: bool) -> bool {
|
||||
let mut changed = false;
|
||||
macro_rules! fill {
|
||||
($field:ident) => {
|
||||
@@ -376,10 +387,12 @@ impl CatalogOffering {
|
||||
fill!(temperature);
|
||||
fill!(open_weights);
|
||||
fill!(interleaved);
|
||||
fill!(transport);
|
||||
fill!(provider_api);
|
||||
fill!(provider_npm);
|
||||
fill!(status);
|
||||
if endpoint_facts {
|
||||
fill!(transport);
|
||||
fill!(provider_api);
|
||||
fill!(provider_npm);
|
||||
fill!(status);
|
||||
}
|
||||
fill!(experimental);
|
||||
fill!(license);
|
||||
fill!(links);
|
||||
@@ -407,7 +420,8 @@ impl CatalogOffering {
|
||||
self.limit = Some(limit);
|
||||
}
|
||||
}
|
||||
if self.cost.is_none()
|
||||
if endpoint_facts
|
||||
&& self.cost.is_none()
|
||||
&& self.cost_source.is_none()
|
||||
&& (lower.cost.is_some() || lower.cost_source.is_some())
|
||||
{
|
||||
|
||||
@@ -1433,7 +1433,7 @@ fn stepfun_bundled_coding_models_preserve_default_and_plan_pricing_boundary() {
|
||||
.find(|row| row.default_for_provider)
|
||||
.unwrap()
|
||||
.wire_model_id,
|
||||
"step-3.7-flash"
|
||||
"step-5-preview"
|
||||
);
|
||||
for row in &rows {
|
||||
assert_eq!(row.reasoning, Some(true));
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
// === Types ===
|
||||
|
||||
/// `[extension_host]`: settings for the experimental TypeScript extension
|
||||
/// host (`[features] extension_host`). User config only: project-scope config
|
||||
/// is applied by an explicit allowlist that does not include this table.
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ExtensionHostConfig {
|
||||
/// Explicit MCP protocol backend. Host is the default and supports stdio,
|
||||
/// HTTP and SSE independently of optional Native extension activation.
|
||||
#[serde(default)]
|
||||
pub mcp_backend: crate::McpBackend,
|
||||
/// Which runtime runs the host: `node` (the default), `bun`, or `auto`
|
||||
/// (Bun when a supported one is found and starts, else Node). `bun` and
|
||||
/// `auto` are opt-ins: Bun is not the default until it is qualified on
|
||||
/// every platform. An explicit `bun` or `node` never falls back to the
|
||||
/// other runtime. Unset means `node`, except that a table setting only
|
||||
/// `bun` means `bun` ([`Self::effective_runtime`]).
|
||||
#[serde(default)]
|
||||
pub runtime: Option<ExtensionHostRuntime>,
|
||||
/// Path to a Node.js runtime (`^22.19 || >=24`). When set it is the only
|
||||
/// Node candidate: if it does not run or is below the floor, Node
|
||||
/// resolution fails with that reason instead of searching `PATH`.
|
||||
/// Unset, every `node` on `PATH` is tried in order, skipping any inside
|
||||
/// a `node_modules` directory or the working directory.
|
||||
#[serde(default)]
|
||||
pub node: Option<String>,
|
||||
/// Path to a Bun runtime (>= 1.4.0). When set it is the only Bun
|
||||
/// candidate, as for `node`. Unset, `bun` on `PATH` and then
|
||||
/// `$BUN_INSTALL/bin` (default `~/.bun/bin`) are tried, with the same
|
||||
/// skips.
|
||||
#[serde(default)]
|
||||
pub bun: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionHostConfig {
|
||||
/// `runtime` as configured, else `bun` when only a Bun path is set (the
|
||||
/// table names no other runtime), else `node`.
|
||||
#[must_use]
|
||||
pub fn effective_runtime(&self) -> ExtensionHostRuntime {
|
||||
match (self.runtime, &self.node, &self.bun) {
|
||||
(Some(runtime), _, _) => runtime,
|
||||
(None, None, Some(_)) => ExtensionHostRuntime::Bun,
|
||||
(None, _, _) => ExtensionHostRuntime::Node,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `[extension_host] runtime`. Node is the default; Bun (`bun`, or `auto`,
|
||||
/// which prefers it) stays an opt-in until an explicit, recorded cutover.
|
||||
#[derive(Debug, Clone, Copy, Default, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ExtensionHostRuntime {
|
||||
Auto,
|
||||
Bun,
|
||||
#[default]
|
||||
Node,
|
||||
}
|
||||
|
||||
impl ExtensionHostRuntime {
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Auto => "auto",
|
||||
Self::Bun => "bun",
|
||||
Self::Node => "node",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `[plugins."<name>"]`: per-plugin settings, keyed by the plugin's manifest
|
||||
/// name. User config only (like `[extension_host]`): project-scope config is
|
||||
/// applied by an explicit allowlist that does not include this table, so a
|
||||
/// repository cannot configure the plugins it asks you to trust.
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginSettings {
|
||||
/// `[plugins."<name>".config]`: a TOML table delivered as the `config`
|
||||
/// argument of the plugin's `apply(ctx, config)` when the extension host
|
||||
/// activates it, validated against the plugin's own `Config` schema when it
|
||||
/// declares one. A change takes effect at `/plugin reload` (the plugin is
|
||||
/// re-activated). Values are not secrets storage: they are shown to the
|
||||
/// plugin's code, and `/plugin show` lists their keys.
|
||||
#[serde(default)]
|
||||
pub config: Option<toml::Table>,
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Events that can trigger hook execution
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HookEvent {
|
||||
/// Triggered when a new session starts
|
||||
SessionStart,
|
||||
/// Triggered when a session ends (quit, Ctrl+C)
|
||||
SessionEnd,
|
||||
/// Triggered before a user message is sent to the LLM
|
||||
MessageSubmit,
|
||||
/// Triggered before a tool is executed
|
||||
ToolCallBefore,
|
||||
/// Triggered after a tool completes (success or failure)
|
||||
ToolCallAfter,
|
||||
/// Triggered when the user changes modes (Plan, Act, Operate)
|
||||
ModeChange,
|
||||
/// Triggered when an error occurs
|
||||
OnError,
|
||||
/// Triggered after a turn completes and post-turn state has been updated
|
||||
TurnEnd,
|
||||
/// Triggered when a sub-agent is spawned
|
||||
SubagentSpawn,
|
||||
/// Triggered when a sub-agent reaches a terminal state
|
||||
SubagentComplete,
|
||||
/// Triggered immediately before each `exec_shell` invocation. The hook's
|
||||
/// stdout is parsed as `KEY=VALUE\n` lines and merged on top of the
|
||||
/// shell command's environment — useful for ephemeral credentials,
|
||||
/// per-skill PATH adjustments, or short-lived tokens (#456). Hooks that
|
||||
/// fail or time out are logged but do *not* abort the shell call; they
|
||||
/// simply contribute no env vars.
|
||||
ShellEnv,
|
||||
/// Triggered when the session becomes idle after real work: a turn
|
||||
/// finished (or a wait ended) and no prompt, approval, or continuation
|
||||
/// is outstanding (#6004). Transient tool errors never fire this by
|
||||
/// themselves; it marks "agent done, waiting for the next instruction".
|
||||
SessionIdle,
|
||||
/// Triggered when a turn ends in a terminal failure (#6004). Transient
|
||||
/// tool failures that the agent absorbs never fire this; only a turn
|
||||
/// whose final status is failed does. Hook authors that want opencode's
|
||||
/// grace-period semantics should debounce inside the hook.
|
||||
SessionError,
|
||||
/// Triggered when the agent starts waiting on the person: an approval
|
||||
/// prompt opens, a `request_user_input` question is presented, or a goal
|
||||
/// continuation is parked between passes (#6004). The payload's `reason`
|
||||
/// field is `approval`, `user_input`, or `goal_continuation`.
|
||||
WaitingForUser,
|
||||
/// Triggered when idle or waiting transitions to active work (#6004).
|
||||
/// Startup and repeated observations of the same state stay silent.
|
||||
SessionBusy,
|
||||
}
|
||||
|
||||
/// Every event name the runtime actually fires, in the order `/hooks events`
|
||||
/// and `docs/HOOKS.md` list them. Tests assert this is exhaustive so a new
|
||||
/// variant cannot ship without a documented firing point.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub const ALL_HOOK_EVENTS: [HookEvent; 15] = [
|
||||
HookEvent::SessionStart,
|
||||
HookEvent::SessionEnd,
|
||||
HookEvent::TurnEnd,
|
||||
HookEvent::MessageSubmit,
|
||||
HookEvent::ToolCallBefore,
|
||||
HookEvent::ToolCallAfter,
|
||||
HookEvent::ModeChange,
|
||||
HookEvent::OnError,
|
||||
HookEvent::SubagentSpawn,
|
||||
HookEvent::SubagentComplete,
|
||||
HookEvent::ShellEnv,
|
||||
HookEvent::SessionIdle,
|
||||
HookEvent::SessionError,
|
||||
HookEvent::WaitingForUser,
|
||||
HookEvent::SessionBusy,
|
||||
];
|
||||
|
||||
/// How much a hook's result can change what Codewhale does next.
|
||||
///
|
||||
/// This is the steering allowlist. "Observer" is a statement about
|
||||
/// Codewhale's control flow only — an observer hook is still an arbitrary
|
||||
/// shell command and can have any external side effect it likes.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HookSteering {
|
||||
/// stdout/exit code can replace or block the submitted text.
|
||||
TransformsSubmittedText,
|
||||
/// stdout/exit code can allow, deny, ask, rewrite input, or add context.
|
||||
DecidesToolCall,
|
||||
/// stdout contributes `KEY=VALUE` pairs to one `exec_shell` invocation.
|
||||
ContributesShellEnv,
|
||||
/// stdout is ignored and the result cannot change Codewhale's behavior.
|
||||
Observer,
|
||||
}
|
||||
|
||||
impl HookEvent {
|
||||
/// Get string representation for environment variable
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
HookEvent::SessionStart => "session_start",
|
||||
HookEvent::SessionEnd => "session_end",
|
||||
HookEvent::MessageSubmit => "message_submit",
|
||||
HookEvent::ToolCallBefore => "tool_call_before",
|
||||
HookEvent::ToolCallAfter => "tool_call_after",
|
||||
HookEvent::ModeChange => "mode_change",
|
||||
HookEvent::OnError => "on_error",
|
||||
HookEvent::TurnEnd => "turn_end",
|
||||
HookEvent::SubagentSpawn => "subagent_spawn",
|
||||
HookEvent::SubagentComplete => "subagent_complete",
|
||||
HookEvent::ShellEnv => "shell_env",
|
||||
HookEvent::SessionIdle => "session_idle",
|
||||
HookEvent::SessionError => "session_error",
|
||||
HookEvent::WaitingForUser => "waiting_for_user",
|
||||
HookEvent::SessionBusy => "session_busy",
|
||||
}
|
||||
}
|
||||
|
||||
/// The steering contract for this event, as implemented.
|
||||
#[must_use]
|
||||
pub fn steering(self) -> HookSteering {
|
||||
match self {
|
||||
HookEvent::MessageSubmit => HookSteering::TransformsSubmittedText,
|
||||
HookEvent::ToolCallBefore => HookSteering::DecidesToolCall,
|
||||
HookEvent::ShellEnv => HookSteering::ContributesShellEnv,
|
||||
HookEvent::SessionStart
|
||||
| HookEvent::SessionEnd
|
||||
| HookEvent::ToolCallAfter
|
||||
| HookEvent::ModeChange
|
||||
| HookEvent::OnError
|
||||
| HookEvent::TurnEnd
|
||||
| HookEvent::SubagentSpawn
|
||||
| HookEvent::SubagentComplete
|
||||
| HookEvent::SessionIdle
|
||||
| HookEvent::SessionError
|
||||
| HookEvent::SessionBusy
|
||||
| HookEvent::WaitingForUser => HookSteering::Observer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a hook result for this event can change Codewhale's own
|
||||
/// behavior. Never read this as "side-effect free" — see [`HookSteering`].
|
||||
#[must_use]
|
||||
pub fn can_steer(self) -> bool {
|
||||
!matches!(self.steering(), HookSteering::Observer)
|
||||
}
|
||||
|
||||
/// Whether this event's context carries a tool name/arguments, so
|
||||
/// `tool_name` / `tool_category` conditions can ever match.
|
||||
///
|
||||
/// `on_error` is included because the tool-failure path in
|
||||
/// `tui/tool_routing.rs` fires it with the tool name, call id, and result
|
||||
/// attached. An `on_error` firing that has no tool behind it (a transport
|
||||
/// or capacity error) simply does not match a tool predicate — it is
|
||||
/// skipped at dispatch, not rejected at load.
|
||||
#[must_use]
|
||||
pub fn provides_tool_identity(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
HookEvent::ToolCallBefore
|
||||
| HookEvent::ToolCallAfter
|
||||
| HookEvent::ShellEnv
|
||||
| HookEvent::OnError
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether this event's context can carry a real process exit code, so
|
||||
/// `exit_code` conditions can ever match. `tool_call_after` observes every
|
||||
/// completed tool and `on_error` observes the failing ones; in both cases
|
||||
/// the code is only present when the tool actually reported one
|
||||
/// (`exec_shell` and friends).
|
||||
#[must_use]
|
||||
pub fn provides_exit_code(self) -> bool {
|
||||
matches!(self, HookEvent::ToolCallAfter | HookEvent::OnError)
|
||||
}
|
||||
|
||||
/// Whether this event's context carries a mode label, so `mode`
|
||||
/// conditions can ever match. `shell_env` fires inside the `exec_shell`
|
||||
/// tool with a deliberately narrow context and has no mode.
|
||||
#[must_use]
|
||||
pub fn provides_mode(self) -> bool {
|
||||
!matches!(self, HookEvent::ShellEnv)
|
||||
}
|
||||
|
||||
/// Whether `background = true` is honored as actual scheduling for this
|
||||
/// event. Events whose result is part of the contract are always run in
|
||||
/// the foreground, so declaring them background is a config error rather
|
||||
/// than a scheduling choice.
|
||||
#[must_use]
|
||||
pub fn honors_background(self) -> bool {
|
||||
!matches!(self, HookEvent::ShellEnv)
|
||||
}
|
||||
}
|
||||
|
||||
/// Condition for when a hook should run
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[derive(Default)]
|
||||
pub enum HookCondition {
|
||||
/// Always run this hook
|
||||
#[default]
|
||||
Always,
|
||||
/// Only run for specific tool names
|
||||
ToolName {
|
||||
/// Tool name to match (e.g., "`exec_shell`", "`write_file`")
|
||||
name: String,
|
||||
},
|
||||
/// Only run for specific tool categories
|
||||
ToolCategory {
|
||||
/// Category: "safe", "`file_write`", "shell"
|
||||
category: String,
|
||||
},
|
||||
/// Only run in specific modes
|
||||
Mode {
|
||||
/// Mode: "plan", "agent", "yolo"
|
||||
mode: String,
|
||||
},
|
||||
/// Only run when exit code matches (for `ToolCallAfter` / `OnError`)
|
||||
ExitCode {
|
||||
/// Exit code to match.
|
||||
///
|
||||
/// `i64`, not `i32`: a Windows crash code such as `3221225477`
|
||||
/// (`0xC0000005`, access violation) is a real code a shell tool
|
||||
/// reports, and narrowing it would silently turn the predicate into
|
||||
/// one that can never match.
|
||||
code: i64,
|
||||
},
|
||||
/// Combine multiple conditions with AND
|
||||
All { conditions: Vec<HookCondition> },
|
||||
/// Combine multiple conditions with OR
|
||||
Any { conditions: Vec<HookCondition> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NativeShellHook {
|
||||
pub owner: crate::OwnerRef,
|
||||
pub scope: Option<crate::EntryRef>,
|
||||
pub handle: u64,
|
||||
pub dialect: String,
|
||||
pub point: String,
|
||||
pub matcher: Option<String>,
|
||||
}
|
||||
|
||||
/// A single hook definition
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Hook {
|
||||
#[serde(skip)]
|
||||
pub native_shell: Option<NativeShellHook>,
|
||||
/// The event that triggers this hook
|
||||
pub event: HookEvent,
|
||||
|
||||
/// Shell command to execute (platform shell: `sh -c` on Unix, `cmd /C` on Windows)
|
||||
pub command: String,
|
||||
|
||||
/// Optional condition for when this hook should run
|
||||
#[serde(default)]
|
||||
pub condition: Option<HookCondition>,
|
||||
|
||||
/// Timeout in seconds (default: 30)
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout_secs: u64,
|
||||
|
||||
/// Run in background (don't wait for completion)
|
||||
#[serde(default)]
|
||||
pub background: bool,
|
||||
|
||||
/// Continue if this hook fails (default: true)
|
||||
#[serde(default = "default_continue_on_error")]
|
||||
pub continue_on_error: bool,
|
||||
|
||||
/// Optional name for logging/debugging
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Content- and generation-bound authority for a plugin-contributed hook.
|
||||
/// Never accepted from TOML; only the reviewed staged adapter may attach
|
||||
/// it after parsing immutable bytes.
|
||||
#[serde(skip)]
|
||||
pub plugin_authority: Option<crate::PluginAuthority>,
|
||||
|
||||
/// Exact-byte project approval attached by the loader; never read from TOML.
|
||||
#[serde(skip)]
|
||||
pub project_authority: Option<crate::ProjectHookAuthority>,
|
||||
}
|
||||
|
||||
fn default_timeout() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
fn default_continue_on_error() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Hook {
|
||||
/// Create a new hook with minimal configuration
|
||||
pub fn new(event: HookEvent, command: &str) -> Self {
|
||||
Self {
|
||||
event,
|
||||
command: command.to_string(),
|
||||
condition: None,
|
||||
timeout_secs: 30,
|
||||
background: false,
|
||||
continue_on_error: true,
|
||||
name: None,
|
||||
plugin_authority: None,
|
||||
project_authority: None,
|
||||
native_shell: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder: set condition
|
||||
pub fn with_condition(mut self, condition: HookCondition) -> Self {
|
||||
self.condition = Some(condition);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: set timeout
|
||||
pub fn with_timeout(mut self, secs: u64) -> Self {
|
||||
self.timeout_secs = secs;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: run in background
|
||||
pub fn background(mut self) -> Self {
|
||||
self.background = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder: set name
|
||||
pub fn with_name(mut self, name: &str) -> Self {
|
||||
self.name = Some(name.to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A configured hook that can never behave the way it is written.
|
||||
///
|
||||
/// Reported by [`HooksConfig::validate`] and, for rejections, surfaced in
|
||||
/// `/hooks list` so a broken hook is visible instead of silently inert.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HookConfigProblem {
|
||||
/// Hook `name`, or `None` for an unnamed entry.
|
||||
pub name: Option<String>,
|
||||
/// The event the hook is registered for, or `None` when the problem is
|
||||
/// with a setting in the `[hooks]` table itself rather than with one
|
||||
/// entry — `default_timeout_secs` governs every hook, so pinning its
|
||||
/// rejection on an arbitrary hook would misreport the blast radius.
|
||||
pub event: Option<HookEvent>,
|
||||
/// What is wrong, in one line, with no paths or payload content.
|
||||
pub detail: String,
|
||||
/// `true` when the hook is dropped at load and will never run.
|
||||
pub rejected: bool,
|
||||
}
|
||||
|
||||
/// Configuration for hooks (loaded from config.toml)
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct HooksConfig {
|
||||
/// List of hooks to execute
|
||||
#[serde(default)]
|
||||
pub hooks: Vec<Hook>,
|
||||
|
||||
/// Global enable/disable for all hooks
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Global timeout override. When set this **replaces** every hook's own
|
||||
/// `timeout_secs` rather than only filling in for hooks that omit one —
|
||||
/// see `HookExecutor::execute_sync_inner`. Documented as-implemented in
|
||||
/// `docs/HOOKS.md`; leave unset for per-hook timeouts.
|
||||
#[serde(default)]
|
||||
pub default_timeout_secs: Option<u64>,
|
||||
|
||||
/// Working directory for hook execution (default: workspace)
|
||||
#[serde(default)]
|
||||
pub working_dir: Option<PathBuf>,
|
||||
|
||||
/// Problems found by [`HooksConfig::validate`] at load time. Never read
|
||||
/// from or written to `config.toml`; populated by
|
||||
/// the host loader so `/hooks` can show a rejected
|
||||
/// hook instead of leaving it silently inert.
|
||||
#[serde(skip)]
|
||||
pub problems: Vec<HookConfigProblem>,
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl HooksConfig {
|
||||
/// Report every configured hook that cannot behave as written.
|
||||
///
|
||||
/// A condition that references context the event never carries can never
|
||||
/// match, so a hook wearing one is inert — the dangerous version of that
|
||||
/// is a `deny` gate the operator believes is armed. Those are reported as
|
||||
/// `rejected` and dropped by the host loader rather than left
|
||||
/// to fail silently at dispatch time. Problems that only affect how a
|
||||
/// hook is scheduled are reported as warnings and the hook still runs.
|
||||
#[must_use]
|
||||
pub fn validate(&self) -> Vec<HookConfigProblem> {
|
||||
self.validate_settings()
|
||||
.into_iter()
|
||||
.chain(self.validate_indexed().into_iter().map(|(_, p)| p))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Problems with the `[hooks]` table itself, independent of any entry.
|
||||
///
|
||||
/// `default_timeout_secs = 0` is the one that matters: it *replaces* every
|
||||
/// hook's own `timeout_secs`, so the per-hook `timeout_secs = 0` rejection
|
||||
/// does nothing to stop one line from killing every hook in the config
|
||||
/// before it can produce output — including a `tool_call_before` gate,
|
||||
/// which then fails closed on every tool call.
|
||||
pub fn validate_settings(&self) -> Vec<HookConfigProblem> {
|
||||
let mut problems = Vec::new();
|
||||
if self.default_timeout_secs == Some(0) {
|
||||
problems.push(HookConfigProblem {
|
||||
name: None,
|
||||
event: None,
|
||||
detail: "`default_timeout_secs = 0` would expire every hook immediately; \
|
||||
the override is ignored and per-hook `timeout_secs` applies"
|
||||
.to_string(),
|
||||
rejected: true,
|
||||
});
|
||||
}
|
||||
problems
|
||||
}
|
||||
|
||||
/// [`Self::validate`], but each problem is paired with the index of the
|
||||
/// entry that produced it.
|
||||
///
|
||||
/// The index is the hook's identity for rejection purposes. Keying on
|
||||
/// `(name, event)` instead would make one invalid unnamed `session_start`
|
||||
/// entry delete *every* unnamed `session_start` entry, and one invalid
|
||||
/// `gate` delete every other hook also called `gate` — innocent hooks
|
||||
/// dropped because they share a label with a broken one.
|
||||
pub fn validate_indexed(&self) -> Vec<(usize, HookConfigProblem)> {
|
||||
let mut problems = Vec::new();
|
||||
for (index, hook) in self.hooks.iter().enumerate() {
|
||||
let mut condition_rejections = Vec::new();
|
||||
collect_condition_problems(hook.event, hook.condition.as_ref(), &mut |detail| {
|
||||
condition_rejections.push(detail);
|
||||
});
|
||||
|
||||
let mut push = |detail: String, rejected: bool| {
|
||||
problems.push((
|
||||
index,
|
||||
HookConfigProblem {
|
||||
name: hook.name.clone(),
|
||||
event: Some(hook.event),
|
||||
detail,
|
||||
rejected,
|
||||
},
|
||||
));
|
||||
};
|
||||
// A condition that can never match makes the hook inert, so it is
|
||||
// dropped; the rest only affect how the hook is scheduled, so the
|
||||
// hook still runs and the problem is a warning.
|
||||
for detail in condition_rejections {
|
||||
push(detail, true);
|
||||
}
|
||||
|
||||
if hook.background && !hook.event.honors_background() {
|
||||
push(
|
||||
format!(
|
||||
"`background = true` is not honored for `{}`; its stdout is the \
|
||||
contract, so it always runs in the foreground",
|
||||
hook.event.as_str()
|
||||
),
|
||||
false,
|
||||
);
|
||||
} else if hook.background && hook.event.can_steer() {
|
||||
push(
|
||||
format!(
|
||||
"`background = true` makes this `{}` hook observer-only — it is \
|
||||
submitted and never awaited, so it cannot steer the turn",
|
||||
hook.event.as_str()
|
||||
),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if hook.timeout_secs == 0 {
|
||||
push(
|
||||
"`timeout_secs = 0` expires immediately; the command is killed \
|
||||
before it can produce output"
|
||||
.to_string(),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
if hook.command.trim().is_empty() {
|
||||
push("`command` is empty".to_string(), true);
|
||||
}
|
||||
}
|
||||
problems
|
||||
}
|
||||
|
||||
/// Get hooks for a specific event
|
||||
pub fn hooks_for_event(&self, event: HookEvent) -> Vec<&Hook> {
|
||||
if !self.enabled {
|
||||
return Vec::new();
|
||||
}
|
||||
self.hooks.iter().filter(|h| h.event == event).collect()
|
||||
}
|
||||
|
||||
/// The timeout the runtime will actually apply to `hook`.
|
||||
///
|
||||
/// `[hooks].default_timeout_secs` *replaces* the per-hook value when set.
|
||||
/// This is the single owner of that rule: the executor enforces it and
|
||||
/// `/hooks list` displays it, so the listing cannot advertise a per-hook
|
||||
/// number the runtime will not use.
|
||||
#[must_use]
|
||||
pub fn effective_timeout_secs(&self, hook: &Hook) -> u64 {
|
||||
// `filter`, not `unwrap_or`: `apply_validation` already strips a zero
|
||||
// override at load, but a `HooksConfig` can also be built in code, and
|
||||
// a zero here means "kill every hook before it speaks".
|
||||
self.default_timeout_secs
|
||||
.filter(|secs| *secs > 0)
|
||||
.unwrap_or(hook.timeout_secs)
|
||||
}
|
||||
|
||||
/// `true` when `[hooks].default_timeout_secs` is overriding per-hook
|
||||
/// timeouts, so surfaces can name the provenance of the number they show.
|
||||
#[must_use]
|
||||
pub fn timeout_is_overridden(&self) -> bool {
|
||||
// An ignored zero override is not provenance: `/hooks list` must not
|
||||
// credit a number to a setting the runtime refused to apply.
|
||||
self.default_timeout_secs.is_some_and(|secs| secs > 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk a condition tree and report every predicate the event can never
|
||||
/// satisfy. `all` / `any` are walked so a nested unsupported predicate is
|
||||
/// caught rather than hidden behind a combinator.
|
||||
fn collect_condition_problems(
|
||||
event: HookEvent,
|
||||
condition: Option<&HookCondition>,
|
||||
reject: &mut impl FnMut(String),
|
||||
) {
|
||||
let Some(condition) = condition else {
|
||||
return;
|
||||
};
|
||||
match condition {
|
||||
HookCondition::Always => {}
|
||||
HookCondition::ToolName { .. } | HookCondition::ToolCategory { .. } => {
|
||||
if !event.provides_tool_identity() {
|
||||
reject(format!(
|
||||
"`{}` never carries a tool name, so this tool condition can never match",
|
||||
event.as_str()
|
||||
));
|
||||
}
|
||||
}
|
||||
HookCondition::Mode { .. } => {
|
||||
if !event.provides_mode() {
|
||||
reject(format!(
|
||||
"`{}` runs with a narrow context that has no mode, so a `mode` \
|
||||
condition can never match; scope it with `tool_name` or \
|
||||
`tool_category` instead",
|
||||
event.as_str()
|
||||
));
|
||||
}
|
||||
}
|
||||
HookCondition::ExitCode { .. } => {
|
||||
if !event.provides_exit_code() {
|
||||
reject(format!(
|
||||
"`{}` has no completed process to read an exit code from; \
|
||||
`exit_code` conditions are only supported on `tool_call_after` \
|
||||
and `on_error`",
|
||||
event.as_str()
|
||||
));
|
||||
}
|
||||
}
|
||||
HookCondition::All { conditions } | HookCondition::Any { conditions } => {
|
||||
for nested in conditions {
|
||||
// Reborrow rather than pass `reject` itself: `&mut F` also
|
||||
// implements `FnMut`, so passing it directly would recurse in
|
||||
// the type parameter and never finish monomorphizing.
|
||||
collect_condition_problems(event, Some(nested), &mut *reject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Absolute per-scope default: 100k estimated tokens.
|
||||
pub const DEFAULT_SCOPE_BUDGET_TOKENS: usize = 100_000;
|
||||
/// Per-scope default as a share of the usable window.
|
||||
pub const DEFAULT_SCOPE_BUDGET_PERCENT: u8 = 25;
|
||||
/// Default ceiling for the whole static prefix, as a share of the usable window.
|
||||
pub const DEFAULT_STATIC_PREFIX_GUARD_PERCENT: u8 = 80;
|
||||
/// Hard memory-safety ceiling for one instruction file (8 MiB).
|
||||
pub const DEFAULT_FILE_MAX_BYTES: u64 = 8 * 1024 * 1024;
|
||||
const MIN_STATIC_RESIDUAL_PERCENT: u8 = 10;
|
||||
|
||||
/// What to do when an instruction scope does not fit.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InstructionBudgetPolicy {
|
||||
/// Trim the scope and continue with a warning (default).
|
||||
#[default]
|
||||
Trim,
|
||||
/// Stop the session instead of trimming or running an unsatisfiable guard.
|
||||
Refuse,
|
||||
}
|
||||
|
||||
impl InstructionBudgetPolicy {
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Trim => "trim",
|
||||
Self::Refuse => "refuse",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `[instruction_budget]` config table.
|
||||
///
|
||||
/// Every field is optional so the table only states what the user changed and
|
||||
/// the defaults stay in one place. `deny_unknown_fields` makes a typo inside
|
||||
/// the table fail loudly — the top-level config is deliberately tolerant of
|
||||
/// unknown *tables* (#6516), so silently accepting `global_budget_token`
|
||||
/// would hide the mistake forever.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, serde::Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct InstructionBudgetConfig {
|
||||
/// Absolute ceiling for the global scope, in estimated tokens.
|
||||
pub global_budget_tokens: Option<usize>,
|
||||
/// Absolute ceiling for the project scope, in estimated tokens.
|
||||
pub project_budget_tokens: Option<usize>,
|
||||
/// Global scope ceiling as a share of the usable window (1–100).
|
||||
pub global_budget_percent: Option<u8>,
|
||||
/// Project scope ceiling as a share of the usable window (1–100).
|
||||
pub project_budget_percent: Option<u8>,
|
||||
/// Ceiling for the whole static prefix as a share of the usable window.
|
||||
pub static_prefix_guard_percent: Option<u8>,
|
||||
/// Hard per-file ceiling in bytes; a larger file is skipped, not trimmed.
|
||||
pub file_max_bytes: Option<u64>,
|
||||
/// `trim` (default) or `refuse`.
|
||||
pub policy: Option<InstructionBudgetPolicy>,
|
||||
/// Read `CLAUDE.md` when no `AGENTS.md`/codewhale file is present.
|
||||
pub claude_fallback: Option<bool>,
|
||||
}
|
||||
|
||||
impl InstructionBudgetConfig {
|
||||
/// Reject values that cannot mean anything.
|
||||
///
|
||||
/// Called while config is applied, so a broken table stops the launch
|
||||
/// instead of silently producing a nonsensical budget.
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
for (name, value) in [
|
||||
("global_budget_tokens", self.global_budget_tokens),
|
||||
("project_budget_tokens", self.project_budget_tokens),
|
||||
] {
|
||||
if value == Some(0) {
|
||||
return Err(format!(
|
||||
"instruction_budget.{name} must be positive (0 is not an unlimited budget)"
|
||||
));
|
||||
}
|
||||
}
|
||||
if self
|
||||
.file_max_bytes
|
||||
.is_some_and(|value| value == 0 || value > DEFAULT_FILE_MAX_BYTES)
|
||||
{
|
||||
return Err(
|
||||
"instruction_budget.file_max_bytes must be between 1 and 8388608 (the hard ceiling cannot be raised)"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
for (name, value) in [
|
||||
("global_budget_percent", self.global_budget_percent),
|
||||
("project_budget_percent", self.project_budget_percent),
|
||||
(
|
||||
"static_prefix_guard_percent",
|
||||
self.static_prefix_guard_percent,
|
||||
),
|
||||
] {
|
||||
if let Some(value) = value
|
||||
&& !(1..=100).contains(&value)
|
||||
{
|
||||
return Err(format!(
|
||||
"instruction_budget.{name} must be between 1 and 100 (got {value})"
|
||||
));
|
||||
}
|
||||
}
|
||||
let global_percent = self
|
||||
.global_budget_percent
|
||||
.unwrap_or(DEFAULT_SCOPE_BUDGET_PERCENT);
|
||||
let project_percent = self
|
||||
.project_budget_percent
|
||||
.unwrap_or(DEFAULT_SCOPE_BUDGET_PERCENT);
|
||||
let guard = self
|
||||
.static_prefix_guard_percent
|
||||
.unwrap_or(DEFAULT_STATIC_PREFIX_GUARD_PERCENT);
|
||||
if guard < global_percent.saturating_add(project_percent) {
|
||||
return Err(format!(
|
||||
"instruction_budget.static_prefix_guard_percent ({guard}) must be at least the sum of the scope percents ({global_percent} + {project_percent})"
|
||||
));
|
||||
}
|
||||
if guard - global_percent.saturating_add(project_percent) < MIN_STATIC_RESIDUAL_PERCENT {
|
||||
return Err(format!(
|
||||
"instruction_budget.static_prefix_guard_percent ({guard}) must leave at least {MIN_STATIC_RESIDUAL_PERCENT} points above the scope percents ({global_percent} + {project_percent}) for non-instruction static content"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+129
-14
@@ -7,7 +7,10 @@ mod config_document;
|
||||
pub mod credentials;
|
||||
pub mod descriptors;
|
||||
pub mod device_code;
|
||||
pub mod extension_host;
|
||||
pub mod external_credentials;
|
||||
pub mod hooks;
|
||||
pub mod instruction_budget;
|
||||
pub mod legacy_root;
|
||||
pub mod model_reference;
|
||||
pub mod models_dev;
|
||||
@@ -25,6 +28,7 @@ pub mod resolve;
|
||||
pub mod route;
|
||||
pub mod settings_schema;
|
||||
pub mod setup_state;
|
||||
pub mod suggest;
|
||||
pub mod user_constitution;
|
||||
#[cfg(windows)]
|
||||
pub mod windows_identity;
|
||||
@@ -1724,16 +1728,130 @@ pub struct ToolsToml {
|
||||
pub extras: BTreeMap<String, toml::Value>,
|
||||
}
|
||||
|
||||
/// Protocol owner selected only by user configuration. Rust is the default;
|
||||
/// Host explicitly selects the SDK and never falls back to Rust on refusal.
|
||||
/// Existing `[workshop]` threshold configuration, retained for compatibility.
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
pub struct WorkshopConfig {
|
||||
/// Token threshold above which results become handle-only evidence.
|
||||
#[serde(default)]
|
||||
pub large_output_threshold_tokens: Option<usize>,
|
||||
|
||||
/// Per-tool threshold overrides (tool name → token limit). A tool whose
|
||||
/// name appears here uses this limit instead of
|
||||
/// `large_output_threshold_tokens`.
|
||||
#[serde(default)]
|
||||
pub per_tool_thresholds: Option<std::collections::HashMap<String, usize>>,
|
||||
|
||||
/// Optional model-visible byte budget for a single `read` / `read_file`
|
||||
/// result. Absent keeps the compile-time default (#5367).
|
||||
#[serde(default)]
|
||||
pub read_result_max_bytes: Option<usize>,
|
||||
|
||||
/// Optional model-visible byte budget for a generic tool result after
|
||||
/// spillover. Absent keeps the compile-time default (#5367).
|
||||
#[serde(default)]
|
||||
pub tool_result_max_bytes: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct PluginId(pub String);
|
||||
|
||||
impl PluginId {
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistable proof of the exact reviewed plugin authority attached to a
|
||||
/// Skill or MCP server. Runtime contexts keep this receipt instead of a
|
||||
/// pointer to mutable process-global discovery state, and revalidate it at
|
||||
/// every side-effect boundary.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginAuthority {
|
||||
pub plugin_id: PluginId,
|
||||
pub plugin_name: String,
|
||||
pub workspace: std::path::PathBuf,
|
||||
pub state_path: std::path::PathBuf,
|
||||
#[serde(default)]
|
||||
pub managed_policy_path: Option<std::path::PathBuf>,
|
||||
#[serde(skip)]
|
||||
pub managed_policy_source: Option<std::path::PathBuf>,
|
||||
pub source_manifest: std::path::PathBuf,
|
||||
pub staged_manifest: std::path::PathBuf,
|
||||
pub content_hash: String,
|
||||
pub capability_hash: String,
|
||||
/// Monotonic generation of this plugin's persisted authority. Any trust,
|
||||
/// enablement, disablement, or revocation transition invalidates receipts
|
||||
/// held by another process immediately, even when hashes are unchanged.
|
||||
pub state_generation: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProjectHookAuthority {
|
||||
pub workspace: PathBuf,
|
||||
pub digest: String,
|
||||
}
|
||||
|
||||
/// Identity of one activation. `generation` bumps on every (re)activation;
|
||||
/// `owner_token` is 128+ random bits minted by the core and handed over only
|
||||
/// in `ext/activate`. It catches bugs and stale fibers; it is not a boundary
|
||||
/// against a malicious plugin in the same process (see the design, §4.4).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OwnerRef {
|
||||
pub plugin_id: String,
|
||||
pub generation: u64,
|
||||
pub owner_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct EntryRef {
|
||||
pub path: String,
|
||||
pub sha256: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PluginId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Host uses the SDK by default; explicit Rust selection remains available.
|
||||
/// A Host refusal never falls back to Rust.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum McpBackend {
|
||||
#[default]
|
||||
Rust,
|
||||
#[default]
|
||||
Host,
|
||||
}
|
||||
|
||||
/// Return only the non-secret network authority of a configured URL.
|
||||
///
|
||||
/// Userinfo, path, query keys and values, and fragments are all omitted because
|
||||
/// every one of those components can carry credentials. Parse failures also
|
||||
/// omit the original input rather than echoing an attacker-controlled value.
|
||||
#[must_use]
|
||||
pub fn structural_url_authority(url: &str) -> String {
|
||||
let Some(parsed) = url::Url::parse(url).ok() else {
|
||||
return "unparseable (configured value omitted)".to_string();
|
||||
};
|
||||
let Some(host) = parsed.host_str() else {
|
||||
return "unparseable (configured value omitted)".to_string();
|
||||
};
|
||||
// Url::host_str already includes the brackets around an IPv6 address.
|
||||
let mut authority = format!("{}://{host}", parsed.scheme());
|
||||
if let Some(port) = parsed.port() {
|
||||
authority.push(':');
|
||||
authority.push_str(&port.to_string());
|
||||
}
|
||||
authority
|
||||
}
|
||||
|
||||
/// Default snapshot retention window: 7 days.
|
||||
pub const DEFAULT_SNAPSHOT_MAX_AGE: std::time::Duration =
|
||||
std::time::Duration::from_secs(7 * 24 * 60 * 60);
|
||||
@@ -1764,7 +1882,7 @@ pub struct SnapshotsToml {
|
||||
pub extras: BTreeMap<String, toml::Value>,
|
||||
}
|
||||
|
||||
fn default_snapshots_enabled() -> bool {
|
||||
pub fn default_snapshots_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1804,9 +1922,6 @@ pub struct FleetConfigToml {
|
||||
pub exec: FleetExecConfig,
|
||||
}
|
||||
|
||||
/// Runtime delegation has no default product depth limit. Explicit owner
|
||||
/// values still restrict child authority; zero disables delegation.
|
||||
pub const DEFAULT_SPAWN_DEPTH: u32 = u32::MAX;
|
||||
pub const DEFAULT_STREAM_CHUNK_TIMEOUT_SECS: u64 = 900;
|
||||
pub const MIN_STREAM_CHUNK_TIMEOUT_SECS: u64 = 1;
|
||||
pub const MAX_STREAM_CHUNK_TIMEOUT_SECS: u64 = 3600;
|
||||
@@ -1883,7 +1998,7 @@ pub struct FleetExecConfig {
|
||||
#[serde(default = "default_fleet_max_turns")]
|
||||
pub max_turns: u32,
|
||||
/// Recursive child-agent budget for headless fleet workers.
|
||||
/// Defaults to [`DEFAULT_SPAWN_DEPTH`], with no product depth cap.
|
||||
/// Defaults to [`codewhale_protocol::fleet::DEFAULT_SPAWN_DEPTH`], with no product depth cap.
|
||||
/// Set zero to block child delegation while preserving the root worker.
|
||||
#[serde(default = "default_fleet_max_spawn_depth")]
|
||||
pub max_spawn_depth: u32,
|
||||
@@ -1908,7 +2023,7 @@ fn default_fleet_max_turns() -> u32 {
|
||||
}
|
||||
|
||||
fn default_fleet_max_spawn_depth() -> u32 {
|
||||
DEFAULT_SPAWN_DEPTH
|
||||
codewhale_protocol::fleet::DEFAULT_SPAWN_DEPTH
|
||||
}
|
||||
|
||||
fn default_fleet_output_format() -> String {
|
||||
@@ -4747,7 +4862,7 @@ fn resolve_deepseek_base_url(
|
||||
}
|
||||
}
|
||||
|
||||
fn xiaomi_mimo_base_url_for_mode(mode: &str) -> Option<&'static str> {
|
||||
pub fn xiaomi_mimo_base_url_for_mode(mode: &str) -> Option<&'static str> {
|
||||
let normalized = mode.trim().to_ascii_lowercase().replace(['_', ' '], "-");
|
||||
if normalized.is_empty() || xiaomi_mimo_mode_uses_standard_endpoint(&normalized) {
|
||||
return None;
|
||||
@@ -4780,7 +4895,7 @@ fn xiaomi_mimo_base_url_for_mode(mode: &str) -> Option<&'static str> {
|
||||
})
|
||||
}
|
||||
|
||||
fn xiaomi_mimo_mode_uses_standard_endpoint(normalized_mode: &str) -> bool {
|
||||
pub fn xiaomi_mimo_mode_uses_standard_endpoint(normalized_mode: &str) -> bool {
|
||||
matches!(
|
||||
normalized_mode,
|
||||
"standard" | "default" | "payg" | "paygo" | "pay-as-you-go" | "pay-as-go"
|
||||
@@ -4827,7 +4942,7 @@ fn xiaomi_mimo_env_api_key_for_runtime(
|
||||
env_value(XIAOMI_MIMO_TOKEN_PLAN_ENV_VARS).or_else(|| env_value(XIAOMI_MIMO_STANDARD_ENV_VARS))
|
||||
}
|
||||
|
||||
fn resolve_xiaomi_mimo_base_url(
|
||||
pub fn resolve_xiaomi_mimo_base_url(
|
||||
configured: Option<String>,
|
||||
api_key: Option<&str>,
|
||||
mode: Option<&str>,
|
||||
@@ -4863,7 +4978,7 @@ fn resolve_xiaomi_mimo_base_url(
|
||||
}
|
||||
}
|
||||
|
||||
fn xiaomi_mimo_api_key_uses_token_plan(api_key: Option<&str>) -> bool {
|
||||
pub fn xiaomi_mimo_api_key_uses_token_plan(api_key: Option<&str>) -> bool {
|
||||
api_key.is_some_and(|key| key.trim_start().starts_with("tp-"))
|
||||
}
|
||||
|
||||
@@ -5111,7 +5226,7 @@ fn base_url_uses_local_host(base_url: &str) -> bool {
|
||||
.is_ok_and(|addr| addr.is_loopback() || addr.is_unspecified())
|
||||
}
|
||||
|
||||
fn base_url_host(base_url: &str) -> Option<&str> {
|
||||
pub fn base_url_host(base_url: &str) -> Option<&str> {
|
||||
let without_scheme = base_url
|
||||
.split_once("://")
|
||||
.map_or(base_url, |(_, rest)| rest);
|
||||
|
||||
@@ -800,7 +800,7 @@ fn reopen_windows_file_for_owner_security(file: &File, path: &Path) -> Result<Fi
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn mark_windows_file_handle_for_deletion(file: &File) -> Result<()> {
|
||||
pub fn mark_windows_file_handle_for_deletion(file: &File) -> Result<()> {
|
||||
use std::os::windows::io::AsRawHandle as _;
|
||||
use windows_sys::Win32::Foundation::{
|
||||
ERROR_INVALID_FUNCTION, ERROR_INVALID_PARAMETER, ERROR_NOT_SUPPORTED,
|
||||
|
||||
@@ -1249,7 +1249,7 @@
|
||||
"label": "StepFun / StepFlash",
|
||||
"endpoint": "https://api.stepfun.ai/v1",
|
||||
"wire": "chat-completions",
|
||||
"defaultModel": "step-3.7-flash",
|
||||
"defaultModel": "step-5-preview",
|
||||
"envVars": [
|
||||
"STEPFUN_API_KEY",
|
||||
"STEP_API_KEY"
|
||||
@@ -1271,12 +1271,12 @@
|
||||
},
|
||||
{
|
||||
"id": "step-3.7-flash",
|
||||
"canonical": "stepfun/step-3.7-flash",
|
||||
"default": true
|
||||
"canonical": "stepfun/step-3.7-flash"
|
||||
},
|
||||
{
|
||||
"id": "step-5-preview",
|
||||
"canonical": "stepfun/step-5-preview"
|
||||
"canonical": "stepfun/step-5-preview",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"transport": "chat-completions"
|
||||
|
||||
@@ -328,6 +328,26 @@ const COST_CURRENCY: &[SettingOption] = &[
|
||||
/// and the command cannot drift. Labels stay empty: an undeclared label means
|
||||
/// "show the raw value", and a capitalized literal would also have to be
|
||||
/// localized in every shipped locale to satisfy the schema message-key check.
|
||||
/// Canonical setting value for a reasoning-effort spelling, accepting the
|
||||
/// documented aliases. Unknown values are refused with the full vocabulary.
|
||||
pub fn canonical_reasoning_effort(value: &str) -> Result<&'static str, String> {
|
||||
let trimmed = value.trim();
|
||||
match trimmed.to_ascii_lowercase().as_str() {
|
||||
"off" | "disabled" | "none" | "false" => Ok("off"),
|
||||
"minimal" => Ok("minimal"),
|
||||
"low" | "minimum" | "light" => Ok("low"),
|
||||
"medium" | "mid" => Ok("medium"),
|
||||
"high" => Ok("high"),
|
||||
"xhigh" => Ok("xhigh"),
|
||||
"auto" | "automatic" => Ok("auto"),
|
||||
"ultra" | "ultracode" => Ok("ultra"),
|
||||
"max" | "maximum" => Ok("max"),
|
||||
_ => Err(format!(
|
||||
"Unrecognized reasoning effort {trimmed:?}. Expected one of: auto, off, minimal, low, medium, high, xhigh, ultra, or max."
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
const REASONING_EFFORT: &[SettingOption] = &[
|
||||
SettingOption::new("auto", "", ""),
|
||||
SettingOption::new("off", "", ""),
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#[must_use]
|
||||
pub fn edit_distance(a: &str, b: &str) -> usize {
|
||||
if a == b {
|
||||
return 0;
|
||||
}
|
||||
if a.is_empty() {
|
||||
return b.chars().count();
|
||||
}
|
||||
if b.is_empty() {
|
||||
return a.chars().count();
|
||||
}
|
||||
|
||||
let b_chars: Vec<char> = b.chars().collect();
|
||||
let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
|
||||
let mut current = vec![0usize; b_chars.len() + 1];
|
||||
|
||||
for (i, a_ch) in a.chars().enumerate() {
|
||||
current[0] = i + 1;
|
||||
for (j, b_ch) in b_chars.iter().enumerate() {
|
||||
let cost = if a_ch == *b_ch { 0 } else { 1 };
|
||||
let delete = previous[j + 1] + 1;
|
||||
let insert = current[j] + 1;
|
||||
let substitute = previous[j] + cost;
|
||||
current[j + 1] = delete.min(insert).min(substitute);
|
||||
}
|
||||
std::mem::swap(&mut previous, &mut current);
|
||||
}
|
||||
|
||||
previous[b_chars.len()]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn best_suggestion_score<'a>(
|
||||
query: &str,
|
||||
candidates: impl IntoIterator<Item = &'a str>,
|
||||
) -> Option<(u8, usize)> {
|
||||
let mut best: Option<(u8, usize)> = None;
|
||||
for candidate in candidates {
|
||||
let prefix_match = candidate.starts_with(query) || query.starts_with(candidate);
|
||||
let contains_match = candidate.contains(query) || query.contains(candidate);
|
||||
let distance = edit_distance(candidate, query);
|
||||
let close_typo = distance <= 2;
|
||||
if !(prefix_match || contains_match || close_typo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rank = if prefix_match {
|
||||
0
|
||||
} else if contains_match {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
|
||||
match best {
|
||||
Some((best_rank, best_distance))
|
||||
if rank > best_rank || (rank == best_rank && distance >= best_distance) => {}
|
||||
_ => best = Some((rank, distance)),
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
@@ -8537,7 +8537,7 @@ fn fleet_exec_config_default_matches_subagent_depth() {
|
||||
// the fleet default equals DEFAULT_SPAWN_DEPTH without a nesting cap.
|
||||
assert_eq!(
|
||||
FleetExecConfig::default().max_spawn_depth,
|
||||
DEFAULT_SPAWN_DEPTH
|
||||
codewhale_protocol::fleet::DEFAULT_SPAWN_DEPTH
|
||||
);
|
||||
assert_eq!(FleetExecConfig::default().max_spawn_depth, u32::MAX);
|
||||
}
|
||||
|
||||
@@ -286,17 +286,19 @@ fn is_symlink(p: &Path) -> bool {
|
||||
.map(|m| m.file_type().is_symlink())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
fn read_capped(p: &Path) -> Option<String> {
|
||||
let meta = std::fs::metadata(p).ok()?;
|
||||
pub fn read_project_instruction_file(file: std::fs::File) -> Option<String> {
|
||||
let meta = file.metadata().ok()?;
|
||||
if !meta.is_file() {
|
||||
return None;
|
||||
}
|
||||
if meta.len() > INSTRUCTIONS_FILE_MAX_BYTES as u64 {
|
||||
let mut file = std::fs::File::open(p).ok()?;
|
||||
let mut buf = vec![0u8; INSTRUCTIONS_FILE_MAX_BYTES];
|
||||
use std::io::Read as _;
|
||||
let n = file.read(&mut buf).ok()?;
|
||||
buf.truncate(n);
|
||||
use std::io::Read as _;
|
||||
let mut buf = Vec::new();
|
||||
file.take(INSTRUCTIONS_FILE_MAX_BYTES as u64 + 1)
|
||||
.read_to_end(&mut buf)
|
||||
.ok()?;
|
||||
let observed_len = buf.len() as u64;
|
||||
if meta.len().max(observed_len) > INSTRUCTIONS_FILE_MAX_BYTES as u64 {
|
||||
buf.truncate(INSTRUCTIONS_FILE_MAX_BYTES);
|
||||
let mut text = String::from_utf8_lossy(&buf).into_owned();
|
||||
let mut end = INSTRUCTIONS_FILE_MAX_BYTES.min(text.len());
|
||||
while end > 0 && !text.is_char_boundary(end) {
|
||||
@@ -305,11 +307,12 @@ fn read_capped(p: &Path) -> Option<String> {
|
||||
text.truncate(end);
|
||||
let omitted = meta
|
||||
.len()
|
||||
.max(observed_len)
|
||||
.saturating_sub(INSTRUCTIONS_FILE_MAX_BYTES as u64);
|
||||
text.push_str(&format!("\n[…truncated: {omitted} bytes omitted]"));
|
||||
return Some(text);
|
||||
}
|
||||
let raw = std::fs::read_to_string(p).ok()?;
|
||||
let raw = String::from_utf8(buf).ok()?;
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
@@ -392,6 +395,7 @@ pub fn selected_project_instruction_candidate_files(
|
||||
fn load_project_instruction_fragment_from_candidates(
|
||||
workspace: &Path,
|
||||
candidates: &[&str],
|
||||
mut opener: impl FnMut(&Path) -> std::io::Result<std::fs::File>,
|
||||
) -> Option<BoundedFragment> {
|
||||
let files = collect_candidate_files(workspace, candidates);
|
||||
if files.is_empty() {
|
||||
@@ -399,7 +403,7 @@ fn load_project_instruction_fragment_from_candidates(
|
||||
}
|
||||
let mut sections = Vec::new();
|
||||
for path in files {
|
||||
if let Some(content) = read_capped(&path) {
|
||||
if let Some(content) = opener(&path).ok().and_then(read_project_instruction_file) {
|
||||
let rel = path
|
||||
.strip_prefix(workspace)
|
||||
.unwrap_or(&path)
|
||||
@@ -420,7 +424,11 @@ fn load_project_instruction_fragment_from_candidates(
|
||||
}
|
||||
|
||||
pub fn load_project_instruction_fragment(workspace: &Path) -> Option<BoundedFragment> {
|
||||
load_project_instruction_fragment_from_candidates(workspace, PROJECT_INSTRUCTION_CANDIDATES)
|
||||
load_project_instruction_fragment_from_candidates(
|
||||
workspace,
|
||||
PROJECT_INSTRUCTION_CANDIDATES,
|
||||
|path| std::fs::File::open(path),
|
||||
)
|
||||
}
|
||||
|
||||
/// Load only instruction formats that the canonical TUI project-context path
|
||||
@@ -434,6 +442,7 @@ pub fn load_additional_project_instruction_fragment(workspace: &Path) -> Option<
|
||||
load_project_instruction_fragment_from_candidates(
|
||||
workspace,
|
||||
ADDITIONAL_PROJECT_INSTRUCTION_CANDIDATES,
|
||||
|path| std::fs::File::open(path),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -444,11 +453,21 @@ pub fn load_additional_project_instruction_fragment(workspace: &Path) -> Option<
|
||||
pub fn load_selected_project_instruction_fragment(
|
||||
workspace: &Path,
|
||||
candidates: &[&str],
|
||||
) -> Option<BoundedFragment> {
|
||||
load_selected_project_instruction_fragment_with_reader(workspace, candidates, |path| {
|
||||
std::fs::File::open(path)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_selected_project_instruction_fragment_with_reader(
|
||||
workspace: &Path,
|
||||
candidates: &[&str],
|
||||
opener: impl FnMut(&Path) -> std::io::Result<std::fs::File>,
|
||||
) -> Option<BoundedFragment> {
|
||||
if candidates.is_empty() {
|
||||
return None;
|
||||
}
|
||||
load_project_instruction_fragment_from_candidates(workspace, candidates)
|
||||
load_project_instruction_fragment_from_candidates(workspace, candidates, opener)
|
||||
}
|
||||
|
||||
pub fn project_instructions_from_sources(
|
||||
|
||||
@@ -129,7 +129,6 @@ pub struct PrefixChange {
|
||||
pub tools_changed: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl PrefixChange {
|
||||
/// Returns a human-readable description of what changed.
|
||||
pub fn description(&self) -> String {
|
||||
@@ -316,7 +315,6 @@ impl ToolCatalogCache {
|
||||
/// Drop every cached entry. Used by tool-registry mutation paths
|
||||
/// (e.g. plugin hot-reload, MCP attach) when the caller cannot
|
||||
/// easily prove the tool set is unchanged.
|
||||
#[allow(dead_code)] // observability; called by /cache flush and tests
|
||||
pub fn invalidate(&mut self) {
|
||||
self.by_identity.clear();
|
||||
self.insertion_order.clear();
|
||||
@@ -329,7 +327,6 @@ impl ToolCatalogCache {
|
||||
}
|
||||
|
||||
/// Returns `true` if the cache has no entries.
|
||||
#[allow(dead_code)] // observability; surfaced via /status
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.by_identity.is_empty()
|
||||
@@ -337,7 +334,6 @@ impl ToolCatalogCache {
|
||||
|
||||
/// Returns `(current_entries, capacity)` for observability. Surfaced via
|
||||
/// the `/status` chip in a follow-up; tests exercise the path.
|
||||
#[allow(dead_code)] // surfaced via /status in a follow-up; tests exercise it
|
||||
#[must_use]
|
||||
pub fn stats(&self) -> (usize, usize) {
|
||||
(self.len(), self.capacity)
|
||||
@@ -423,7 +419,6 @@ fn hash_json_value<H: Hasher>(value: &serde_json::Value, state: &mut H) {
|
||||
/// [`PrefixFingerprint::compute_with_tool_cache`] and pass the cache in
|
||||
/// directly, both to share state and to avoid the thread-local lookup
|
||||
/// on the hot path.
|
||||
#[allow(dead_code)]
|
||||
impl PrefixStabilityManager {
|
||||
/// Create a new manager and immediately pin the first fingerprint.
|
||||
pub fn new(system_text: &str, tools: Option<&[Tool]>) -> Self {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "codewhale-dependencies"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Codewhale external tool resolution and bounded subprocess probes"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
codewhale-config = { path = "../config", version = "0.10.2" }
|
||||
codewhale-paths = { path = "../paths", version = "0.10.2" }
|
||||
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
|
||||
codewhale-util = { path = "../util", version = "0.10.2" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
wait-timeout = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
|
||||
@@ -25,9 +25,12 @@
|
||||
//! — probing a binary involves a `Command::output` per candidate and
|
||||
//! we'd rather not pay that on every model turn.
|
||||
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::process::{Command, Output};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use wait_timeout::ChildExt;
|
||||
|
||||
/// Candidate executable names for the Python interpreter, in the
|
||||
/// order we try them. On Windows the launcher convention is `py -3`,
|
||||
@@ -70,7 +73,17 @@ pub fn probe_executable_with_flag(spec: &str, version_flag: &str) -> bool {
|
||||
};
|
||||
let mut cmd = version_probe_command(program);
|
||||
cmd.args(parts).arg(version_flag);
|
||||
matches!(probe_output(&mut cmd, false, VERSION_PROBE_TIMEOUT), Ok(output) if output.status.success())
|
||||
probe_found(&mut cmd)
|
||||
}
|
||||
|
||||
/// A probe that launched and was still running at its deadline found an
|
||||
/// installed tool that is slow under load; only a failed launch or a failing
|
||||
/// exit means the tool is absent.
|
||||
fn probe_found(command: &mut Command) -> bool {
|
||||
match probe_output(command, false, VERSION_PROBE_TIMEOUT) {
|
||||
Ok(output) => output.status.success(),
|
||||
Err(error) => error.kind() == std::io::ErrorKind::TimedOut,
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe a single executable and capture its version banner in one spawn.
|
||||
@@ -98,7 +111,7 @@ const VERSION_PROBE_MAX_OUTPUT: u64 = 16 * 1024;
|
||||
|
||||
fn version_probe_command(program: impl AsRef<std::ffi::OsStr>) -> Command {
|
||||
let mut cmd = Command::new(program);
|
||||
crate::utils::suppress_console_window(&mut cmd);
|
||||
codewhale_util::utils::suppress_console_window(&mut cmd);
|
||||
codewhale_runtime::child_env::apply_to_command(&mut cmd, std::iter::empty::<(&str, &str)>());
|
||||
// A presence probe needs no bootstrap code or import paths from the
|
||||
// parent. The general child allowlist retains these for normal SDK tools.
|
||||
@@ -118,7 +131,7 @@ fn probe_output(
|
||||
probe_output_bounded(command, capture, timeout, VERSION_PROBE_MAX_OUTPUT)
|
||||
}
|
||||
|
||||
pub(crate) fn probe_output_bounded(
|
||||
pub fn probe_output_bounded(
|
||||
command: &mut Command,
|
||||
capture: bool,
|
||||
timeout: std::time::Duration,
|
||||
@@ -126,7 +139,6 @@ pub(crate) fn probe_output_bounded(
|
||||
) -> std::io::Result<std::process::Output> {
|
||||
use std::io::Read;
|
||||
use std::process::Stdio;
|
||||
use wait_timeout::ChildExt;
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
@@ -244,8 +256,7 @@ fn resolve_executable_path(spec: &str, version_flag: &str) -> Option<String> {
|
||||
let mut cmd = version_probe_command(&candidate);
|
||||
cmd.args(&args).arg(version_flag);
|
||||
|
||||
if matches!(probe_output(&mut cmd, false, VERSION_PROBE_TIMEOUT), Ok(output) if output.status.success())
|
||||
{
|
||||
if probe_found(&mut cmd) {
|
||||
return Some(candidate.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
@@ -361,8 +372,8 @@ pub fn resolve_pandoc() -> Option<String> {
|
||||
/// a converter, an OCR engine) is available: `probe` decides, except that a
|
||||
/// test fixture answers with the recorded host's set so results do not
|
||||
/// depend on what the machine running them has installed (test builds only).
|
||||
pub(crate) fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> bool {
|
||||
#[cfg(test)]
|
||||
pub fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> bool {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some(available) = RECORDED_HOST_TOOLS.with(|cell| {
|
||||
cell.borrow()
|
||||
.as_ref()
|
||||
@@ -371,12 +382,12 @@ pub(crate) fn host_tool_available(tool: &str, probe: impl FnOnce() -> bool) -> b
|
||||
return available;
|
||||
}
|
||||
// Only test fixtures read the name.
|
||||
#[cfg(not(test))]
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
let _ = tool;
|
||||
probe()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
thread_local! {
|
||||
static RECORDED_HOST_TOOLS: std::cell::RefCell<Option<Vec<String>>> =
|
||||
const { std::cell::RefCell::new(None) };
|
||||
@@ -384,16 +395,16 @@ thread_local! {
|
||||
|
||||
/// Pin [`host_tool_available`] on this thread to a recorded host's tools until
|
||||
/// the guard drops.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn pin_recorded_host_tools(tools: Vec<String>) -> RecordedHostToolsGuard {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn pin_recorded_host_tools(tools: Vec<String>) -> RecordedHostToolsGuard {
|
||||
RECORDED_HOST_TOOLS.with(|cell| *cell.borrow_mut() = Some(tools));
|
||||
RecordedHostToolsGuard
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) struct RecordedHostToolsGuard;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct RecordedHostToolsGuard;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Drop for RecordedHostToolsGuard {
|
||||
fn drop(&mut self) {
|
||||
RECORDED_HOST_TOOLS.with(|cell| *cell.borrow_mut() = None);
|
||||
@@ -589,7 +600,7 @@ impl HostRuntime {
|
||||
/// runtime was not probed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HostRuntimeResolution {
|
||||
pub choice: crate::config::ExtensionHostRuntime,
|
||||
pub choice: codewhale_config::extension_host::ExtensionHostRuntime,
|
||||
pub selected: Option<HostRuntime>,
|
||||
pub bun: Option<NodeResolution>,
|
||||
pub node: Option<NodeResolution>,
|
||||
@@ -656,13 +667,13 @@ impl HostRuntimeResolution {
|
||||
}
|
||||
let (bun, node) = (HostRuntimeKind::Bun.floor(), HostRuntimeKind::Node.floor());
|
||||
let wanted = match self.choice {
|
||||
crate::config::ExtensionHostRuntime::Auto => {
|
||||
codewhale_config::extension_host::ExtensionHostRuntime::Auto => {
|
||||
format!("Bun {bun} or Node.js {node} (set `[extension_host] bun` or `node`)")
|
||||
}
|
||||
crate::config::ExtensionHostRuntime::Bun => {
|
||||
codewhale_config::extension_host::ExtensionHostRuntime::Bun => {
|
||||
format!("Bun {bun} (`runtime = \"bun\"`; set `[extension_host] bun`)")
|
||||
}
|
||||
crate::config::ExtensionHostRuntime::Node => {
|
||||
codewhale_config::extension_host::ExtensionHostRuntime::Node => {
|
||||
format!("Node.js {node} (set `[extension_host] node`)")
|
||||
}
|
||||
};
|
||||
@@ -675,7 +686,7 @@ impl HostRuntimeResolution {
|
||||
/// `SystemRoot` to load system DLLs.
|
||||
fn probe_command(path: &Path) -> Command {
|
||||
let mut cmd = Command::new(path);
|
||||
crate::utils::suppress_console_window(&mut cmd);
|
||||
codewhale_util::utils::suppress_console_window(&mut cmd);
|
||||
cmd.env_clear();
|
||||
#[cfg(windows)]
|
||||
if let Some(root) = std::env::var_os("SystemRoot") {
|
||||
@@ -686,7 +697,11 @@ fn probe_command(path: &Path) -> Command {
|
||||
cmd
|
||||
}
|
||||
|
||||
fn probe_runtime_version(kind: HostRuntimeKind, path: &Path) -> Result<(u32, u32, u32), String> {
|
||||
fn probe_runtime_version(
|
||||
kind: HostRuntimeKind,
|
||||
path: &Path,
|
||||
bundle_sha256: &str,
|
||||
) -> Result<(u32, u32, u32), String> {
|
||||
// Only absolute candidates are run: a relative `PATH` entry resolves
|
||||
// against the current (workspace) directory, where a repository could
|
||||
// plant a `node`.
|
||||
@@ -706,7 +721,7 @@ fn probe_runtime_version(kind: HostRuntimeKind, path: &Path) -> Result<(u32, u32
|
||||
return Err(format!("does not run (exit {})", output.status));
|
||||
}
|
||||
if compiled {
|
||||
return parse_compiled_host_info(&output.stdout, crate::extension_host::bundle_sha256());
|
||||
return parse_compiled_host_info(&output.stdout, bundle_sha256);
|
||||
}
|
||||
let banner = String::from_utf8_lossy(&output.stdout);
|
||||
kind.parse(&banner)
|
||||
@@ -849,9 +864,13 @@ fn untrusted_location(
|
||||
/// runtime with its reason, rather than falling through to a search.
|
||||
/// Otherwise the search candidates, minus those in an [`untrusted_location`],
|
||||
/// which are recorded as rejected without being run. Blocking.
|
||||
fn resolve_runtime(kind: HostRuntimeKind, override_path: Option<&Path>) -> NodeResolution {
|
||||
fn resolve_runtime(
|
||||
kind: HostRuntimeKind,
|
||||
override_path: Option<&Path>,
|
||||
bundle_sha256: &str,
|
||||
) -> NodeResolution {
|
||||
if let Some(path) = override_path {
|
||||
return select_runtime(kind, vec![path.to_path_buf()]);
|
||||
return select_runtime(kind, vec![path.to_path_buf()], bundle_sha256);
|
||||
}
|
||||
let cwd = std::env::current_dir().ok();
|
||||
let home = codewhale_paths::user_home();
|
||||
@@ -878,7 +897,7 @@ fn resolve_runtime(kind: HostRuntimeKind, override_path: Option<&Path>) -> NodeR
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut resolution = select_runtime(kind, candidates);
|
||||
let mut resolution = select_runtime(kind, candidates, bundle_sha256);
|
||||
skipped.append(&mut resolution.rejected);
|
||||
resolution.rejected = skipped;
|
||||
resolution
|
||||
@@ -890,23 +909,24 @@ fn resolve_runtime(kind: HostRuntimeKind, override_path: Option<&Path>) -> NodeR
|
||||
/// why Bun was passed over. Blocking: call from `spawn_blocking`.
|
||||
#[must_use]
|
||||
pub fn resolve_extension_host_runtime(
|
||||
choice: crate::config::ExtensionHostRuntime,
|
||||
choice: codewhale_config::extension_host::ExtensionHostRuntime,
|
||||
node_override: Option<&Path>,
|
||||
bun_override: Option<&Path>,
|
||||
bundle_sha256: &str,
|
||||
) -> HostRuntimeResolution {
|
||||
select_host_runtime(
|
||||
choice,
|
||||
|| resolve_runtime(HostRuntimeKind::Bun, bun_override),
|
||||
|| resolve_runtime(HostRuntimeKind::Node, node_override),
|
||||
|| resolve_runtime(HostRuntimeKind::Bun, bun_override, bundle_sha256),
|
||||
|| resolve_runtime(HostRuntimeKind::Node, node_override, bundle_sha256),
|
||||
)
|
||||
}
|
||||
|
||||
fn select_host_runtime(
|
||||
choice: crate::config::ExtensionHostRuntime,
|
||||
choice: codewhale_config::extension_host::ExtensionHostRuntime,
|
||||
probe_bun: impl FnOnce() -> NodeResolution,
|
||||
probe_node: impl FnOnce() -> NodeResolution,
|
||||
) -> HostRuntimeResolution {
|
||||
use crate::config::ExtensionHostRuntime as Choice;
|
||||
use codewhale_config::extension_host::ExtensionHostRuntime as Choice;
|
||||
let mut resolution = HostRuntimeResolution {
|
||||
choice,
|
||||
selected: None,
|
||||
@@ -953,7 +973,11 @@ fn accepted_flags(path: &Path, flags: &[&'static str]) -> Vec<&'static str> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn select_runtime(kind: HostRuntimeKind, candidates: Vec<PathBuf>) -> NodeResolution {
|
||||
fn select_runtime(
|
||||
kind: HostRuntimeKind,
|
||||
candidates: Vec<PathBuf>,
|
||||
bundle_sha256: &str,
|
||||
) -> NodeResolution {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut resolution = NodeResolution::default();
|
||||
for candidate in candidates {
|
||||
@@ -962,7 +986,7 @@ fn select_runtime(kind: HostRuntimeKind, candidates: Vec<PathBuf>) -> NodeResolu
|
||||
if !seen.insert(candidate.clone()) {
|
||||
continue;
|
||||
}
|
||||
match probe_runtime_version(kind, &candidate) {
|
||||
match probe_runtime_version(kind, &candidate, bundle_sha256) {
|
||||
Ok(version) if kind.supported(version) => {
|
||||
resolution.selected = Some((candidate, version));
|
||||
break;
|
||||
@@ -1044,7 +1068,6 @@ pub trait ExternalTool {
|
||||
|
||||
/// Convenience: run the tool with arguments and return only the
|
||||
/// exit status (discards stdout/stderr).
|
||||
#[cfg_attr(not(test), expect(dead_code))]
|
||||
fn status(args: &[&str], cwd: &std::path::Path) -> std::io::Result<std::process::ExitStatus> {
|
||||
let mut cmd = Self::command().ok_or_else(Self::not_found_error)?;
|
||||
cmd.args(args).current_dir(cwd).status()
|
||||
@@ -1060,7 +1083,7 @@ pub trait ExternalTool {
|
||||
let spec = Self::resolve()?;
|
||||
let (program, fixed_args) = split_interpreter_spec(&spec);
|
||||
let mut cmd = tokio::process::Command::new(&program);
|
||||
crate::utils::suppress_tokio_console_window(&mut cmd);
|
||||
codewhale_util::utils::suppress_tokio_console_window(&mut cmd);
|
||||
for arg in &fixed_args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
@@ -1072,7 +1095,7 @@ pub trait ExternalTool {
|
||||
fn command_for_spec(spec: &str) -> Command {
|
||||
let (program, fixed_args) = split_interpreter_spec(spec);
|
||||
let mut cmd = Command::new(&program);
|
||||
crate::utils::suppress_console_window(&mut cmd);
|
||||
codewhale_util::utils::suppress_console_window(&mut cmd);
|
||||
for arg in &fixed_args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
@@ -1109,7 +1132,7 @@ pub struct Git;
|
||||
/// This is the single definition site; [`Git::command`] and
|
||||
/// [`Git::tokio_command`] apply it to every product git spawn. Call it
|
||||
/// directly only for a non-git program that may shell out to git (`gh`).
|
||||
pub(crate) fn apply_git_noninteractive_env(cmd: &mut Command) {
|
||||
pub fn apply_git_noninteractive_env(cmd: &mut Command) {
|
||||
cmd.env("GIT_TERMINAL_PROMPT", "0").env("GIT_PAGER", "");
|
||||
if std::env::var_os("GIT_SSH_COMMAND").is_none() && std::env::var_os("GIT_SSH").is_none() {
|
||||
cmd.env("GIT_SSH_COMMAND", "ssh -o BatchMode=yes");
|
||||
@@ -1124,7 +1147,7 @@ impl Git {
|
||||
/// only. A read that touches the working tree also runs the superproject's
|
||||
/// clean filters, which no flag disables: build it from
|
||||
/// [`Self::review_command`] too.
|
||||
pub(crate) const REVIEW_DIFF_ARGS: [&'static str; 4] = [
|
||||
pub const REVIEW_DIFF_ARGS: [&'static str; 4] = [
|
||||
"--no-ext-diff",
|
||||
"--no-textconv",
|
||||
"--submodule=short",
|
||||
@@ -1136,7 +1159,7 @@ impl Git {
|
||||
/// Construct a read-only review command with content conversion disabled.
|
||||
/// Review callers also pass [`Self::REVIEW_DIFF_ARGS`] for diffs.
|
||||
/// Configured filters otherwise execute even when those flags are present.
|
||||
pub(crate) fn review_command(workspace: &Path) -> anyhow::Result<Command> {
|
||||
pub fn review_command(workspace: &Path) -> anyhow::Result<Command> {
|
||||
let overrides = Self::review_filter_overrides(workspace)?;
|
||||
let mut command = Self::review_base(workspace)?;
|
||||
let mut count = 2;
|
||||
@@ -1153,7 +1176,7 @@ impl Git {
|
||||
|
||||
/// Git with fsmonitor, hooks, lazy fetch and replace objects disabled,
|
||||
/// running in `workspace`. [`Self::review_command`] adds filter overrides.
|
||||
pub(crate) fn review_base(workspace: &Path) -> anyhow::Result<Command> {
|
||||
pub fn review_base(workspace: &Path) -> anyhow::Result<Command> {
|
||||
use anyhow::Context;
|
||||
|
||||
let mut command = Self::command().context("git not found on PATH")?;
|
||||
@@ -1178,7 +1201,7 @@ impl Git {
|
||||
.env("GIT_CONFIG_KEY_0", "codewhale.reviewConfigCapability")
|
||||
.env("GIT_CONFIG_VALUE_0", &value)
|
||||
.args(["config", "--get", "codewhale.reviewConfigCapability"]);
|
||||
match crate::snapshot::repo::run_bounded_git_with_timeout(
|
||||
match run_bounded_git_with_timeout(
|
||||
&mut probe,
|
||||
"config",
|
||||
Self::REVIEW_CONFIG_TIMEOUT,
|
||||
@@ -1229,7 +1252,7 @@ impl Git {
|
||||
/// Config overrides (`key`, `value`) that neutralize every clean/process
|
||||
/// filter driver configured for the repository at `workspace`. Callers
|
||||
/// apply them through `GIT_CONFIG_KEY_n`/`GIT_CONFIG_VALUE_n`.
|
||||
pub(crate) fn review_filter_overrides(
|
||||
pub fn review_filter_overrides(
|
||||
workspace: &Path,
|
||||
) -> anyhow::Result<Vec<(String, &'static str)>> {
|
||||
use anyhow::{Context, bail};
|
||||
@@ -1242,12 +1265,9 @@ impl Git {
|
||||
"--get-regexp",
|
||||
r"^filter\..*\.(clean|process|required)$",
|
||||
]);
|
||||
let output = crate::snapshot::repo::run_bounded_git_with_timeout(
|
||||
&mut command,
|
||||
"config",
|
||||
Self::REVIEW_CONFIG_TIMEOUT,
|
||||
)
|
||||
.context("Failed to inspect Git review filters")?;
|
||||
let output =
|
||||
run_bounded_git_with_timeout(&mut command, "config", Self::REVIEW_CONFIG_TIMEOUT)
|
||||
.context("Failed to inspect Git review filters")?;
|
||||
let no_filters =
|
||||
output.status.code() == Some(1) && output.stdout.is_empty() && output.stderr.is_empty();
|
||||
if (!output.status.success() && !no_filters)
|
||||
@@ -1314,7 +1334,7 @@ impl ExternalTool for Git {
|
||||
/// credentials. The guards below are applied after the scrub.
|
||||
fn command() -> Option<Command> {
|
||||
let mut cmd = Command::new(Self::resolve()?);
|
||||
crate::utils::suppress_console_window(&mut cmd);
|
||||
codewhale_util::utils::suppress_console_window(&mut cmd);
|
||||
codewhale_runtime::child_env::apply_to_git_command(&mut cmd);
|
||||
cmd.env("GIT_OPTIONAL_LOCKS", "0");
|
||||
apply_git_noninteractive_env(&mut cmd);
|
||||
@@ -1508,6 +1528,126 @@ pub fn split_interpreter_spec(spec: &str) -> (String, Vec<String>) {
|
||||
(program, args)
|
||||
}
|
||||
|
||||
/// Grace granted to the pipe readers after git has exited. A clean git
|
||||
/// closes its own write ends, so EOF is already waiting; only a grandchild
|
||||
/// that inherited the pipes (a post-checkout hook, a `git gc` pack worker)
|
||||
/// can hold them past exit, and it must not hold the turn pipeline either.
|
||||
const GIT_PIPE_DRAIN_GRACE: Duration = Duration::from_secs(2);
|
||||
|
||||
/// Run a pre-configured git command under `timeout` with both pipes drained
|
||||
/// while the child runs (the same concurrent drain `Command::output`
|
||||
/// performs). A wedged git degrades to an error instead of hanging the caller.
|
||||
///
|
||||
/// The timeout path kills the child and reaps it on a detached thread: on a
|
||||
/// hard-wedged mount git can sit in uninterruptible kernel I/O where even
|
||||
/// SIGKILL is deferred.
|
||||
pub fn run_bounded_git_with_timeout(
|
||||
cmd: &mut std::process::Command,
|
||||
subcommand: &str,
|
||||
timeout: Duration,
|
||||
) -> io::Result<Output> {
|
||||
cmd.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
let mut child = cmd.spawn()?;
|
||||
// Drain both pipes while waiting: the restore path's `ls-tree -r` and
|
||||
// the diff commands emit output that grows with workspace size, and a
|
||||
// child blocked on a full pipe buffer never exits — it would turn every
|
||||
// such call into a guaranteed timeout. Readers stream into shared
|
||||
// buffers, and the completion channel lets the collect below bound how
|
||||
// long a grandchild-held pipe may hold the call after git has exited.
|
||||
let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
|
||||
let stdout_pipe = child.stdout.take();
|
||||
let stderr_pipe = child.stderr.take();
|
||||
let stdout_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let stderr_buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
{
|
||||
let buf = std::sync::Arc::clone(&stdout_buf);
|
||||
let done_tx = done_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(mut reader) = stdout_pipe {
|
||||
read_pipe_to_buffer(&mut reader, &buf);
|
||||
}
|
||||
let _ = done_tx.send(());
|
||||
});
|
||||
}
|
||||
{
|
||||
let buf = std::sync::Arc::clone(&stderr_buf);
|
||||
let done_tx = done_tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(mut reader) = stderr_pipe {
|
||||
read_pipe_to_buffer(&mut reader, &buf);
|
||||
}
|
||||
let _ = done_tx.send(());
|
||||
});
|
||||
}
|
||||
drop(done_tx);
|
||||
|
||||
let Some(status) = child.wait_timeout(timeout)? else {
|
||||
let _ = child.kill();
|
||||
// Reap off the pipeline thread (see the doc comment): the kernel may
|
||||
// not deliver the kill until an uninterruptible syscall returns.
|
||||
std::thread::spawn(move || {
|
||||
let _ = child.wait();
|
||||
});
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
format!("git {subcommand} timed out after {}s", timeout.as_secs()),
|
||||
));
|
||||
};
|
||||
// Wait for the readers up to the grace, then return what was captured. A
|
||||
// clean git already closed its write ends, so both readers deliver
|
||||
// immediately; the grace only covers a pipe still held open by an
|
||||
// inherited copy (a daemonizing hook). On expiry the captured output is
|
||||
// returned with a note on stderr instead of silently truncating; the
|
||||
// reader itself ends whenever whatever holds the pipe exits, holding
|
||||
// only a pipe read — never the turn pipeline.
|
||||
let mut partial = false;
|
||||
let mut drained = 0;
|
||||
while drained < 2 {
|
||||
match done_rx.recv_timeout(GIT_PIPE_DRAIN_GRACE) {
|
||||
Ok(()) => drained += 1,
|
||||
Err(_) => {
|
||||
partial = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let stdout = stdout_buf.lock().map(|buf| buf.clone()).unwrap_or_default();
|
||||
let mut stderr = stderr_buf.lock().map(|buf| buf.clone()).unwrap_or_default();
|
||||
if partial {
|
||||
if !stderr.is_empty() && stderr.last() != Some(&b'\n') {
|
||||
stderr.push(b'\n');
|
||||
}
|
||||
stderr.extend_from_slice(
|
||||
b"[codewhale] git output pipes did not close after git exited \
|
||||
(kept open by a hook or subprocess?); captured output may be partial\n",
|
||||
);
|
||||
}
|
||||
Ok(Output {
|
||||
status,
|
||||
stdout,
|
||||
stderr,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read a child's pipe to end into a shared buffer, tolerating interruption.
|
||||
fn read_pipe_to_buffer(reader: &mut impl io::Read, buf: &std::sync::Mutex<Vec<u8>>) {
|
||||
let mut chunk = [0_u8; 8192];
|
||||
loop {
|
||||
match reader.read(&mut chunk) {
|
||||
Ok(0) => return,
|
||||
Ok(read) => {
|
||||
if let Ok(mut buf) = buf.lock() {
|
||||
buf.extend_from_slice(&chunk[..read]);
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
|
||||
Err(_) => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1555,10 +1695,13 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn version_probes_scrub_credentials_preloads_and_close_stdin() {
|
||||
let _home = crate::test_support::SealedHome::new();
|
||||
let _secret =
|
||||
crate::test_support::EnvVarGuard::set("DEEPSEEK_API_KEY", "synthetic-probe-secret");
|
||||
let _preload = crate::test_support::EnvVarGuard::set("NODE_OPTIONS", "synthetic-preload");
|
||||
let _home = codewhale_runtime::test_support::SealedHome::new();
|
||||
let _secret = codewhale_runtime::test_support::EnvVarGuard::set(
|
||||
"DEEPSEEK_API_KEY",
|
||||
"synthetic-probe-secret",
|
||||
);
|
||||
let _preload =
|
||||
codewhale_runtime::test_support::EnvVarGuard::set("NODE_OPTIONS", "synthetic-preload");
|
||||
let mut command = version_probe_command("/bin/sh");
|
||||
command.args(["-c", "[ -z \"${DEEPSEEK_API_KEY+x}\" ] && [ -z \"${NODE_OPTIONS+x}\" ] && ! read ignored && printf clean"]);
|
||||
let output = probe_output(&mut command, true, VERSION_PROBE_TIMEOUT).unwrap();
|
||||
@@ -1665,6 +1808,7 @@ mod tests {
|
||||
good.clone(),
|
||||
later,
|
||||
],
|
||||
"",
|
||||
);
|
||||
assert_eq!(resolution.selected, Some((good, (22, 20, 0))));
|
||||
assert_eq!(resolution.rejected.len(), 3);
|
||||
@@ -1689,7 +1833,7 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn host_runtime_auto_prefers_bun_and_explicit_choices_never_fall_back() {
|
||||
use crate::config::ExtensionHostRuntime as Choice;
|
||||
use codewhale_config::extension_host::ExtensionHostRuntime as Choice;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let script = |name: &str, body: &str| {
|
||||
@@ -1711,6 +1855,7 @@ mod tests {
|
||||
select_runtime(
|
||||
kind,
|
||||
candidates.iter().map(|path| (*path).clone()).collect(),
|
||||
"",
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1798,7 +1943,7 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn untrusted_search_candidates_are_skipped_and_a_failing_override_is_final() {
|
||||
use crate::config::ExtensionHostRuntime as Choice;
|
||||
use codewhale_config::extension_host::ExtensionHostRuntime as Choice;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let script = |name: &str, body: &str| {
|
||||
@@ -1860,7 +2005,8 @@ mod tests {
|
||||
|
||||
// A failing override never falls through to a search.
|
||||
let broken = script("broken-bun", "exit 3");
|
||||
let bun_only = resolve_extension_host_runtime(Choice::Bun, None, Some(broken.as_path()));
|
||||
let bun_only =
|
||||
resolve_extension_host_runtime(Choice::Bun, None, Some(broken.as_path()), "");
|
||||
assert!(bun_only.selected.is_none());
|
||||
let failure = bun_only.failure();
|
||||
assert!(
|
||||
@@ -1874,6 +2020,7 @@ mod tests {
|
||||
Choice::Auto,
|
||||
Some(node.as_path()),
|
||||
Some(broken.as_path()),
|
||||
"",
|
||||
);
|
||||
let summary = auto.summary();
|
||||
assert!(summary.starts_with("node 24.1.0 at "), "{summary}");
|
||||
@@ -2218,8 +2365,8 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn runtime_commands_do_not_inherit_parent_secret_env() {
|
||||
let _env_lock = crate::test_support::lock_test_env();
|
||||
let _secret = crate::test_support::EnvVarGuard::set(
|
||||
let _env_lock = codewhale_runtime::test_support::lock_test_env();
|
||||
let _secret = codewhale_runtime::test_support::EnvVarGuard::set(
|
||||
"CODEWHALE_TEST_RUNTIME_SECRET",
|
||||
"runtime-secret-value",
|
||||
);
|
||||
@@ -2255,9 +2402,11 @@ mod tests {
|
||||
if !Git::available() {
|
||||
return;
|
||||
}
|
||||
let _env_lock = crate::test_support::lock_test_env();
|
||||
let _secret =
|
||||
crate::test_support::EnvVarGuard::set("CODEWHALE_TEST_GIT_SECRET", "git-secret-value");
|
||||
let _env_lock = codewhale_runtime::test_support::lock_test_env();
|
||||
let _secret = codewhale_runtime::test_support::EnvVarGuard::set(
|
||||
"CODEWHALE_TEST_GIT_SECRET",
|
||||
"git-secret-value",
|
||||
);
|
||||
let repo = tempfile::tempdir().expect("repo");
|
||||
let hooks = tempfile::tempdir().expect("hooks");
|
||||
let marker = hooks.path().join("seen");
|
||||
@@ -2357,7 +2506,7 @@ mod tests {
|
||||
assert!(!repo.join("helper-seen").exists());
|
||||
return;
|
||||
}
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let _lock = codewhale_runtime::test_support::lock_test_env();
|
||||
let real_git = resolve_executable_path("git", "--version").expect("absolute native Git");
|
||||
let repo = tempfile::tempdir().unwrap();
|
||||
let wrapper_dir = tempfile::tempdir().unwrap();
|
||||
@@ -2432,7 +2581,7 @@ mod tests {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
const CHILD: &str = "CODEWHALE_TEST_GIT_REVIEW_PATH_CHANGE";
|
||||
if let Some(repo) = std::env::var_os(CHILD) {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let _lock = codewhale_runtime::test_support::lock_test_env();
|
||||
let repo = PathBuf::from(repo);
|
||||
let initial = Git::review_command(&repo)
|
||||
.unwrap()
|
||||
@@ -2442,7 +2591,7 @@ mod tests {
|
||||
assert!(initial.status.success());
|
||||
assert_eq!(initial.stdout, b"false\n");
|
||||
let changed = std::env::var_os("CODEWHALE_TEST_CHANGED_GIT_PATH").unwrap();
|
||||
let _path = crate::test_support::EnvVarGuard::set("PATH", changed);
|
||||
let _path = codewhale_runtime::test_support::EnvVarGuard::set("PATH", changed);
|
||||
let output = Git::review_command(&repo)
|
||||
.unwrap()
|
||||
.args(["status", "--porcelain"])
|
||||
@@ -2457,7 +2606,7 @@ mod tests {
|
||||
);
|
||||
return;
|
||||
}
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let _lock = codewhale_runtime::test_support::lock_test_env();
|
||||
let real_git = resolve_executable_path("git", "--version").expect("absolute native Git");
|
||||
let quoted_git = real_git.replace('\'', "'\\''");
|
||||
let repo = tempfile::tempdir().unwrap();
|
||||
@@ -15,5 +15,6 @@ anyhow.workspace = true
|
||||
codewhale-protocol = { path = "../protocol", version = "0.10.2" }
|
||||
regex = "1.11"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
shlex = "2.0.1"
|
||||
toml.workspace = true
|
||||
@@ -17,6 +17,14 @@ pub enum ApprovalMode {
|
||||
}
|
||||
|
||||
impl ApprovalMode {
|
||||
pub fn from_auto_approve(auto_approve: bool) -> Self {
|
||||
if auto_approve {
|
||||
Self::Bypass
|
||||
} else {
|
||||
Self::Suggest
|
||||
}
|
||||
}
|
||||
|
||||
/// Shift+Tab permission cycle order (#0.8.68 M2).
|
||||
pub const PERMISSION_CYCLE: [Self; 3] = [Self::Suggest, Self::Auto, Self::Bypass];
|
||||
|
||||
@@ -44,6 +52,14 @@ impl ApprovalMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn agent_baseline_sandbox_mode(self) -> &'static str {
|
||||
match self {
|
||||
Self::Bypass => "danger-full-access",
|
||||
Self::Auto | Self::Suggest | Self::Never => "workspace-write",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn cycle_permission_next(self) -> Self {
|
||||
let Some(index) = Self::PERMISSION_CYCLE.iter().position(|mode| *mode == self) else {
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
//! Semantic aliases for the model-facing action tools.
|
||||
//!
|
||||
//! `Bash`, `File`, `Git`, `Run`, `Web`, and `rlm` deliberately keep their
|
||||
//! canonical names at the execution and audit boundaries. Presentation and
|
||||
//! policy consumers, however, still understand the older per-action names.
|
||||
//! Resolve that semantic name in one place so live calls and saved legacy
|
||||
//! transcripts receive identical downstream behavior without rewriting the
|
||||
//! original call.
|
||||
//!
|
||||
//! This table is not documentation — it is the **action-policy seam**. A
|
||||
//! permission check that denies `fetch_url` only reaches `Web{action:"fetch"}`
|
||||
//! because the pair is listed here. A family that is missing from the table is
|
||||
//! a family whose actions no deny list can see, which is why `rlm` was added:
|
||||
//! `rlm{action:"open", url:...}` calls `FetchUrlTool` *inside the process*,
|
||||
//! under its own name, and a name-keyed deny list never sees that call.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
pub const CANONICAL_ACTION_ALIASES: &[(&str, &str, &str)] = &[
|
||||
("bash", "run", "exec_shell"),
|
||||
("bash", "wait", "exec_shell_wait"),
|
||||
("bash", "interact", "exec_shell_interact"),
|
||||
("bash", "cancel", "exec_shell_cancel"),
|
||||
("Bash", "run", "exec_shell"),
|
||||
("Bash", "wait", "exec_shell_wait"),
|
||||
("Bash", "interact", "exec_shell_interact"),
|
||||
("Bash", "cancel", "exec_shell_cancel"),
|
||||
("File", "read", "read_file"),
|
||||
("File", "list", "list_dir"),
|
||||
("File", "search_name", "file_search"),
|
||||
("File", "search_content", "grep_files"),
|
||||
("File", "write", "write_file"),
|
||||
("File", "edit", "edit_file"),
|
||||
("File", "patch", "apply_patch"),
|
||||
("Git", "status", "git_status"),
|
||||
("Git", "diff", "git_diff"),
|
||||
("Git", "log", "git_log"),
|
||||
("Git", "show", "git_show"),
|
||||
("Git", "blame", "git_blame"),
|
||||
("Git", "commit_plan", "git_commit_plan"),
|
||||
("Git", "fetch", "git_fetch"),
|
||||
("Git", "merge_tree", "git_merge_tree"),
|
||||
("Run", "tests", "run_tests"),
|
||||
("Run", "verifiers", "run_verifiers"),
|
||||
("Web", "search", "web_search"),
|
||||
("Web", "fetch", "fetch_url"),
|
||||
("Web", "wait", "wait_for_dev_server"),
|
||||
// The RLM session family. `open` reaches the network (it fetches a `url`
|
||||
// through `FetchUrlTool` in-process) and `eval` runs operator-supplied
|
||||
// Python against a live kernel — sockets and filesystem both. The other
|
||||
// three actions are bounded local metadata. Listing every pair is what lets
|
||||
// a deny list keep the local half and remove the reaching half, instead of
|
||||
// having to choose between the whole family and nothing.
|
||||
("rlm", "session_objects", "rlm_session_objects"),
|
||||
("rlm", "open", "rlm_open"),
|
||||
("rlm", "eval", "rlm_eval"),
|
||||
("rlm", "configure", "rlm_configure"),
|
||||
("rlm", "close", "rlm_close"),
|
||||
// The durable-work families. These were absent for the same reason `rlm`
|
||||
// was: they are model-visible under one canonical name (`tasks`,
|
||||
// `automation`, `github`) and their per-action legacy names are registered
|
||||
// as *hidden* aliases. A deny list naming `task_gate_run` therefore never
|
||||
// saw `tasks{action:"gate_run"}`, and the action-enum pruner never saw the
|
||||
// family at all — so an operator ceiling could not express "durable task
|
||||
// bookkeeping, yes; running a gate command, no".
|
||||
//
|
||||
// `gate_run` runs an operator-supplied command, `automation.run` executes a
|
||||
// stored automation, and the mutating `automation.*` actions schedule agent
|
||||
// runs with their own cwd. All three are execution primitives spelled as
|
||||
// bookkeeping, which is exactly the shape
|
||||
// `execution_envelope` classifies from capabilities.
|
||||
("tasks", "create", "task_create"),
|
||||
("tasks", "list", "task_list"),
|
||||
("tasks", "read", "task_read"),
|
||||
("tasks", "cancel", "task_cancel"),
|
||||
("tasks", "gate_run", "task_gate_run"),
|
||||
("tasks", "pr_attempt_record", "pr_attempt_record"),
|
||||
("tasks", "pr_attempt_list", "pr_attempt_list"),
|
||||
("tasks", "pr_attempt_read", "pr_attempt_read"),
|
||||
("tasks", "pr_attempt_preflight", "pr_attempt_preflight"),
|
||||
("automation", "create", "automation_create"),
|
||||
("automation", "list", "automation_list"),
|
||||
("automation", "read", "automation_read"),
|
||||
("automation", "update", "automation_update"),
|
||||
("automation", "pause", "automation_pause"),
|
||||
("automation", "resume", "automation_resume"),
|
||||
("automation", "delete", "automation_delete"),
|
||||
("automation", "run", "automation_run"),
|
||||
("github", "issue_context", "github_issue_context"),
|
||||
("github", "pr_context", "github_pr_context"),
|
||||
("github", "comment", "github_comment"),
|
||||
("github", "close_issue", "github_close_issue"),
|
||||
("github", "close_pr", "github_close_pr"),
|
||||
("github", "report_draft", "github_report_draft"),
|
||||
("github", "report_read", "github_report_read"),
|
||||
];
|
||||
|
||||
/// The conservative action label policy uses when the model omits `action`.
|
||||
///
|
||||
/// This is a *policy* fallback only. Execution rejects an actionless call in
|
||||
/// every family (see `required_action`); approval and parallel-safety
|
||||
/// predicates cannot return an error, so they still need a label, and it must
|
||||
/// be the family's least dangerous action.
|
||||
///
|
||||
/// `None` means the family never had even a policy default — `rlm`'s contract:
|
||||
/// `RlmTool::resolve_action` errors rather than guessing.
|
||||
/// Policy still resolves an *explicit* action for such a family — see
|
||||
/// [`canonical_action_alias`].
|
||||
#[must_use]
|
||||
pub fn action_family_default(tool_name: &str) -> Option<Option<&'static str>> {
|
||||
match tool_name {
|
||||
"bash" | "Bash" => Some(Some("run")),
|
||||
"File" => Some(Some("read")),
|
||||
"Git" => Some(Some("status")),
|
||||
"Run" => Some(Some("tests")),
|
||||
"Web" => Some(Some("search")),
|
||||
// Families whose wrappers reject an actionless call rather than
|
||||
// guessing. Policy still resolves an *explicit* action for them.
|
||||
"rlm" | "tasks" | "automation" | "github" => Some(None),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `tool_name` is a model-facing action family whose `action` enum
|
||||
/// policy may prune.
|
||||
///
|
||||
/// Derived from [`action_family_default`] rather than spelled out at each call
|
||||
/// site: a hard-coded family list that falls behind the alias table is a family
|
||||
/// whose actions stay visible after policy removed them.
|
||||
#[must_use]
|
||||
pub fn is_action_family(tool_name: &str) -> bool {
|
||||
action_family_default(tool_name).is_some()
|
||||
}
|
||||
|
||||
/// Resolve a canonical action tool to the legacy name for that exact action.
|
||||
///
|
||||
/// A missing action falls back to the family's conservative default so the
|
||||
/// *policy* label is never absent; execution itself refuses the call (see
|
||||
/// `required_action`). Unknown actions stay canonical so policy remains
|
||||
/// conservative and the eventual tool error is attributed to the call the
|
||||
/// model actually made.
|
||||
///
|
||||
/// A family with no default (`rlm`) still resolves an **explicit** action. The
|
||||
/// earlier shape returned the family name for any such call, which meant
|
||||
/// `rlm{action:"eval"}` never resolved to `rlm_eval` and therefore never met a
|
||||
/// deny list entry naming it.
|
||||
#[must_use]
|
||||
pub fn canonical_action_alias<'a>(tool_name: &'a str, input: &Value) -> &'a str {
|
||||
// The new model-facing file primitives deliberately reuse the old
|
||||
// semantic policy names. This keeps permissions.toml, repo law, resource
|
||||
// envelopes, approval caches, audit aggregation, and saved policy state
|
||||
// compatible across the presentation change.
|
||||
match tool_name {
|
||||
"read" => return "read_file",
|
||||
"write" => return "write_file",
|
||||
"edit" => return "edit_file",
|
||||
_ => {}
|
||||
}
|
||||
let Some(default_action) = action_family_default(tool_name) else {
|
||||
return tool_name;
|
||||
};
|
||||
let Some(action) = input
|
||||
.get("action")
|
||||
.and_then(Value::as_str)
|
||||
.or(default_action)
|
||||
else {
|
||||
return tool_name;
|
||||
};
|
||||
|
||||
CANONICAL_ACTION_ALIASES
|
||||
.iter()
|
||||
.find_map(|(family, candidate_action, alias)| {
|
||||
(*family == tool_name && *candidate_action == action).then_some(*alias)
|
||||
})
|
||||
.unwrap_or(tool_name)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod approval_mode;
|
||||
pub mod bash_arity;
|
||||
pub mod canonical_action;
|
||||
pub mod command_safety;
|
||||
pub mod matcher;
|
||||
pub mod shell_expand;
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "només raonament {attempt}/{limit}; sense resposta ni crida d’eina; {action}",
|
||||
"OutputLimitAnswerRetryBody": "límit de sortida {attempt}/{limit}; sense resposta ni crida d’eina; {action}",
|
||||
"AnswerRetryActionBare": "tornant a demanar la resposta",
|
||||
"AnswerRetryActionNudge": "tornant a demanar la resposta amb un recordatori",
|
||||
"AnswerRetryCounter": "només raonament/límit de sortida",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Recomanat",
|
||||
"OrcaRouterOfferAction": "Mostra l’oferta d’OrcaRouter a /provider o /model",
|
||||
"OrcaRouterOfferBody": "Aconsegueix un 30% de crèdit addicional per a DeepSeek V4.1 Flash en les recàrregues amb l’oferta Codewhale × OrcaRouter. Consulta les condicions vigents a la pàgina de l’oferta.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale rep una comissió de recomanació d’OrcaRouter.",
|
||||
"ShellWaitNoActive": "No s'està esperant cap ordre de l'intèrpret.",
|
||||
"ShellWaitInteractive": "Les ordres interactives no poden continuar en segon pla. Cancel·la el torn o espera que acabin.",
|
||||
"ShellWaitUnavailable": "Aquesta eina no pot continuar en segon pla. Cancel·la el torn o espera que acabi.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ desar per més tard · /provider connecta",
|
||||
"ComposerHintSendNow": "↵ enviar ara",
|
||||
"ComposerHintSendIntoTurn": "↵ enviar a aquest torn",
|
||||
"ComposerHintLongDraft": "{shortcut}: editor extern",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ S'està enviant a aquest torn: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ No s'ha pogut enviar a aquest torn: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ S'està editant el seguiment: ",
|
||||
@@ -464,6 +474,9 @@
|
||||
"CmdImportClaudeDescription": "Planifica una importació revisable de la configuració i els servidors MCP de Claude Code",
|
||||
"CmdClearDescription": "Esborra l'historial de la conversació",
|
||||
"CmdColorDescription": "Defineix el color del quadre de redacció d'aquest xat",
|
||||
"CmdColorSet": "Color del quadre de redacció d'aquest xat: {color}",
|
||||
"CmdColorReset": "El color del quadre de redacció d'aquest xat s'ha restablert al valor per defecte del tema",
|
||||
"CmdColorUnknown": "Color desconegut. Tria'n un: {colors} o off.",
|
||||
"CmdCompactDescription": "Dispara la compactació del context per alliberar espai",
|
||||
"CmdPurgeDescription": "Permet que l'agent podi quirúrgicament l'historial de la conversació per alliberar espai de context",
|
||||
"CmdConfigDescription": "Inspecciona i canvia la configuració (claus en brut com a detall avançat)",
|
||||
@@ -795,7 +808,7 @@
|
||||
"KbSendDraft": "Envia quan estigui lliure; encua mentre estigui ocupat; amb el redactor buit envia ara el següent seguiment en cua",
|
||||
"KbSteerCurrentTurn": "Dirigeix el torn actual (envia normalment quan està lliure)",
|
||||
"KbCloseMenu": "Tanca el menú, cancel·la la petició, descarta l'esborrany o neteja l'entrada",
|
||||
"KbCancelOrExit": "Cancel·la la petició, o surt quan està lliure",
|
||||
"KbCancelOrExit": "Cancel·la, esborra l’esborrany o surt quan està lliure",
|
||||
"KbShellControls": "Mou l'espera del shell en execució a /jobs",
|
||||
"KbExitEmpty": "Surt quan l'entrada és buida",
|
||||
"KbCommandPalette": "Obre la paleta de comandes",
|
||||
@@ -1257,6 +1270,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ revisa · {details} detalls · Esc atura",
|
||||
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g ves a l'agent",
|
||||
"UserInputActionRequired": "Cal una acció",
|
||||
"UserInputQuestionProgress": "Pregunta {current} de {total}",
|
||||
"UserInputOtherLabel": "Altres",
|
||||
"UserInputOtherDescription": "Escriu una resposta personalitzada",
|
||||
"UserInputConfirmLabel": "Confirma la selecció",
|
||||
"UserInputConfirmDescription": "Envia la selecció ({count})",
|
||||
"UserInputCustomPrompt": "Resposta personalitzada:",
|
||||
"UserInputCustomPlaceholder": "(escriu la teva resposta)",
|
||||
"UserInputHintSubmit": "envia",
|
||||
"UserInputHintBack": "tornar",
|
||||
"UserInputHintMove": "mou",
|
||||
"UserInputHintToggle": "commutar",
|
||||
"UserInputHintSelectConfirm": "tria/confirma",
|
||||
"UserInputHintCancel": "cancel·la",
|
||||
"UserInputHintQuickPick": "tria ràpida",
|
||||
"UserInputHintConfirm": "confirmar",
|
||||
"UserInputHintDigit": "dígit",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ revisa · {details} detalls · Esc amaga",
|
||||
"PendingApprovalInAgent": "Cal una aprovació a {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … truncat · prem {details} per a tots els detalls",
|
||||
@@ -1353,6 +1383,8 @@
|
||||
"ToolReceiptLinesSingular": "1 línia",
|
||||
"ToolReceiptLinesPlural": "{count} línies",
|
||||
"ToolDescriptionNote": "nota",
|
||||
"ToolStatusWaitingForApproval": "esperant aprovació",
|
||||
"ToolStatusQueued": "a la cua",
|
||||
"CmdConstitutionAccountApplies": "Constitució del perfil del compte: s’aplica (sessió iniciada; el motor la carrega a cada torn)",
|
||||
"CmdConstitutionAccountSignedOut": "Constitució del perfil del compte: no has iniciat sessió",
|
||||
"CmdConstitutionAccountCheckFailed": "Constitució del perfil del compte: ha fallat la comprovació ({error})",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Continua planificant",
|
||||
"PlanHandoffKeepPlanningDetail": "Segueix a Plan. No es canvia res.",
|
||||
"PlanHandoffProceed": "Endavant amb el pla.",
|
||||
"PlanHandoffTodoNote": "Fes el seguiment d'aquest pla amb todo_write: mantén l'element \"{title}\" a la llista i marca'l com a completat quan el pla estigui acabat.",
|
||||
"PetAppearanceLegacy": "La mascota funciona amb una versió anterior. Reinicia-la després d’actualitzar Codewhale per triar l’estil de balena i la llengua del text.",
|
||||
"PluginOAuthBrowser": "Prem Retorn per iniciar sessió al navegador o Esc per tornar. No cal cap clau API.",
|
||||
"PluginOAuthReady": "{provider}: sessió iniciada. Tria un model.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Política de sandbox d’aquesta execució; qualsevol --sandbox diferent de danger-full-access (o un sandbox_mode equivalent a la configuració) desactiva el mode de confiança.",
|
||||
"ExecAfterHelp": "Exemples:\n codewhale exec \"explica aquesta funció\"\n codewhale exec --auto \"llista crates/ amb ls\"\n codewhale exec --auto --output-format stream-json \"corregeix la prova que falla\"\n\n`codewhale exec` sense opcions produeix una única resposta del model: un torn\ndel motor Engine amb el mateix prompt de sistema que les altres execucions i\nsense eines. Fes servir `--auto` per executar un agent amb eines sense interacció.\nNomés s’ofereixen eines amb `--auto`, `--yolo`, `--allowed-tools` o en reprendre\nuna sessió; límits com `--max-turns`, `--disallowed-tools` o `--sandbox`, i el\nformat de sortida, no afegeixen mai eines. Una resposta tallada pel límit de\nsortida del proveïdor continua en el mateix torn, amb un màxim de 8 passos\ndel model, llevat que `--max-turns` indiqui un altre valor.\n--auto fa servir Full Access per defecte: sense sandbox del shell i amb el mode de confiança activat. Fes servir --sandbox workspace-write per mantenir el sandbox del shell i les restriccions de les eines de fitxers a l’espai de treball; també s’apliquen les polítiques més restrictives configurades. Tornar a provar una operació denegada pel sandbox requereix --allow-sandbox-elevation o --sandbox danger-full-access.\nLes execucions que ofereixen eines, les execucions stream-json i les sessions represes es desen per a codewhale sessions, exec --continue i codewhale receipts <id>. Les execucions text/--json que només responen no es desen, llevat que la compactació necessiti dades de recuperació.\nQualsevol --sandbox diferent de danger-full-access (o un sandbox_mode equivalent a la configuració) desactiva el mode de confiança.\n",
|
||||
"ExecPromptHelp": "Indicació que s’enviarà al model. Posa -- abans d’una indicació que comenci amb - o contingui una opció literal d’exec; fes servir --prompt-file - per llegir l’entrada estàndard.",
|
||||
"ExecFlagAfterPrompt": "{flag} apareix després de la indicació i es tractaria com a text. Posa les opcions abans de la indicació o, per usar-les com a text literal, després de --."
|
||||
"ExecFlagAfterPrompt": "{flag} apareix després de la indicació i es tractaria com a text. Posa les opcions abans de la indicació o, per usar-les com a text literal, després de --.",
|
||||
"InstructionNoticeHeader": "S'han retallat o ometut instruccions permanents abans de començar aquesta sessió:",
|
||||
"InstructionNoticeFooter": "Executeu /context report per veure el desglossament complet per font.",
|
||||
"InstructionNoticeMore": "…i {count} més; la llista completa és a /context report.",
|
||||
"InstructionNoticeLine": "instruccions: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " s'han conservat {kept_bytes} de {total_bytes} bytes i ~{kept_tokens} de ~{total_tokens} tokens estimats",
|
||||
"InstructionNoticeDroppedBytes": " s'han descartat {bytes} bytes",
|
||||
"InstructionNoticeFallback": "; alternativa: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "instruccions: el prefix estàtic encara supera en ~{tokens} tokens estimats el límit del prefix estàtic després de retallar les instruccions; es continua amb la política `trim` — augmenteu static_prefix_guard_percent o reduïu la superfície d'eines.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "instruccions: l'àmbit {scope} s'ha retallat ~{tokens} tokens estimats per mantenir el prefix estàtic sota el límit",
|
||||
"InstructionDetailDroppedBroadest": "descartat com a segment de l'àmbit més ampli",
|
||||
"InstructionDetailNoRoom": "el pressupost d'àmbit de {tokens} tokens estimats no ha deixat espai per al contingut",
|
||||
"InstructionDetailRulesExhausted": "les instruccions han esgotat el pressupost d'àmbit del projecte de {tokens} tokens estimats",
|
||||
"InstructionDetailOverCeiling": "supera el límit de {bytes} bytes",
|
||||
"InstructionDetailUnreadable": "no s'ha pogut llegir: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "ja carregat des d'un àmbit més ampli",
|
||||
"InstructionDetailRulesCap": "supera el límit de {count} fitxers de regles per directori",
|
||||
"InstructionDetailSymlinkedRules": "es rebutja el directori de regles perquè és un enllaç simbòlic",
|
||||
"InstructionBudgetRefused": "el pressupost d'instruccions ha rebutjat aquesta sol·licitud: {reason}",
|
||||
"InstructionBudgetOverGuard": "el prefix estàtic de ~{measured} tokens estimats supera el límit {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "nur Überlegungen {attempt}/{limit}; keine Antwort oder Werkzeuganfrage; {action}",
|
||||
"OutputLimitAnswerRetryBody": "Ausgabelimit {attempt}/{limit}; keine Antwort oder Werkzeuganfrage; {action}",
|
||||
"AnswerRetryActionBare": "Antwort wird erneut angefordert",
|
||||
"AnswerRetryActionNudge": "Antwort wird mit einem Hinweis erneut angefordert",
|
||||
"AnswerRetryCounter": "Überlegungen/Ausgabelimit",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Empfohlen",
|
||||
"OrcaRouterOfferAction": "OrcaRouter-Angebot in /provider oder /model anzeigen",
|
||||
"OrcaRouterOfferBody": "Erhalte mit dem Angebot Codewhale × OrcaRouter bei Aufladungen 30% zusätzliches Guthaben für DeepSeek V4.1 Flash. Die aktuellen Bedingungen stehen auf der Angebotsseite.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale erhält von OrcaRouter eine Vermittlungsprovision.",
|
||||
"ShellWaitNoActive": "Aktuell wird auf keinen Shell-Befehl gewartet.",
|
||||
"ShellWaitInteractive": "Interaktive Befehle können nicht im Hintergrund weiterlaufen. Brich den aktuellen Durchgang ab oder warte, bis der Befehl fertig ist.",
|
||||
"ShellWaitUnavailable": "Dieses Werkzeug kann nicht im Hintergrund weiterlaufen. Brich den aktuellen Durchgang ab oder warte, bis es fertig ist.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ für später speichern · /provider verbindet",
|
||||
"ComposerHintSendNow": "↵ jetzt senden",
|
||||
"ComposerHintSendIntoTurn": "↵ in diesen Zug senden",
|
||||
"ComposerHintLongDraft": "{shortcut}: externer Editor",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ Wird in diesen Zug gesendet: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ Konnte nicht in diesen Zug: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ Folgebeitrag wird bearbeitet: ",
|
||||
@@ -464,6 +474,9 @@
|
||||
"CmdImportClaudeDescription": "Einen prüfbaren Import der Claude-Code-Einstellungen und MCP-Server planen",
|
||||
"CmdClearDescription": "Gesprächsverlauf löschen",
|
||||
"CmdColorDescription": "Farbe des Eingabefelds für diesen Chat festlegen",
|
||||
"CmdColorSet": "Farbe des Eingabefelds für diesen Chat: {color}",
|
||||
"CmdColorReset": "Farbe des Eingabefelds für diesen Chat auf den Theme-Standard zurückgesetzt",
|
||||
"CmdColorUnknown": "Unbekannte Farbe. Wähle eine von: {colors} oder off.",
|
||||
"CmdCompactDescription": "Kontextkomprimierung auslösen, um Platz freizugeben",
|
||||
"CmdPurgeDescription": "Den Agenten den Gesprächsverlauf gezielt kürzen lassen, um Kontextplatz freizugeben",
|
||||
"CmdConfigDescription": "Einstellungen ansehen und ändern (Rohschlüssel als Detail für Fortgeschrittene)",
|
||||
@@ -795,7 +808,7 @@
|
||||
"KbSendDraft": "Bei Leerlauf senden; bei Arbeit einreihen; leerer Composer sendet die nächste Folgenachricht sofort",
|
||||
"KbSteerCurrentTurn": "Aktuellen Zug steuern (sendet normal bei Leerlauf)",
|
||||
"KbCloseMenu": "Menü schließen, Anfrage abbrechen, Entwurf verwerfen oder Eingabe leeren",
|
||||
"KbCancelOrExit": "Anfrage abbrechen oder bei Leerlauf beenden",
|
||||
"KbCancelOrExit": "Abbrechen, Entwurf leeren oder im Leerlauf beenden",
|
||||
"KbShellControls": "Laufendes Shell-Warten nach /jobs verschieben",
|
||||
"KbExitEmpty": "Beenden, wenn die Eingabe leer ist",
|
||||
"KbCommandPalette": "Befehlspalette öffnen",
|
||||
@@ -1257,6 +1270,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ prüfen · {details} Details · Esc stoppen",
|
||||
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g zum Agenten",
|
||||
"UserInputActionRequired": "Aktion erforderlich",
|
||||
"UserInputQuestionProgress": "Frage {current} von {total}",
|
||||
"UserInputOtherLabel": "Sonstiges",
|
||||
"UserInputOtherDescription": "Eigene Antwort eingeben",
|
||||
"UserInputConfirmLabel": "Auswahl bestätigen",
|
||||
"UserInputConfirmDescription": "Auswahl senden ({count})",
|
||||
"UserInputCustomPrompt": "Eigene Antwort:",
|
||||
"UserInputCustomPlaceholder": "(Antwort eingeben)",
|
||||
"UserInputHintSubmit": "senden",
|
||||
"UserInputHintBack": "zurück",
|
||||
"UserInputHintMove": "bewegen",
|
||||
"UserInputHintToggle": "an/aus",
|
||||
"UserInputHintSelectConfirm": "wählen/senden",
|
||||
"UserInputHintCancel": "abbrechen",
|
||||
"UserInputHintQuickPick": "Schnellwahl",
|
||||
"UserInputHintConfirm": "bestätigen",
|
||||
"UserInputHintDigit": "Ziffer",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ prüfen · {details} Details · Esc ausblenden",
|
||||
"PendingApprovalInAgent": "Freigabe nötig in {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … gekürzt · {details} für volle Details",
|
||||
@@ -1353,6 +1383,8 @@
|
||||
"ToolReceiptLinesSingular": "1 Zeile",
|
||||
"ToolReceiptLinesPlural": "{count} Zeilen",
|
||||
"ToolDescriptionNote": "Hinweis",
|
||||
"ToolStatusWaitingForApproval": "wartet auf Freigabe",
|
||||
"ToolStatusQueued": "in Warteschlange",
|
||||
"CmdConstitutionAccountApplies": "Kontoprofil-Verfassung: gilt (angemeldet; die Engine lädt sie bei jedem Schritt)",
|
||||
"CmdConstitutionAccountSignedOut": "Kontoprofil-Verfassung: nicht angemeldet",
|
||||
"CmdConstitutionAccountCheckFailed": "Kontoprofil-Verfassung: Kontoprüfung fehlgeschlagen ({error})",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Weiter planen",
|
||||
"PlanHandoffKeepPlanningDetail": "In Plan bleiben. Nichts wird geändert.",
|
||||
"PlanHandoffProceed": "Setze den Plan jetzt um.",
|
||||
"PlanHandoffTodoNote": "Verfolge diesen Plan mit todo_write: Behalte den Eintrag \"{title}\" in deiner Liste und markiere ihn als erledigt, sobald der Plan abgeschlossen ist.",
|
||||
"PetAppearanceLegacy": "Das Haustier verwendet eine ältere Version. Starte es nach dem Codewhale-Update neu, um den Walstil und die Textsprache auszuwählen.",
|
||||
"PluginOAuthBrowser": "Mit Eingabe im Browser anmelden, mit Esc zurück. Kein API-Schlüssel erforderlich.",
|
||||
"PluginOAuthReady": "{provider}: angemeldet. Wählen Sie ein Modell.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Sandbox-Richtlinie für diesen Lauf; jedes --sandbox außer danger-full-access (oder ein entsprechender sandbox_mode in der Konfiguration) deaktiviert den Vertrauensmodus.",
|
||||
"ExecAfterHelp": "Beispiele:\n codewhale exec \"erkläre diese Funktion\"\n codewhale exec --auto \"liste crates/ mit ls auf\"\n codewhale exec --auto --output-format stream-json \"behebe den fehlschlagenden Test\"\n\nEin einfaches `codewhale exec` liefert eine einmalige Modellantwort: einen\nEngine-Turn mit demselben System-Prompt wie andere Läufe und ohne Tools.\nMit `--auto` wird ein Agent mit Tools ohne Interaktion ausgeführt. Tools\nwerden nur mit `--auto`, `--yolo`, `--allowed-tools` oder beim Fortsetzen einer\nSitzung angeboten; Grenzen wie `--max-turns`, `--disallowed-tools` oder\n`--sandbox` sowie das Ausgabeformat fügen niemals Tools hinzu. Eine durch das\nAusgabelimit des Anbieters abgeschnittene Antwort wird im selben Turn\nfortgesetzt, mit höchstens 8 Modellschritten, sofern `--max-turns` nichts\nanderes vorgibt.\n--auto verwendet standardmäßig Full Access: keine Shell-Sandbox und aktiver Vertrauensmodus. Mit --sandbox workspace-write bleiben die Shell-Sandbox und die Arbeitsbereichsbeschränkungen der Datei-Tools erhalten; strengere konfigurierte Richtlinien gelten ebenfalls. Ein erneuter Versuch nach einer Sandbox-Verweigerung erfordert --allow-sandbox-elevation oder --sandbox danger-full-access.\nLäufe mit angebotenen Tools, stream-json-Läufe und fortgesetzte Sitzungen werden für codewhale sessions, exec --continue und codewhale receipts <id> gespeichert. Reine Antwortläufe mit text/--json werden nicht gespeichert, es sei denn, die Komprimierung benötigt Wiederherstellungsdaten.\nJedes --sandbox außer danger-full-access (oder ein entsprechender sandbox_mode in der Konfiguration) deaktiviert den Vertrauensmodus.\n",
|
||||
"ExecPromptHelp": "Prompt für das Modell. Setze -- vor einen Prompt, der mit - beginnt oder eine exec-Option als wörtlichen Text enthält; --prompt-file - liest die Standardeingabe.",
|
||||
"ExecFlagAfterPrompt": "{flag} steht nach dem Prompt und würde als Text behandelt. Setze Optionen vor den Prompt oder wörtlichen Optionstext nach --."
|
||||
"ExecFlagAfterPrompt": "{flag} steht nach dem Prompt und würde als Text behandelt. Setze Optionen vor den Prompt oder wörtlichen Optionstext nach --.",
|
||||
"InstructionNoticeHeader": "Dauerhafte Anweisungen wurden vor Sitzungsbeginn gekürzt oder übersprungen:",
|
||||
"InstructionNoticeFooter": "Führe /context report aus, um die vollständige Aufschlüsselung pro Quelle zu sehen.",
|
||||
"InstructionNoticeMore": "…und {count} weitere; die vollständige Liste steht in /context report.",
|
||||
"InstructionNoticeLine": "Anweisungen: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " {kept_bytes} von {total_bytes} Bytes und ~{kept_tokens} von ~{total_tokens} geschätzten Tokens behalten",
|
||||
"InstructionNoticeDroppedBytes": " {bytes} Bytes verworfen",
|
||||
"InstructionNoticeFallback": "; Ersatz: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "Anweisungen: Das statische Präfix liegt nach dem Kürzen der Anweisungen noch ~{tokens} geschätzte Tokens über der Grenze für das statische Präfix; es geht mit der Richtlinie `trim` weiter — erhöhe static_prefix_guard_percent oder verkleinere die Werkzeugoberfläche.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "Anweisungen: Bereich {scope} um ~{tokens} geschätzte Tokens gekürzt, damit das statische Präfix unter der Grenze bleibt",
|
||||
"InstructionDetailDroppedBroadest": "als Segment des breitesten Bereichs verworfen",
|
||||
"InstructionDetailNoRoom": "das Bereichsbudget von {tokens} geschätzten Tokens ließ keinen Platz für Inhalt",
|
||||
"InstructionDetailRulesExhausted": "das Projektbudget von {tokens} geschätzten Tokens ist durch Anweisungen aufgebraucht",
|
||||
"InstructionDetailOverCeiling": "größer als die Obergrenze von {bytes} Bytes",
|
||||
"InstructionDetailUnreadable": "konnte nicht gelesen werden: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "bereits aus einem breiteren Bereich geladen",
|
||||
"InstructionDetailRulesCap": "über der Grenze von {count} Regeldateien pro Verzeichnis",
|
||||
"InstructionDetailSymlinkedRules": "Regelverzeichnis als Symlink abgelehnt",
|
||||
"InstructionBudgetRefused": "Das Anweisungsbudget hat diese Anfrage abgelehnt: {reason}",
|
||||
"InstructionBudgetOverGuard": "statisches Präfix von ~{measured} geschätzten Tokens überschreitet die Grenze {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "reasoning-only {attempt}/{limit}; no answer or tool call; {action}",
|
||||
"OutputLimitAnswerRetryBody": "output-limit {attempt}/{limit}; no answer or tool call; {action}",
|
||||
"AnswerRetryActionBare": "re-requesting the answer",
|
||||
"AnswerRetryActionNudge": "re-requesting the answer with a nudge",
|
||||
"AnswerRetryCounter": "reasoning-only/output-limit",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Recommended",
|
||||
"OrcaRouterOfferAction": "View OrcaRouter offer in /provider or /model",
|
||||
"OrcaRouterOfferBody": "Get a 30% top-up match for DeepSeek V4.1 Flash with the Codewhale × OrcaRouter offer. Check the offer page for current terms.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale earns a referral fee from OrcaRouter.",
|
||||
"ShellWaitNoActive": "No shell wait is active.",
|
||||
"ShellWaitInteractive": "Interactive commands cannot continue in the background. Cancel the turn, or wait for completion.",
|
||||
"ShellWaitUnavailable": "This tool cannot continue in the background. Cancel the turn, or wait for completion.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ save for later · /provider to connect",
|
||||
"ComposerHintSendNow": "↵ send now",
|
||||
"ComposerHintSendIntoTurn": "↵ send into this turn",
|
||||
"ComposerHintLongDraft": "{shortcut}: external editor",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ Sending into this turn: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ Held for next turn: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ Editing follow-up: ",
|
||||
@@ -467,6 +477,9 @@
|
||||
"CmdImportClaudeDescription": "Plan a reviewable import of Claude Code settings and servers",
|
||||
"CmdClearDescription": "Clear conversation history",
|
||||
"CmdColorDescription": "Set this chat's composer box color",
|
||||
"CmdColorSet": "Composer color for this chat: {color}",
|
||||
"CmdColorReset": "Composer color for this chat reset to the theme default",
|
||||
"CmdColorUnknown": "Unknown color. Choose one of: {colors}, or off.",
|
||||
"CmdCompactDescription": "Shrink the context to free space",
|
||||
"CmdPurgeDescription": "Let the agent prune or offload history to free context",
|
||||
"CmdConfigDescription": "Inspect and change settings — raw keys under advanced detail",
|
||||
@@ -812,7 +825,7 @@
|
||||
"KbSendDraft": "Send when idle; queue when busy; empty input sends the next queued item",
|
||||
"KbSteerCurrentTurn": "Steer this turn (sends normally when idle)",
|
||||
"KbCloseMenu": "Close menus, cancel, discard the draft, or clear input",
|
||||
"KbCancelOrExit": "Cancel, or exit when idle",
|
||||
"KbCancelOrExit": "Cancel, clear draft, or exit when idle",
|
||||
"KbShellControls": "Move the running shell wait to /jobs",
|
||||
"KbExitEmpty": "Exit when input is empty",
|
||||
"KbCommandPalette": "Open the command palette",
|
||||
@@ -1280,6 +1293,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ review · {details} details · Esc stop",
|
||||
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g go to agent",
|
||||
"UserInputActionRequired": "Action required",
|
||||
"UserInputQuestionProgress": "Question {current} of {total}",
|
||||
"UserInputOtherLabel": "Other",
|
||||
"UserInputOtherDescription": "Type a custom response",
|
||||
"UserInputConfirmLabel": "Confirm selection",
|
||||
"UserInputConfirmDescription": "Submit {count} selected",
|
||||
"UserInputCustomPrompt": "Custom response:",
|
||||
"UserInputCustomPlaceholder": "(type your response)",
|
||||
"UserInputHintSubmit": "submit",
|
||||
"UserInputHintBack": "back",
|
||||
"UserInputHintMove": "move",
|
||||
"UserInputHintToggle": "toggle",
|
||||
"UserInputHintSelectConfirm": "select/confirm",
|
||||
"UserInputHintCancel": "cancel",
|
||||
"UserInputHintQuickPick": "quick pick",
|
||||
"UserInputHintConfirm": "confirm",
|
||||
"UserInputHintDigit": "digit",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ review · {details} details · Esc hide",
|
||||
"PendingApprovalInAgent": "Approval needed in {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … truncated · press {details} for full details",
|
||||
@@ -1376,6 +1406,8 @@
|
||||
"ToolReceiptLinesSingular": "1 line",
|
||||
"ToolReceiptLinesPlural": "{count} lines",
|
||||
"ToolDescriptionNote": "note",
|
||||
"ToolStatusWaitingForApproval": "waiting for approval",
|
||||
"ToolStatusQueued": "queued",
|
||||
"CmdConstitutionAccountApplies": "Account profile constitution: applies (signed in; loaded by the Engine each turn)",
|
||||
"CmdConstitutionAccountSignedOut": "Account profile constitution: not signed in",
|
||||
"CmdConstitutionAccountCheckFailed": "Account profile constitution: account check failed ({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "a project, profile, environment, managed config, or organization requirement controls this value",
|
||||
"ConfigHintManagedAllowShell": "a project, profile, environment, or managed config controls shell access",
|
||||
"ConfigHintAllowShell": "on exposes shell tools in Work mode; permission rules still apply",
|
||||
"ConfigHintComposerMultilineMode": "off: Enter sends, Shift+Enter adds a line; on: Enter adds a line, Shift+Enter sends",
|
||||
"ConfigHintComposerMultilineMode": "off: Enter sends, Alt+Enter / Shift+Enter adds a line; on: Enter adds a line, Alt+Enter / Shift+Enter sends",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off; full change stays in Alt/Option+V",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Keep planning",
|
||||
"PlanHandoffKeepPlanningDetail": "Stay in Plan. Nothing is changed.",
|
||||
"PlanHandoffProceed": "Go ahead with the plan.",
|
||||
"PlanHandoffTodoNote": "Track this plan with todo_write: keep the item \"{title}\" in your list and mark it completed when the plan is done.",
|
||||
"PetAppearanceLegacy": "The pet companion is running an older version. Restart it after updating Codewhale to use whale styles and text languages.",
|
||||
"PluginOAuthBrowser": "Press Enter to connect in your browser. Esc returns. No API key is needed.",
|
||||
"PluginOAuthReady": "{provider}: connected. Choose a model.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Sandbox policy for this run; any --sandbox other than danger-full-access (or an equivalent sandbox_mode in config) disables trust mode.",
|
||||
"ExecAfterHelp": "Examples:\n codewhale exec \"explain this function\"\n codewhale exec --auto \"list crates/ with ls\"\n codewhale exec --auto --output-format stream-json \"fix the failing test\"\n\nPlain `codewhale exec` is a one-shot model response: one Engine turn with the\nsame system prompt as every other run, and no tools. Use `--auto` for\nnon-interactive agent-with-tools execution. Tools are offered only with\n`--auto`, `--yolo`, `--allowed-tools`, or when resuming a session; limits such\nas `--max-turns`, `--disallowed-tools` or `--sandbox`, and the output format,\nnever add tools. A reply cut off at the provider's output limit is continued\nin the same turn, at most 8 model steps unless `--max-turns` says otherwise.\n--auto defaults to Full Access: no shell sandbox and trust mode on. Use --sandbox workspace-write to keep the shell sandbox and file-tool workspace restrictions; stricter configured policies also apply. A denied sandbox retry requires --allow-sandbox-elevation or --sandbox danger-full-access.\nRuns that offer tools, stream-json runs, and resumed sessions are saved for codewhale sessions, exec --continue, and codewhale receipts <id>. Plain answer-only text/--json runs are not saved unless compaction needs recovery.\nAny --sandbox other than danger-full-access (or an equivalent sandbox_mode in config) disables trust mode.\n",
|
||||
"ExecPromptHelp": "Prompt to send to the model. Put -- before a prompt starting with - or containing a literal exec flag; use --prompt-file - to read stdin.",
|
||||
"ExecFlagAfterPrompt": "{flag} appears after the prompt and would be treated as text. Put flags before the prompt, or put literal flag text after --."
|
||||
"ExecFlagAfterPrompt": "{flag} appears after the prompt and would be treated as text. Put flags before the prompt, or put literal flag text after --.",
|
||||
"InstructionNoticeHeader": "Standing instructions were trimmed or skipped before this session started:",
|
||||
"InstructionNoticeFooter": "Run /context report for the full per-source breakdown.",
|
||||
"InstructionNoticeMore": "…and {count} more; the full list is in /context report.",
|
||||
"InstructionNoticeLine": "instructions: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " kept {kept_bytes} of {total_bytes} bytes and ~{kept_tokens} of ~{total_tokens} estimated tokens",
|
||||
"InstructionNoticeDroppedBytes": " dropped {bytes} bytes",
|
||||
"InstructionNoticeFallback": "; fallback: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "instructions: the static prefix is still ~{tokens} estimated tokens over the static-prefix guard after instruction trimming; continuing under policy `trim` — raise static_prefix_guard_percent or narrow the tool surface.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "instructions: {scope} scope trimmed by ~{tokens} estimated tokens to keep the static prefix under the guard",
|
||||
"InstructionDetailDroppedBroadest": "dropped as a broadest-scope segment",
|
||||
"InstructionDetailNoRoom": "scope budget of {tokens} estimated tokens left no room for content",
|
||||
"InstructionDetailRulesExhausted": "project scope budget of {tokens} estimated tokens exhausted by instructions",
|
||||
"InstructionDetailOverCeiling": "larger than the {bytes} byte ceiling",
|
||||
"InstructionDetailUnreadable": "could not be read: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "already loaded from a wider scope",
|
||||
"InstructionDetailRulesCap": "beyond the per-directory rules cap of {count} files",
|
||||
"InstructionDetailSymlinkedRules": "refusing symlinked rules directory",
|
||||
"InstructionBudgetRefused": "instruction budget refused this request: {reason}",
|
||||
"InstructionBudgetOverGuard": "static prefix ~{measured} estimated tokens exceeds guard {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "solo razonamiento {attempt}/{limit}; sin respuesta ni llamada a una herramienta; {action}",
|
||||
"OutputLimitAnswerRetryBody": "límite de salida {attempt}/{limit}; sin respuesta ni llamada a una herramienta; {action}",
|
||||
"AnswerRetryActionBare": "solicitando otra vez la respuesta",
|
||||
"AnswerRetryActionNudge": "solicitando otra vez la respuesta con un recordatorio",
|
||||
"AnswerRetryCounter": "solo razonamiento/límite de salida",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Recomendado",
|
||||
"OrcaRouterOfferAction": "Ver oferta de OrcaRouter en /provider o /model",
|
||||
"OrcaRouterOfferBody": "Obtén un 30% de crédito extra para DeepSeek V4.1 Flash en tus recargas con la oferta Codewhale × OrcaRouter. Consulta las condiciones vigentes en la página de la oferta.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale recibe una comisión por recomendación de OrcaRouter.",
|
||||
"ShellWaitNoActive": "No hay ninguna espera de shell activa.",
|
||||
"ShellWaitInteractive": "Los comandos interactivos no pueden continuar en segundo plano. Cancela el turno o espera a que terminen.",
|
||||
"ShellWaitUnavailable": "Esta herramienta no puede continuar en segundo plano. Cancela el turno o espera a que termine.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ guardar para después · /provider conecta",
|
||||
"ComposerHintSendNow": "↵ enviar ahora",
|
||||
"ComposerHintSendIntoTurn": "↵ enviar a este turno",
|
||||
"ComposerHintLongDraft": "{shortcut}: editor externo",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ Enviando a este turno: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ No se pudo enviar a este turno: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ Editando el seguimiento: ",
|
||||
@@ -467,6 +477,9 @@
|
||||
"CmdImportClaudeDescription": "Planificar una importación revisable de la configuración y los servidores MCP de Claude Code",
|
||||
"CmdClearDescription": "Limpiar el historial de la conversación",
|
||||
"CmdColorDescription": "Definir el color del cuadro de mensaje de este chat",
|
||||
"CmdColorSet": "Color del cuadro de mensaje de este chat: {color}",
|
||||
"CmdColorReset": "Color del cuadro de mensaje de este chat restablecido al predeterminado del tema",
|
||||
"CmdColorUnknown": "Color desconocido. Elige uno de: {colors} u off.",
|
||||
"CmdCompactDescription": "Compactar el contexto para liberar espacio",
|
||||
"CmdPurgeDescription": "Permite al agente eliminar quirúrgicamente historial innecesario para liberar espacio de contexto",
|
||||
"CmdConfigDescription": "Inspeccionar y cambiar ajustes (claves crudas en detalle avanzado)",
|
||||
@@ -812,7 +825,7 @@
|
||||
"KbSendDraft": "Enviar si está inactivo; encolar si está ocupado; con el compositor vacío, enviar ahora el siguiente seguimiento en cola",
|
||||
"KbSteerCurrentTurn": "Guiar el turno actual (envía normalmente si está inactivo)",
|
||||
"KbCloseMenu": "Cerrar menú, cancelar solicitud, descartar borrador o limpiar entrada",
|
||||
"KbCancelOrExit": "Cancelar solicitud o salir cuando está inactivo",
|
||||
"KbCancelOrExit": "Cancelar, borrar el borrador o salir cuando está inactivo",
|
||||
"KbShellControls": "Mover la espera del shell a /jobs",
|
||||
"KbExitEmpty": "Salir cuando la entrada está vacía",
|
||||
"KbCommandPalette": "Abrir paleta de comandos",
|
||||
@@ -1278,6 +1291,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ revisar · {details} detalles · Esc detener",
|
||||
"ApprovalOwnerHeader": "Agente: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g ir al agente",
|
||||
"UserInputActionRequired": "Se requiere acción",
|
||||
"UserInputQuestionProgress": "Pregunta {current} de {total}",
|
||||
"UserInputOtherLabel": "Otro",
|
||||
"UserInputOtherDescription": "Escribe una respuesta personalizada",
|
||||
"UserInputConfirmLabel": "Confirmar selección",
|
||||
"UserInputConfirmDescription": "Enviar selección ({count})",
|
||||
"UserInputCustomPrompt": "Respuesta personalizada:",
|
||||
"UserInputCustomPlaceholder": "(escribe tu respuesta)",
|
||||
"UserInputHintSubmit": "enviar",
|
||||
"UserInputHintBack": "volver",
|
||||
"UserInputHintMove": "mover",
|
||||
"UserInputHintToggle": "alternar",
|
||||
"UserInputHintSelectConfirm": "elegir/confirmar",
|
||||
"UserInputHintCancel": "cancelar",
|
||||
"UserInputHintQuickPick": "selección rápida",
|
||||
"UserInputHintConfirm": "confirmar",
|
||||
"UserInputHintDigit": "núm.",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ revisar · {details} detalles · Esc ocultar",
|
||||
"PendingApprovalInAgent": "Se necesita aprobación en {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … truncado · presiona {details} para ver todos los detalles",
|
||||
@@ -1374,6 +1404,8 @@
|
||||
"ToolReceiptLinesSingular": "1 línea",
|
||||
"ToolReceiptLinesPlural": "{count} líneas",
|
||||
"ToolDescriptionNote": "nota",
|
||||
"ToolStatusWaitingForApproval": "esperando aprobación",
|
||||
"ToolStatusQueued": "en cola",
|
||||
"CmdConstitutionAccountApplies": "Constitución del perfil de cuenta: se aplica (sesión iniciada; el motor la carga en cada turno)",
|
||||
"CmdConstitutionAccountSignedOut": "Constitución del perfil de cuenta: sesión no iniciada",
|
||||
"CmdConstitutionAccountCheckFailed": "Constitución del perfil de cuenta: falló la comprobación ({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "un proyecto, perfil, entorno, config administrado o requisito de la organización controla este valor",
|
||||
"ConfigHintManagedAllowShell": "un proyecto, perfil, entorno o config administrado controla el acceso al shell",
|
||||
"ConfigHintAllowShell": "activado expone herramientas de shell en modo Agente; las reglas de permiso siguen aplicando",
|
||||
"ConfigHintComposerMultilineMode": "desactivado: Enter envía, Shift+Enter agrega línea; activado: Enter agrega línea, Shift+Enter envía",
|
||||
"ConfigHintComposerMultilineMode": "desactivado: Enter envía, Alt+Enter / Shift+Enter agrega línea; activado: Enter agrega línea, Alt+Enter / Shift+Enter envía",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off; el cambio exacto permanece en los detalles de Alt/Option+V",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Seguir planificando",
|
||||
"PlanHandoffKeepPlanningDetail": "Sigue en Plan. No se cambia nada.",
|
||||
"PlanHandoffProceed": "Adelante con el plan.",
|
||||
"PlanHandoffTodoNote": "Haz seguimiento de este plan con todo_write: mantén el elemento \"{title}\" en tu lista y márcalo como completado cuando el plan esté terminado.",
|
||||
"PetAppearanceLegacy": "La mascota usa una versión anterior. Reiníciala después de actualizar Codewhale para elegir el estilo de ballena y el idioma del texto.",
|
||||
"PluginOAuthBrowser": "Presiona Intro para iniciar sesión en el navegador o Esc para volver. No se necesita una clave API.",
|
||||
"PluginOAuthReady": "{provider}: sesión iniciada. Elige un modelo.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Política de sandbox para esta ejecución; cualquier --sandbox distinto de danger-full-access (o un sandbox_mode equivalente en la configuración) desactiva el modo de confianza.",
|
||||
"ExecAfterHelp": "Ejemplos:\n codewhale exec \"explica esta función\"\n codewhale exec --auto \"lista crates/ con ls\"\n codewhale exec --auto --output-format stream-json \"corrige la prueba que falla\"\n\n`codewhale exec` sin opciones genera una única respuesta del modelo: un turno\ndel Engine con el mismo prompt del sistema que las demás ejecuciones y sin\nherramientas. Usa `--auto` para ejecutar un agente con herramientas sin interacción.\nSolo se ofrecen herramientas con `--auto`, `--yolo`, `--allowed-tools` o al\nreanudar una sesión; límites como `--max-turns`, `--disallowed-tools` o\n`--sandbox`, y el formato de salida, nunca agregan herramientas. Una respuesta\ncortada por el límite de salida del proveedor continúa en el mismo turno,\ncon un máximo de 8 pasos del modelo salvo que `--max-turns` indique otro valor.\n--auto usa Full Access de forma predeterminada: sin sandbox del shell y con el modo de confianza activado. Usa --sandbox workspace-write para mantener el sandbox del shell y las restricciones de las herramientas de archivos al espacio de trabajo; también se aplican las políticas más restrictivas configuradas. Reintentar una operación denegada por el sandbox requiere --allow-sandbox-elevation o --sandbox danger-full-access.\nLas ejecuciones que ofrecen herramientas, las ejecuciones stream-json y las sesiones reanudadas se guardan para codewhale sessions, exec --continue y codewhale receipts <id>. Las ejecuciones text/--json que solo responden no se guardan, a menos que la compactación necesite datos de recuperación.\nCualquier --sandbox distinto de danger-full-access (o un sandbox_mode equivalente en la configuración) desactiva el modo de confianza.\n",
|
||||
"ExecPromptHelp": "Prompt que se enviará al modelo. Coloca -- antes de un prompt que empiece con - o contenga una opción literal de exec; usa --prompt-file - para leer la entrada estándar.",
|
||||
"ExecFlagAfterPrompt": "{flag} aparece después del prompt y se trataría como texto. Coloca las opciones antes del prompt o, para usarlas como texto literal, después de --."
|
||||
"ExecFlagAfterPrompt": "{flag} aparece después del prompt y se trataría como texto. Coloca las opciones antes del prompt o, para usarlas como texto literal, después de --.",
|
||||
"InstructionNoticeHeader": "Se recortaron u omitieron instrucciones permanentes antes de que empezara esta sesión:",
|
||||
"InstructionNoticeFooter": "Ejecuta /context report para ver el desglose completo por fuente.",
|
||||
"InstructionNoticeMore": "…y {count} más; la lista completa está en /context report.",
|
||||
"InstructionNoticeLine": "instrucciones: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " se conservaron {kept_bytes} de {total_bytes} bytes y ~{kept_tokens} de ~{total_tokens} tokens estimados",
|
||||
"InstructionNoticeDroppedBytes": " se descartaron {bytes} bytes",
|
||||
"InstructionNoticeFallback": "; alternativa: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "instrucciones: el prefijo estático sigue ~{tokens} tokens estimados por encima del límite del prefijo estático tras recortar las instrucciones; se continúa con la política `trim` — aumenta static_prefix_guard_percent o reduce la superficie de herramientas.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "instrucciones: el ámbito {scope} se recortó ~{tokens} tokens estimados para mantener el prefijo estático bajo el límite",
|
||||
"InstructionDetailDroppedBroadest": "descartado como segmento del ámbito más amplio",
|
||||
"InstructionDetailNoRoom": "el presupuesto del ámbito de {tokens} tokens estimados no dejó espacio para contenido",
|
||||
"InstructionDetailRulesExhausted": "las instrucciones agotaron el presupuesto del ámbito del proyecto de {tokens} tokens estimados",
|
||||
"InstructionDetailOverCeiling": "supera el límite de {bytes} bytes",
|
||||
"InstructionDetailUnreadable": "no se pudo leer: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "ya se cargó desde un ámbito más amplio",
|
||||
"InstructionDetailRulesCap": "excede el tope de {count} archivos de reglas por directorio",
|
||||
"InstructionDetailSymlinkedRules": "se rechaza el directorio de reglas por ser un enlace simbólico",
|
||||
"InstructionBudgetRefused": "el presupuesto de instrucciones rechazó esta solicitud: {reason}",
|
||||
"InstructionBudgetOverGuard": "el prefijo estático de ~{measured} tokens estimados supera el límite {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "réflexion seule {attempt}/{limit} ; aucune réponse ni appel d’outil ; {action}",
|
||||
"OutputLimitAnswerRetryBody": "limite de sortie {attempt}/{limit} ; aucune réponse ni appel d’outil ; {action}",
|
||||
"AnswerRetryActionBare": "nouvelle demande de réponse",
|
||||
"AnswerRetryActionNudge": "nouvelle demande de réponse avec un rappel",
|
||||
"AnswerRetryCounter": "réflexion seule/limite de sortie",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Recommandé",
|
||||
"OrcaRouterOfferAction": "Voir l’offre OrcaRouter dans /provider ou /model",
|
||||
"OrcaRouterOfferBody": "Obtenez 30% de crédit supplémentaire pour DeepSeek V4.1 Flash sur vos recharges avec l’offre Codewhale × OrcaRouter. Consultez les conditions en vigueur sur la page de l’offre.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale reçoit une commission de recommandation d’OrcaRouter.",
|
||||
"ShellWaitNoActive": "Aucune attente de commande shell en cours.",
|
||||
"ShellWaitInteractive": "Les commandes interactives ne peuvent pas continuer en arrière-plan. Annulez le tour ou attendez la fin de la commande.",
|
||||
"ShellWaitUnavailable": "Cet outil ne peut pas continuer en arrière-plan. Annulez le tour ou attendez la fin de son exécution.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ enregistrer pour plus tard · /provider connecte",
|
||||
"ComposerHintSendNow": "↵ envoyer maintenant",
|
||||
"ComposerHintSendIntoTurn": "↵ envoyer dans ce tour",
|
||||
"ComposerHintLongDraft": "{shortcut} : éditeur externe",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ Envoi dans ce tour : ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ Impossible d'envoyer dans ce tour : ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ Modification du suivi : ",
|
||||
@@ -464,6 +474,9 @@
|
||||
"CmdImportClaudeDescription": "Planifier une importation vérifiable des réglages et serveurs MCP de Claude Code",
|
||||
"CmdClearDescription": "Effacer l'historique de la conversation",
|
||||
"CmdColorDescription": "Définir la couleur de la zone de saisie de cette conversation",
|
||||
"CmdColorSet": "Couleur de la zone de saisie de cette conversation : {color}",
|
||||
"CmdColorReset": "Couleur de la zone de saisie de cette conversation rétablie à la valeur par défaut du thème",
|
||||
"CmdColorUnknown": "Couleur inconnue. Choisissez parmi : {colors} ou off.",
|
||||
"CmdCompactDescription": "Déclencher la compaction du contexte pour libérer de l'espace",
|
||||
"CmdPurgeDescription": "Laisser l'agent élaguer chirurgicalement l'historique pour libérer du contexte",
|
||||
"CmdConfigDescription": "Inspecter et modifier les paramètres (clés brutes en détail avancé)",
|
||||
@@ -795,7 +808,7 @@
|
||||
"KbSendDraft": "Envoyer si inactif ; mettre en file si occupé ; composer vide envoie le prochain suivi en file",
|
||||
"KbSteerCurrentTurn": "Orienter le tour en cours (envoie normalement si inactif)",
|
||||
"KbCloseMenu": "Fermer le menu, annuler la requête, abandonner le brouillon ou vider la saisie",
|
||||
"KbCancelOrExit": "Annuler la requête, ou quitter si inactif",
|
||||
"KbCancelOrExit": "Annuler, effacer le brouillon ou quitter si inactif",
|
||||
"KbShellControls": "Déplacer l'attente du shell en cours vers /jobs",
|
||||
"KbExitEmpty": "Quitter quand la saisie est vide",
|
||||
"KbCommandPalette": "Ouvrir la palette de commandes",
|
||||
@@ -1257,6 +1270,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ réviser · {details} détails · Esc arrêter",
|
||||
"ApprovalOwnerHeader": "Agent : {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g aller à l'agent",
|
||||
"UserInputActionRequired": "Action requise",
|
||||
"UserInputQuestionProgress": "Question {current} sur {total}",
|
||||
"UserInputOtherLabel": "Autre",
|
||||
"UserInputOtherDescription": "Saisir une réponse personnalisée",
|
||||
"UserInputConfirmLabel": "Confirmer la sélection",
|
||||
"UserInputConfirmDescription": "Envoyer la sélection ({count})",
|
||||
"UserInputCustomPrompt": "Réponse personnalisée :",
|
||||
"UserInputCustomPlaceholder": "(saisissez votre réponse)",
|
||||
"UserInputHintSubmit": "envoyer",
|
||||
"UserInputHintBack": "retour",
|
||||
"UserInputHintMove": "déplacer",
|
||||
"UserInputHintToggle": "basculer",
|
||||
"UserInputHintSelectConfirm": "choisir/valider",
|
||||
"UserInputHintCancel": "annuler",
|
||||
"UserInputHintQuickPick": "choix rapide",
|
||||
"UserInputHintConfirm": "confirmer",
|
||||
"UserInputHintDigit": "n°",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ réviser · {details} détails · Esc masquer",
|
||||
"PendingApprovalInAgent": "Approbation requise dans {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … tronqué · appuyez sur {details} pour tous les détails",
|
||||
@@ -1353,6 +1383,8 @@
|
||||
"ToolReceiptLinesSingular": "1 ligne",
|
||||
"ToolReceiptLinesPlural": "{count} lignes",
|
||||
"ToolDescriptionNote": "note",
|
||||
"ToolStatusWaitingForApproval": "en attente d'approbation",
|
||||
"ToolStatusQueued": "en file",
|
||||
"CmdConstitutionAccountApplies": "Constitution du profil du compte : appliquée (connecté ; chargée par le moteur à chaque tour)",
|
||||
"CmdConstitutionAccountSignedOut": "Constitution du profil du compte : non connecté",
|
||||
"CmdConstitutionAccountCheckFailed": "Constitution du profil du compte : vérification échouée ({error})",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Continuer à planifier",
|
||||
"PlanHandoffKeepPlanningDetail": "Rester en Plan. Rien n'est modifié.",
|
||||
"PlanHandoffProceed": "Allez-y, appliquez le plan.",
|
||||
"PlanHandoffTodoNote": "Suivez ce plan avec todo_write : gardez l'élément \"{title}\" dans votre liste et marquez-le comme terminé quand le plan est achevé.",
|
||||
"PetAppearanceLegacy": "Le compagnon utilise une ancienne version. Redémarrez-le après la mise à jour de Codewhale pour choisir le style de baleine et la langue du texte.",
|
||||
"PluginOAuthBrowser": "Appuyez sur Entrée pour vous connecter dans le navigateur, ou Échap pour revenir. Aucune clé API nécessaire.",
|
||||
"PluginOAuthReady": "{provider} : connexion établie. Choisissez un modèle.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Politique de sandbox pour cette exécution ; tout --sandbox autre que danger-full-access (ou un sandbox_mode équivalent dans la configuration) désactive le mode de confiance.",
|
||||
"ExecAfterHelp": "Exemples :\n codewhale exec \"explique cette fonction\"\n codewhale exec --auto \"liste crates/ avec ls\"\n codewhale exec --auto --output-format stream-json \"corrige le test en échec\"\n\n`codewhale exec` seul produit une réponse unique du modèle : un tour Engine\navec la même instruction système que les autres exécutions, sans outils.\nUtilisez `--auto` pour exécuter un agent avec outils sans interaction.\nLes outils sont proposés uniquement avec `--auto`, `--yolo`, `--allowed-tools`\nou lors de la reprise d’une session ; les limites comme `--max-turns`,\n`--disallowed-tools` ou `--sandbox`, ainsi que le format de sortie, n’ajoutent\njamais d’outils. Une réponse interrompue par la limite de sortie du fournisseur\nse poursuit dans le même tour, avec au plus 8 étapes du modèle, sauf indication\ncontraire de `--max-turns`.\n--auto utilise Full Access par défaut : aucun bac à sable du shell et mode de confiance activé. Utilisez --sandbox workspace-write pour conserver le bac à sable du shell et les restrictions des outils de fichiers à l’espace de travail ; les politiques configurées plus strictes s’appliquent aussi. Réessayer une opération refusée par le bac à sable nécessite --allow-sandbox-elevation ou --sandbox danger-full-access.\nLes exécutions proposant des outils, les exécutions stream-json et les sessions reprises sont enregistrées pour codewhale sessions, exec --continue et codewhale receipts <id>. Les exécutions text/--json qui ne font que répondre ne sont pas enregistrées, sauf si la compaction nécessite des données de récupération.\nTout --sandbox autre que danger-full-access (ou un sandbox_mode équivalent dans la configuration) désactive le mode de confiance.\n",
|
||||
"ExecPromptHelp": "Prompt à envoyer au modèle. Placez -- avant un prompt qui commence par - ou contient une option exec comme texte littéral ; utilisez --prompt-file - pour lire l’entrée standard.",
|
||||
"ExecFlagAfterPrompt": "{flag} apparaît après le prompt et serait traité comme du texte. Placez les options avant le prompt, ou le texte littéral d’une option après --."
|
||||
"ExecFlagAfterPrompt": "{flag} apparaît après le prompt et serait traité comme du texte. Placez les options avant le prompt, ou le texte littéral d’une option après --.",
|
||||
"InstructionNoticeHeader": "Des instructions permanentes ont été tronquées ou ignorées avant le début de cette session :",
|
||||
"InstructionNoticeFooter": "Exécutez /context report pour le détail complet par source.",
|
||||
"InstructionNoticeMore": "…et {count} de plus ; la liste complète figure dans /context report.",
|
||||
"InstructionNoticeLine": "instructions : {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " {kept_bytes} sur {total_bytes} octets et ~{kept_tokens} sur ~{total_tokens} tokens estimés conservés",
|
||||
"InstructionNoticeDroppedBytes": " {bytes} octets supprimés",
|
||||
"InstructionNoticeFallback": " ; repli : {source}",
|
||||
"InstructionNoticeStaticGuardOver": "instructions : le préfixe statique dépasse encore de ~{tokens} tokens estimés la limite du préfixe statique après réduction des instructions ; poursuite sous la politique `trim` — augmentez static_prefix_guard_percent ou réduisez la surface d'outils.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "instructions : portée {scope} réduite de ~{tokens} tokens estimés pour garder le préfixe statique sous la limite",
|
||||
"InstructionDetailDroppedBroadest": "supprimé comme segment de la portée la plus large",
|
||||
"InstructionDetailNoRoom": "le budget de portée de {tokens} tokens estimés n'a laissé aucune place au contenu",
|
||||
"InstructionDetailRulesExhausted": "le budget de portée du projet de {tokens} tokens estimés est épuisé par les instructions",
|
||||
"InstructionDetailOverCeiling": "plus grand que le plafond de {bytes} octets",
|
||||
"InstructionDetailUnreadable": "lecture impossible : {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "déjà chargé depuis une portée plus large",
|
||||
"InstructionDetailRulesCap": "au-delà du plafond de {count} fichiers de règles par répertoire",
|
||||
"InstructionDetailSymlinkedRules": "refus : le répertoire de règles est un lien symbolique",
|
||||
"InstructionBudgetRefused": "le budget d'instructions a refusé cette requête : {reason}",
|
||||
"InstructionBudgetOverGuard": "le préfixe statique de ~{measured} tokens estimés dépasse la limite {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "केवल तर्क {attempt}/{limit}; कोई उत्तर या टूल कॉल नहीं; {action}",
|
||||
"OutputLimitAnswerRetryBody": "आउटपुट सीमा {attempt}/{limit}; कोई उत्तर या टूल कॉल नहीं; {action}",
|
||||
"AnswerRetryActionBare": "उत्तर दोबारा माँगा जा रहा है",
|
||||
"AnswerRetryActionNudge": "संकेत के साथ उत्तर दोबारा माँगा जा रहा है",
|
||||
"AnswerRetryCounter": "केवल तर्क/आउटपुट सीमा",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · सुझाया गया",
|
||||
"OrcaRouterOfferAction": "/provider या /model में OrcaRouter का ऑफ़र देखें",
|
||||
"OrcaRouterOfferBody": "Codewhale × OrcaRouter ऑफ़र के साथ टॉप-अप पर DeepSeek V4.1 Flash के लिए 30% अतिरिक्त क्रेडिट पाएँ। मौजूदा शर्तों के लिए ऑफ़र पेज देखें।",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale को OrcaRouter से रेफ़रल शुल्क मिलता है।",
|
||||
"ShellWaitNoActive": "अभी किसी शेल कमांड के पूरा होने का इंतज़ार नहीं हो रहा है।",
|
||||
"ShellWaitInteractive": "इंटरैक्टिव कमांड बैकग्राउंड में जारी नहीं रह सकतीं। इस बारी को रद्द करें या कमांड पूरी होने की प्रतीक्षा करें।",
|
||||
"ShellWaitUnavailable": "यह टूल बैकग्राउंड में जारी नहीं रह सकता। इस बारी को रद्द करें या इसके पूरा होने की प्रतीक्षा करें।",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ बाद के लिए सहेजें · /provider जोड़ता है",
|
||||
"ComposerHintSendNow": "↵ अभी भेजें",
|
||||
"ComposerHintSendIntoTurn": "↵ इस टर्न में भेजें",
|
||||
"ComposerHintLongDraft": "{shortcut}: बाहरी संपादक",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ इस टर्न में भेजा जा रहा है: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ इस टर्न में नहीं भेजा जा सका: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ फॉलो-अप संपादित हो रहा है: ",
|
||||
@@ -464,6 +474,9 @@
|
||||
"CmdImportClaudeDescription": "Claude Code की सेटिंग्स और MCP सर्वरों के लिए समीक्षा-योग्य आयात योजना बनाएं",
|
||||
"CmdClearDescription": "वार्तालाप इतिहास साफ़ करें",
|
||||
"CmdColorDescription": "इस चैट के इनपुट बॉक्स का रंग सेट करें",
|
||||
"CmdColorSet": "इस चैट के इनपुट बॉक्स का रंग: {color}",
|
||||
"CmdColorReset": "इस चैट के इनपुट बॉक्स का रंग थीम के डिफ़ॉल्ट पर रीसेट किया गया",
|
||||
"CmdColorUnknown": "अज्ञात रंग। इनमें से चुनें: {colors}, या off।",
|
||||
"CmdCompactDescription": "जगह खाली करने के लिए संदर्भ कॉम्पैक्शन चलाएँ",
|
||||
"CmdPurgeDescription": "एजेंट को संदर्भ जगह खाली करने के लिए वार्तालाप इतिहास चुनिंदा काटने दें",
|
||||
"CmdConfigDescription": "सेटिंग्स देखें और बदलें (उन्नत विवरण के रूप में कच्ची कुंजियाँ)",
|
||||
@@ -795,7 +808,7 @@
|
||||
"KbSendDraft": "खाली होने पर भेजें; व्यस्त होने पर कतार करें; खाली कम्पोज़र अगला कतारबद्ध फ़ॉलो-अप तुरंत भेजता है",
|
||||
"KbSteerCurrentTurn": "वर्तमान टर्न को दिशा दें (खाली समय में सामान्य भेजता है)",
|
||||
"KbCloseMenu": "मेनू बंद करें, अनुरोध रद्द करें, मसौदा छोड़ें, या इनपुट साफ़ करें",
|
||||
"KbCancelOrExit": "अनुरोध रद्द करें, या खाली समय में बाहर निकलें",
|
||||
"KbCancelOrExit": "रद्द करें, मसौदा साफ़ करें या निष्क्रिय होने पर बाहर निकलें",
|
||||
"KbShellControls": "चलती शेल प्रतीक्षा को /jobs में ले जाएँ",
|
||||
"KbExitEmpty": "इनपुट खाली होने पर बाहर निकलें",
|
||||
"KbCommandPalette": "कमांड पैलेट खोलें",
|
||||
@@ -1257,6 +1270,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ समीक्षा · {details} विवरण · Esc रोकें",
|
||||
"ApprovalOwnerHeader": "एजेंट: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g एजेंट पर जाएँ",
|
||||
"UserInputActionRequired": "कार्रवाई आवश्यक",
|
||||
"UserInputQuestionProgress": "प्रश्न {current} / {total}",
|
||||
"UserInputOtherLabel": "अन्य",
|
||||
"UserInputOtherDescription": "अपना उत्तर लिखें",
|
||||
"UserInputConfirmLabel": "चयन की पुष्टि करें",
|
||||
"UserInputConfirmDescription": "चयन जमा करें ({count})",
|
||||
"UserInputCustomPrompt": "अपना उत्तर:",
|
||||
"UserInputCustomPlaceholder": "(अपना उत्तर लिखें)",
|
||||
"UserInputHintSubmit": "जमा करें",
|
||||
"UserInputHintBack": "वापस",
|
||||
"UserInputHintMove": "घुमाएँ",
|
||||
"UserInputHintToggle": "टॉगल",
|
||||
"UserInputHintSelectConfirm": "चुनें/पुष्टि",
|
||||
"UserInputHintCancel": "रद्द करें",
|
||||
"UserInputHintQuickPick": "त्वरित चयन",
|
||||
"UserInputHintConfirm": "पुष्टि",
|
||||
"UserInputHintDigit": "अंक",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ समीक्षा · {details} विवरण · Esc छिपाएँ",
|
||||
"PendingApprovalInAgent": "{agent} में स्वीकृति चाहिए — /agents",
|
||||
"ApprovalTruncationHint": " … छाँटा गया · पूरा विवरण के लिए {details} दबाएँ",
|
||||
@@ -1353,6 +1383,8 @@
|
||||
"ToolReceiptLinesSingular": "1 पंक्ति",
|
||||
"ToolReceiptLinesPlural": "{count} पंक्तियाँ",
|
||||
"ToolDescriptionNote": "टिप्पणी",
|
||||
"ToolStatusWaitingForApproval": "अनुमति की प्रतीक्षा",
|
||||
"ToolStatusQueued": "कतार में",
|
||||
"CmdConstitutionAccountApplies": "खाता प्रोफ़ाइल संविधान: लागू है (साइन इन; इंजन हर चरण में लोड करता है)",
|
||||
"CmdConstitutionAccountSignedOut": "खाता प्रोफ़ाइल संविधान: साइन इन नहीं है",
|
||||
"CmdConstitutionAccountCheckFailed": "खाता प्रोफ़ाइल संविधान: खाते की जाँच विफल ({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "कोई प्रोजेक्ट, प्रोफ़ाइल, परिवेश, प्रबंधित config या संगठन की आवश्यकता इस मान को नियंत्रित करती है",
|
||||
"ConfigHintManagedAllowShell": "कोई प्रोजेक्ट, प्रोफ़ाइल, परिवेश या प्रबंधित config shell पहुँच को नियंत्रित करता है",
|
||||
"ConfigHintAllowShell": "चालू होने पर एजेंट मोड में shell टूल उपलब्ध होते हैं; अनुमति नियम फिर भी लागू रहते हैं",
|
||||
"ConfigHintComposerMultilineMode": "बंद: Enter भेजता है, Shift+Enter पंक्ति जोड़ता है; चालू: Enter पंक्ति जोड़ता है, Shift+Enter भेजता है",
|
||||
"ConfigHintComposerMultilineMode": "बंद: Enter भेजता है, Alt+Enter / Shift+Enter पंक्ति जोड़ता है; चालू: Enter पंक्ति जोड़ता है, Alt+Enter / Shift+Enter भेजता है",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off; सटीक बदलाव Alt/Option+V विवरण में बना रहता है",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "योजना बनाना जारी रखें",
|
||||
"PlanHandoffKeepPlanningDetail": "Plan में बने रहें। कुछ भी नहीं बदलता।",
|
||||
"PlanHandoffProceed": "योजना के अनुसार काम शुरू करें।",
|
||||
"PlanHandoffTodoNote": "इस योजना को todo_write से ट्रैक करें: \"{title}\" आइटम को अपनी सूची में रखें और योजना पूरी होने पर उसे पूरा हुआ चिह्नित करें।",
|
||||
"PetAppearanceLegacy": "पेट पुराने संस्करण पर चल रहा है। व्हेल का रूप और टेक्स्ट की भाषा चुनने के लिए Codewhale अपडेट करने के बाद पेट को फिर से शुरू करें।",
|
||||
"PluginOAuthBrowser": "ब्राउज़र से साइन इन करने के लिए Enter दबाएँ। वापस जाने के लिए Esc दबाएँ। API कुंजी की ज़रूरत नहीं है।",
|
||||
"PluginOAuthReady": "{provider}: साइन इन हो गया। मॉडल चुनें।",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "इस रन की sandbox नीति; danger-full-access के अलावा कोई भी --sandbox (या कॉन्फ़िगरेशन में समान sandbox_mode) ट्रस्ट मोड बंद करता है।",
|
||||
"ExecAfterHelp": "उदाहरण:\n codewhale exec \"इस फ़ंक्शन को समझाएँ\"\n codewhale exec --auto \"ls से crates/ की सूची दिखाएँ\"\n codewhale exec --auto --output-format stream-json \"विफल हो रहा टेस्ट ठीक करें\"\n\nसामान्य `codewhale exec` मॉडल से एक बार जवाब लेता है: अन्य रन वाले ही सिस्टम\nप्रॉम्प्ट के साथ Engine का एक टर्न, बिना टूल के। बिना इंटरैक्शन के टूल वाले\nएजेंट को चलाने के लिए `--auto` इस्तेमाल करें। टूल केवल `--auto`, `--yolo`,\n`--allowed-tools` के साथ या सत्र फिर शुरू करने पर उपलब्ध होते हैं;\n`--max-turns`, `--disallowed-tools` या `--sandbox` जैसी सीमाएँ और आउटपुट\nफ़ॉर्मैट कभी टूल नहीं जोड़ते। प्रदाता की आउटपुट सीमा से कटे जवाब को उसी टर्न\nमें आगे जारी रखा जाता है। `--max-turns` से अन्य सीमा न देने पर मॉडल के\nअधिकतम 8 चरण होते हैं।\n--auto डिफ़ॉल्ट रूप से Full Access इस्तेमाल करता है: शेल सैंडबॉक्स नहीं और भरोसा मोड चालू। शेल सैंडबॉक्स और फ़ाइल टूल की कार्यक्षेत्र संबंधी पाबंदियाँ बनाए रखने के लिए --sandbox workspace-write इस्तेमाल करें; कॉन्फ़िगर की गई अधिक सख्त नीतियाँ भी लागू होती हैं। सैंडबॉक्स द्वारा रोकी गई कार्रवाई दोबारा आज़माने के लिए --allow-sandbox-elevation या --sandbox danger-full-access ज़रूरी है।\nटूल उपलब्ध कराने वाले रन, stream-json रन और फिर शुरू किए गए सत्र सहेजे जाते हैं, ताकि codewhale sessions, exec --continue और codewhale receipts <id> में उनका उपयोग हो सके। केवल जवाब देने वाले सामान्य text/--json रन नहीं सहेजे जाते, जब तक संदर्भ संपीड़न के लिए पुनर्प्राप्ति डेटा की ज़रूरत न हो।\ndanger-full-access के अलावा कोई भी --sandbox (या कॉन्फ़िगरेशन में समान sandbox_mode) ट्रस्ट मोड बंद करता है।\n",
|
||||
"ExecPromptHelp": "मॉडल को भेजने वाला प्रॉम्प्ट। - से शुरू होने वाले या exec फ़्लैग को सामान्य टेक्स्ट के रूप में रखने वाले प्रॉम्प्ट से पहले -- लगाएँ; मानक इनपुट पढ़ने के लिए --prompt-file - का उपयोग करें।",
|
||||
"ExecFlagAfterPrompt": "{flag} प्रॉम्प्ट के बाद है और टेक्स्ट माना जाएगा। फ़्लैग प्रॉम्प्ट से पहले रखें, या फ़्लैग को सामान्य टेक्स्ट के रूप में भेजने के लिए उसे -- के बाद रखें।"
|
||||
"ExecFlagAfterPrompt": "{flag} प्रॉम्प्ट के बाद है और टेक्स्ट माना जाएगा। फ़्लैग प्रॉम्प्ट से पहले रखें, या फ़्लैग को सामान्य टेक्स्ट के रूप में भेजने के लिए उसे -- के बाद रखें।",
|
||||
"InstructionNoticeHeader": "स्थायी निर्देश इस सत्र के शुरू होने से पहले छाँट दिए गए या छोड़ दिए गए:",
|
||||
"InstructionNoticeFooter": "स्रोत-वार पूरा ब्योरा देखने के लिए /context report चलाएँ।",
|
||||
"InstructionNoticeMore": "…और {count} अन्य; पूरी सूची /context report में है।",
|
||||
"InstructionNoticeLine": "निर्देश: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " {total_bytes} में से {kept_bytes} बाइट और ~{total_tokens} में से ~{kept_tokens} अनुमानित टोकन रखे गए",
|
||||
"InstructionNoticeDroppedBytes": " {bytes} बाइट हटाए गए",
|
||||
"InstructionNoticeFallback": "; विकल्प: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "निर्देश: निर्देश छाँटने के बाद भी स्थिर प्रीफ़िक्स, स्थिर प्रीफ़िक्स सीमा से ~{tokens} अनुमानित टोकन ऊपर है; नीति `trim` के साथ जारी है — static_prefix_guard_percent बढ़ाएँ या टूल का दायरा घटाएँ।",
|
||||
"InstructionNoticeStaticGuardTrimmed": "निर्देश: स्थिर प्रीफ़िक्स को सीमा के भीतर रखने के लिए {scope} दायरा ~{tokens} अनुमानित टोकन छाँटा गया",
|
||||
"InstructionDetailDroppedBroadest": "सबसे व्यापक दायरे के खंड के रूप में हटाया गया",
|
||||
"InstructionDetailNoRoom": "{tokens} अनुमानित टोकन के दायरा बजट में सामग्री के लिए जगह नहीं बची",
|
||||
"InstructionDetailRulesExhausted": "{tokens} अनुमानित टोकन का प्रोजेक्ट दायरा बजट निर्देशों से समाप्त हो गया",
|
||||
"InstructionDetailOverCeiling": "{bytes} बाइट की सीमा से बड़ा",
|
||||
"InstructionDetailUnreadable": "पढ़ा नहीं जा सका: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "अधिक व्यापक दायरे से पहले ही लोड हो चुका है",
|
||||
"InstructionDetailRulesCap": "प्रति निर्देशिका {count} नियम फ़ाइलों की सीमा से अधिक",
|
||||
"InstructionDetailSymlinkedRules": "सिमलिंक वाली नियम निर्देशिका अस्वीकार की गई",
|
||||
"InstructionBudgetRefused": "निर्देश बजट ने इस अनुरोध को अस्वीकार कर दिया: {reason}",
|
||||
"InstructionBudgetOverGuard": "स्थिर प्रीफ़िक्स ~{measured} अनुमानित टोकन सीमा {guard} से अधिक है"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "hanya penalaran {attempt}/{limit}; tanpa jawaban atau panggilan alat; {action}",
|
||||
"OutputLimitAnswerRetryBody": "batas keluaran {attempt}/{limit}; tanpa jawaban atau panggilan alat; {action}",
|
||||
"AnswerRetryActionBare": "meminta jawaban kembali",
|
||||
"AnswerRetryActionNudge": "meminta jawaban kembali dengan pengingat",
|
||||
"AnswerRetryCounter": "hanya penalaran/batas keluaran",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Direkomendasikan",
|
||||
"OrcaRouterOfferAction": "Lihat penawaran OrcaRouter di /provider atau /model",
|
||||
"OrcaRouterOfferBody": "Dapatkan tambahan kredit DeepSeek V4.1 Flash sebesar 30% dari isi ulang melalui penawaran Codewhale × OrcaRouter. Lihat ketentuan terbaru di halaman penawaran.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale menerima komisi rujukan dari OrcaRouter.",
|
||||
"ShellWaitNoActive": "Tidak ada perintah shell yang sedang ditunggu.",
|
||||
"ShellWaitInteractive": "Perintah interaktif tidak dapat berjalan di latar belakang. Batalkan giliran atau tunggu hingga selesai.",
|
||||
"ShellWaitUnavailable": "Alat ini tidak dapat berjalan di latar belakang. Batalkan giliran atau tunggu hingga selesai.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ simpan untuk nanti · /provider menghubungkan",
|
||||
"ComposerHintSendNow": "↵ kirim sekarang",
|
||||
"ComposerHintSendIntoTurn": "↵ kirim ke giliran ini",
|
||||
"ComposerHintLongDraft": "{shortcut}: editor eksternal",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ Mengirim ke giliran ini: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ Tidak bisa mengirim ke giliran ini: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ Menyunting tindak lanjut: ",
|
||||
@@ -464,6 +474,9 @@
|
||||
"CmdImportClaudeDescription": "Rencanakan impor yang dapat ditinjau untuk pengaturan dan server MCP Claude Code",
|
||||
"CmdClearDescription": "Hapus riwayat percakapan",
|
||||
"CmdColorDescription": "Atur warna kotak tulis untuk obrolan ini",
|
||||
"CmdColorSet": "Warna kotak tulis untuk obrolan ini: {color}",
|
||||
"CmdColorReset": "Warna kotak tulis untuk obrolan ini dikembalikan ke bawaan tema",
|
||||
"CmdColorUnknown": "Warna tidak dikenal. Pilih salah satu: {colors}, atau off.",
|
||||
"CmdCompactDescription": "Picu pemadatan konteks untuk mengosongkan ruang",
|
||||
"CmdPurgeDescription": "Biarkan agen memangkas riwayat percakapan secara selektif untuk mengosongkan konteks",
|
||||
"CmdConfigDescription": "Periksa dan ubah pengaturan (kunci mentah sebagai detail lanjutan)",
|
||||
@@ -795,7 +808,7 @@
|
||||
"KbSendDraft": "Kirim saat idle; antrekan saat sibuk; komposer kosong mengirim tindak lanjut berikutnya dalam antrean sekarang",
|
||||
"KbSteerCurrentTurn": "Arahkan giliran saat ini (terkirim normal saat idle)",
|
||||
"KbCloseMenu": "Tutup menu, batalkan permintaan, buang draf, atau bersihkan input",
|
||||
"KbCancelOrExit": "Batalkan permintaan, atau keluar saat idle",
|
||||
"KbCancelOrExit": "Batalkan, hapus draf, atau keluar saat idle",
|
||||
"KbShellControls": "Pindahkan penantian shell yang berjalan ke /jobs",
|
||||
"KbExitEmpty": "Keluar saat input kosong",
|
||||
"KbCommandPalette": "Buka palet perintah",
|
||||
@@ -1257,6 +1270,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ tinjau · {details} detail · Esc hentikan",
|
||||
"ApprovalOwnerHeader": "Agen: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g buka agen",
|
||||
"UserInputActionRequired": "Tindakan diperlukan",
|
||||
"UserInputQuestionProgress": "Pertanyaan {current} dari {total}",
|
||||
"UserInputOtherLabel": "Lainnya",
|
||||
"UserInputOtherDescription": "Ketik jawaban kustom",
|
||||
"UserInputConfirmLabel": "Konfirmasi pilihan",
|
||||
"UserInputConfirmDescription": "Kirim pilihan ({count})",
|
||||
"UserInputCustomPrompt": "Jawaban kustom:",
|
||||
"UserInputCustomPlaceholder": "(ketik jawaban Anda)",
|
||||
"UserInputHintSubmit": "kirim",
|
||||
"UserInputHintBack": "kembali",
|
||||
"UserInputHintMove": "pindah",
|
||||
"UserInputHintToggle": "alihkan",
|
||||
"UserInputHintSelectConfirm": "pilih/konfirmasi",
|
||||
"UserInputHintCancel": "batal",
|
||||
"UserInputHintQuickPick": "pilih cepat",
|
||||
"UserInputHintConfirm": "konfirmasi",
|
||||
"UserInputHintDigit": "angka",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ tinjau · {details} detail · Esc sembunyikan",
|
||||
"PendingApprovalInAgent": "Persetujuan diperlukan di {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … terpotong · tekan {details} untuk detail lengkap",
|
||||
@@ -1353,6 +1383,8 @@
|
||||
"ToolReceiptLinesSingular": "1 baris",
|
||||
"ToolReceiptLinesPlural": "{count} baris",
|
||||
"ToolDescriptionNote": "catatan",
|
||||
"ToolStatusWaitingForApproval": "menunggu persetujuan",
|
||||
"ToolStatusQueued": "mengantre",
|
||||
"CmdConstitutionAccountApplies": "Konstitusi profil akun: berlaku (masuk; dimuat Engine setiap giliran)",
|
||||
"CmdConstitutionAccountSignedOut": "Konstitusi profil akun: belum masuk",
|
||||
"CmdConstitutionAccountCheckFailed": "Konstitusi profil akun: pemeriksaan akun gagal ({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "proyek, profil, lingkungan, config terkelola, atau persyaratan organisasi mengendalikan nilai ini",
|
||||
"ConfigHintManagedAllowShell": "proyek, profil, lingkungan, atau config terkelola mengendalikan akses shell",
|
||||
"ConfigHintAllowShell": "aktif memunculkan alat shell di mode Agen; aturan izin tetap berlaku",
|
||||
"ConfigHintComposerMultilineMode": "nonaktif: Enter mengirim, Shift+Enter menambah baris; aktif: Enter menambah baris, Shift+Enter mengirim",
|
||||
"ConfigHintComposerMultilineMode": "nonaktif: Enter mengirim, Alt+Enter / Shift+Enter menambah baris; aktif: Enter menambah baris, Alt+Enter / Shift+Enter mengirim",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off; perubahan persis tetap ada di detail Alt/Option+V",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Lanjutkan merencanakan",
|
||||
"PlanHandoffKeepPlanningDetail": "Tetap di Plan. Tidak ada yang diubah.",
|
||||
"PlanHandoffProceed": "Silakan jalankan rencananya.",
|
||||
"PlanHandoffTodoNote": "Lacak rencana ini dengan todo_write: pertahankan item \"{title}\" di daftar Anda dan tandai selesai setelah rencana rampung.",
|
||||
"PetAppearanceLegacy": "Pet masih memakai versi lama. Mulai ulang pet setelah memperbarui Codewhale untuk memilih gaya paus dan bahasa teks.",
|
||||
"PluginOAuthBrowser": "Tekan Enter untuk masuk melalui peramban atau Esc untuk kembali. Kunci API tidak diperlukan.",
|
||||
"PluginOAuthReady": "{provider}: berhasil masuk. Pilih model.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Kebijakan sandbox untuk proses ini; setiap --sandbox selain danger-full-access (atau sandbox_mode yang setara dalam konfigurasi) menonaktifkan mode kepercayaan.",
|
||||
"ExecAfterHelp": "Contoh:\n codewhale exec \"jelaskan fungsi ini\"\n codewhale exec --auto \"tampilkan isi crates/ dengan ls\"\n codewhale exec --auto --output-format stream-json \"perbaiki pengujian yang gagal\"\n\n`codewhale exec` biasa menghasilkan satu respons model: satu giliran Engine\ndengan prompt sistem yang sama seperti eksekusi lain, tanpa alat. Gunakan\n`--auto` untuk menjalankan agen dengan alat secara noninteraktif. Alat hanya\ndisediakan dengan `--auto`, `--yolo`, `--allowed-tools`, atau saat melanjutkan\nsesi; batas seperti `--max-turns`, `--disallowed-tools`, atau `--sandbox`, dan\nformat keluaran, tidak pernah menambahkan alat. Respons yang terpotong karena\nbatas keluaran penyedia dilanjutkan dalam giliran yang sama, hingga 8 langkah\nmodel kecuali `--max-turns` menentukan batas lain.\n--auto menggunakan Full Access secara bawaan: tanpa sandbox shell dan dengan mode kepercayaan aktif. Gunakan --sandbox workspace-write untuk mempertahankan sandbox shell dan pembatasan alat berkas pada ruang kerja; kebijakan lebih ketat yang dikonfigurasi juga berlaku. Mencoba ulang operasi yang ditolak sandbox memerlukan --allow-sandbox-elevation atau --sandbox danger-full-access.\nEksekusi yang menyediakan alat, eksekusi stream-json, dan sesi yang dilanjutkan disimpan untuk codewhale sessions, exec --continue, dan codewhale receipts <id>. Eksekusi text/--json biasa yang hanya memberikan jawaban tidak disimpan, kecuali pemadatan memerlukan data pemulihan.\nSetiap --sandbox selain danger-full-access (atau sandbox_mode yang setara dalam konfigurasi) menonaktifkan mode kepercayaan.\n",
|
||||
"ExecPromptHelp": "Prompt untuk dikirim ke model. Letakkan -- sebelum prompt yang diawali - atau memuat flag exec sebagai teks literal; gunakan --prompt-file - untuk membaca input standar.",
|
||||
"ExecFlagAfterPrompt": "{flag} muncul setelah prompt dan akan dianggap sebagai teks. Letakkan flag sebelum prompt, atau letakkan flag yang dimaksudkan sebagai teks literal setelah --."
|
||||
"ExecFlagAfterPrompt": "{flag} muncul setelah prompt dan akan dianggap sebagai teks. Letakkan flag sebelum prompt, atau letakkan flag yang dimaksudkan sebagai teks literal setelah --.",
|
||||
"InstructionNoticeHeader": "Instruksi tetap dipangkas atau dilewati sebelum sesi ini dimulai:",
|
||||
"InstructionNoticeFooter": "Jalankan /context report untuk rincian lengkap per sumber.",
|
||||
"InstructionNoticeMore": "…dan {count} lagi; daftar lengkap ada di /context report.",
|
||||
"InstructionNoticeLine": "instruksi: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " dipertahankan {kept_bytes} dari {total_bytes} byte dan ~{kept_tokens} dari ~{total_tokens} token perkiraan",
|
||||
"InstructionNoticeDroppedBytes": " {bytes} byte dibuang",
|
||||
"InstructionNoticeFallback": "; cadangan: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "instruksi: prefiks statis masih ~{tokens} token perkiraan di atas batas prefiks statis setelah instruksi dipangkas; dilanjutkan dengan kebijakan `trim` — naikkan static_prefix_guard_percent atau persempit cakupan alat.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "instruksi: cakupan {scope} dipangkas ~{tokens} token perkiraan agar prefiks statis tetap di bawah batas",
|
||||
"InstructionDetailDroppedBroadest": "dibuang sebagai segmen cakupan terluas",
|
||||
"InstructionDetailNoRoom": "anggaran cakupan sebesar {tokens} token perkiraan tidak menyisakan ruang untuk konten",
|
||||
"InstructionDetailRulesExhausted": "anggaran cakupan proyek sebesar {tokens} token perkiraan habis oleh instruksi",
|
||||
"InstructionDetailOverCeiling": "melebihi batas {bytes} byte",
|
||||
"InstructionDetailUnreadable": "tidak dapat dibaca: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "sudah dimuat dari cakupan yang lebih luas",
|
||||
"InstructionDetailRulesCap": "melebihi batas {count} berkas aturan per direktori",
|
||||
"InstructionDetailSymlinkedRules": "menolak direktori aturan berupa symlink",
|
||||
"InstructionBudgetRefused": "anggaran instruksi menolak permintaan ini: {reason}",
|
||||
"InstructionBudgetOverGuard": "prefiks statis ~{measured} token perkiraan melebihi batas {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "推論のみ {attempt}/{limit}、回答もツール呼び出しもありません。{action}",
|
||||
"OutputLimitAnswerRetryBody": "出力上限 {attempt}/{limit}、回答もツール呼び出しもありません。{action}",
|
||||
"AnswerRetryActionBare": "回答を再要求します",
|
||||
"AnswerRetryActionNudge": "補足指示を添えて回答を再要求します",
|
||||
"AnswerRetryCounter": "推論のみ/出力上限",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · おすすめ",
|
||||
"OrcaRouterOfferAction": "/provider または /model で OrcaRouter の特典を表示",
|
||||
"OrcaRouterOfferBody": "Codewhale × OrcaRouter の特典で、DeepSeek V4.1 Flash 向けチャージ額の30%が追加されます。最新の条件は特典ページでご確認ください。",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale は OrcaRouter から紹介料を受け取ります。",
|
||||
"ShellWaitNoActive": "待機中のシェルコマンドはありません。",
|
||||
"ShellWaitInteractive": "対話型コマンドはバックグラウンドで続行できません。ターンをキャンセルするか、完了をお待ちください。",
|
||||
"ShellWaitUnavailable": "このツールはバックグラウンドで続行できません。ターンをキャンセルするか、完了をお待ちください。",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ あとで送るために保存 · /provider で接続",
|
||||
"ComposerHintSendNow": "↵ すぐ送信",
|
||||
"ComposerHintSendIntoTurn": "↵ このターンに送る",
|
||||
"ComposerHintLongDraft": "{shortcut}: 外部エディター",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ このターンに送信中: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ このターンに送れませんでした: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ フォローアップを編集中: ",
|
||||
@@ -467,6 +477,9 @@
|
||||
"CmdImportClaudeDescription": "Claude Code の設定と MCP サーバーの確認可能なインポート計画を作成",
|
||||
"CmdClearDescription": "会話履歴をクリア",
|
||||
"CmdColorDescription": "このチャットの入力欄の色を設定",
|
||||
"CmdColorSet": "このチャットの入力欄の色: {color}",
|
||||
"CmdColorReset": "このチャットの入力欄の色をテーマのデフォルトに戻しました",
|
||||
"CmdColorUnknown": "不明な色です。次から選択してください: {colors}、または off。",
|
||||
"CmdCompactDescription": "コンテキスト圧縮で容量を確保",
|
||||
"CmdPurgeDescription": "エージェントに会話履歴を分析させ、不要なメッセージを削除・要約",
|
||||
"CmdConfigDescription": "設定を確認・変更(生キーは詳細/高度)",
|
||||
@@ -812,7 +825,7 @@
|
||||
"KbSendDraft": "待機中は送信、実行中はキュー。入力欄が空なら次のキューを今すぐ送信",
|
||||
"KbSteerCurrentTurn": "現在のターンをステア(待機中は通常送信)",
|
||||
"KbCloseMenu": "メニューを閉じる、リクエストをキャンセル、下書きを破棄、または入力をクリア",
|
||||
"KbCancelOrExit": "リクエストをキャンセル、またはアイドル時に終了",
|
||||
"KbCancelOrExit": "キャンセル、下書きを消去、またはアイドル時に終了",
|
||||
"KbShellControls": "実行中のシェル待機を /jobs へ移動",
|
||||
"KbExitEmpty": "入力が空の時に終了",
|
||||
"KbCommandPalette": "コマンドパレットを開く",
|
||||
@@ -1278,6 +1291,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ 履歴 · {details} 詳細 · Esc 停止",
|
||||
"ApprovalOwnerHeader": "エージェント: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g エージェントへ移動",
|
||||
"UserInputActionRequired": "対応が必要です",
|
||||
"UserInputQuestionProgress": "質問 {current}/{total}",
|
||||
"UserInputOtherLabel": "その他",
|
||||
"UserInputOtherDescription": "自由入力で回答する",
|
||||
"UserInputConfirmLabel": "選択を確定",
|
||||
"UserInputConfirmDescription": "選択した {count} 件を送信",
|
||||
"UserInputCustomPrompt": "自由入力:",
|
||||
"UserInputCustomPlaceholder": "(回答を入力)",
|
||||
"UserInputHintSubmit": "送信",
|
||||
"UserInputHintBack": "戻る",
|
||||
"UserInputHintMove": "移動",
|
||||
"UserInputHintToggle": "切替",
|
||||
"UserInputHintSelectConfirm": "選択/確定",
|
||||
"UserInputHintCancel": "キャンセル",
|
||||
"UserInputHintQuickPick": "クイック選択",
|
||||
"UserInputHintConfirm": "確定",
|
||||
"UserInputHintDigit": "数字",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ 履歴 · {details} 詳細 · Esc 隠す",
|
||||
"PendingApprovalInAgent": "{agent} で承認が必要です — /agents",
|
||||
"ApprovalTruncationHint": " … 省略 · {details} で詳細を表示",
|
||||
@@ -1374,6 +1404,8 @@
|
||||
"ToolReceiptLinesSingular": "1 行",
|
||||
"ToolReceiptLinesPlural": "{count} 行",
|
||||
"ToolDescriptionNote": "説明",
|
||||
"ToolStatusWaitingForApproval": "承認待ち",
|
||||
"ToolStatusQueued": "待機中",
|
||||
"CmdConstitutionAccountApplies": "アカウントの憲章: 適用(ログイン済み。各ターンで Engine が読み込み)",
|
||||
"CmdConstitutionAccountSignedOut": "アカウントの憲章: 未ログイン",
|
||||
"CmdConstitutionAccountCheckFailed": "アカウントの憲章: アカウント確認に失敗({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "プロジェクト・プロファイル・環境・管理された設定・組織の要件がこの値を制御します",
|
||||
"ConfigHintManagedAllowShell": "プロジェクト・プロファイル・環境・管理された設定がシェルアクセスを制御します",
|
||||
"ConfigHintAllowShell": "オンにするとエージェントモードでシェルツールを公開します。権限ルールは引き続き適用されます",
|
||||
"ConfigHintComposerMultilineMode": "オフ: Enter で送信、Shift+Enter で改行。オン: Enter で改行、Shift+Enter で送信",
|
||||
"ConfigHintComposerMultilineMode": "オフ: Enter で送信、Alt+Enter / Shift+Enter で改行。オン: Enter で改行、Alt+Enter / Shift+Enter で送信",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off。正確な変更は Alt/Option+V の詳細に残ります",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "計画を続ける",
|
||||
"PlanHandoffKeepPlanningDetail": "Plan のままにします。何も変更されません。",
|
||||
"PlanHandoffProceed": "計画どおりに進めてください。",
|
||||
"PlanHandoffTodoNote": "この計画を todo_write で追跡してください。リストに項目「{title}」を残し、計画が終わったら完了にしてください。",
|
||||
"PetAppearanceLegacy": "ペットは以前のバージョンで動作しています。Codewhale を更新してペットを再起動すると、クジラのスタイルと文字の言語を選べます。",
|
||||
"PluginOAuthBrowser": "Enter でブラウザーからログインします。Esc で戻ります。API キーは不要です。",
|
||||
"PluginOAuthReady": "{provider}:ログインしました。モデルを選択してください。",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "この実行のサンドボックスポリシー。danger-full-access 以外の --sandbox(または設定内の同等の sandbox_mode)は信頼モードを無効にします。",
|
||||
"ExecAfterHelp": "使用例:\n codewhale exec \"この関数を説明して\"\n codewhale exec --auto \"ls で crates/ の内容を表示して\"\n codewhale exec --auto --output-format stream-json \"失敗しているテストを修正して\"\n\n通常の `codewhale exec` はモデルの単発応答です。他の実行と同じシステム\nプロンプトを使い、ツールなしで Engine を 1 ターン実行します。対話なしで\nツールを使うエージェントを実行するには `--auto` を指定してください。\nツールが提供されるのは `--auto`、`--yolo`、`--allowed-tools` の指定時、\nまたはセッションの再開時のみです。`--max-turns`、`--disallowed-tools`、\n`--sandbox` などの制限や出力形式を指定してもツールは追加されません。\nプロバイダーの出力上限で途切れた応答は同じターン内で続行されます。\n`--max-turns` で変更しない限り、モデルのステップ数は最大 8 回です。\n--auto は既定で Full Access です。シェルのサンドボックスはなく、信頼モードが有効です。シェルのサンドボックスと、ファイルツールのワークスペース制限を維持するには --sandbox workspace-write を指定してください。設定済みのより厳しいポリシーも適用されます。サンドボックスで拒否された操作の再試行には --allow-sandbox-elevation または --sandbox danger-full-access が必要です。\nツールを提供する実行、stream-json の実行、再開したセッションは保存され、codewhale sessions、exec --continue、codewhale receipts <id> で利用できます。応答のみの text/--json 実行は、圧縮時に復旧用の保存が必要な場合を除いて保存されません。\ndanger-full-access 以外の --sandbox(または設定内の同等の sandbox_mode)は信頼モードを無効にします。\n",
|
||||
"ExecPromptHelp": "モデルに送信するプロンプト。- で始まる場合や exec のフラグを文字列として含む場合は、プロンプトの前に -- を置いてください。標準入力を読むには --prompt-file - を使用してください。",
|
||||
"ExecFlagAfterPrompt": "{flag} がプロンプトの後にあり、文字列として扱われます。フラグはプロンプトの前に置くか、文字列として渡す場合は -- の後に置いてください。"
|
||||
"ExecFlagAfterPrompt": "{flag} がプロンプトの後にあり、文字列として扱われます。フラグはプロンプトの前に置くか、文字列として渡す場合は -- の後に置いてください。",
|
||||
"InstructionNoticeHeader": "常設の指示は、このセッションの開始前に切り詰められたか、スキップされました:",
|
||||
"InstructionNoticeFooter": "ソースごとの詳細は /context report を実行してください。",
|
||||
"InstructionNoticeMore": "…ほか {count} 件。全一覧は /context report にあります。",
|
||||
"InstructionNoticeLine": "指示: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " {total_bytes} バイト中 {kept_bytes} バイト、推定 ~{total_tokens} トークン中 ~{kept_tokens} トークンを保持",
|
||||
"InstructionNoticeDroppedBytes": " {bytes} バイトを破棄",
|
||||
"InstructionNoticeFallback": "; 代替: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "指示: 指示を切り詰めても、静的プレフィックスはなお推定 ~{tokens} トークン、静的プレフィックスの上限を超えています。ポリシー `trim` のまま続行します — static_prefix_guard_percent を上げるか、ツールの範囲を絞ってください。",
|
||||
"InstructionNoticeStaticGuardTrimmed": "指示: 静的プレフィックスを上限内に収めるため、{scope} スコープを推定 ~{tokens} トークン切り詰めました",
|
||||
"InstructionDetailDroppedBroadest": "最も広いスコープのセグメントとして破棄",
|
||||
"InstructionDetailNoRoom": "推定 {tokens} トークンのスコープ予算では内容を置く余地がありませんでした",
|
||||
"InstructionDetailRulesExhausted": "推定 {tokens} トークンのプロジェクトスコープ予算を指示が使い切りました",
|
||||
"InstructionDetailOverCeiling": "上限 {bytes} バイトを超えています",
|
||||
"InstructionDetailUnreadable": "読み取れませんでした: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "より広いスコープから読み込み済み",
|
||||
"InstructionDetailRulesCap": "ディレクトリごとのルール上限 {count} ファイルを超えています",
|
||||
"InstructionDetailSymlinkedRules": "シンボリックリンクのルールディレクトリは拒否します",
|
||||
"InstructionBudgetRefused": "指示の予算により、このリクエストは拒否されました: {reason}",
|
||||
"InstructionBudgetOverGuard": "静的プレフィックス ~{measured} 推定トークンが上限 {guard} を超えています"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "추론만 반환 {attempt}/{limit}; 답변이나 도구 호출 없음; {action}",
|
||||
"OutputLimitAnswerRetryBody": "출력 한도 {attempt}/{limit}; 답변이나 도구 호출 없음; {action}",
|
||||
"AnswerRetryActionBare": "답변을 다시 요청합니다",
|
||||
"AnswerRetryActionNudge": "안내를 덧붙여 답변을 다시 요청합니다",
|
||||
"AnswerRetryCounter": "추론만 반환/출력 한도",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · 추천",
|
||||
"OrcaRouterOfferAction": "/provider 또는 /model에서 OrcaRouter 혜택 보기",
|
||||
"OrcaRouterOfferBody": "Codewhale × OrcaRouter 혜택으로 충전 금액의 30%를 DeepSeek V4.1 Flash 크레딧으로 추가 적립하세요. 최신 조건은 혜택 페이지에서 확인하세요.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale은 OrcaRouter로부터 추천 수수료를 받습니다.",
|
||||
"ShellWaitNoActive": "현재 완료를 기다리는 셸 명령이 없습니다.",
|
||||
"ShellWaitInteractive": "대화형 명령은 백그라운드에서 계속 실행할 수 없습니다. 턴을 취소하거나 명령이 완료될 때까지 기다리세요.",
|
||||
"ShellWaitUnavailable": "이 도구는 백그라운드에서 계속 실행할 수 없습니다. 턴을 취소하거나 완료될 때까지 기다리세요.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ 나중에 보내도록 저장 · /provider로 연결",
|
||||
"ComposerHintSendNow": "↵ 지금 보내기",
|
||||
"ComposerHintSendIntoTurn": "↵ 이 턴으로 보내기",
|
||||
"ComposerHintLongDraft": "{shortcut}: 외부 편집기",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ 이 턴으로 보내는 중: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ 이 턴으로 보내지 못함: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ 후속 메시지 편집 중: ",
|
||||
@@ -467,6 +477,9 @@
|
||||
"CmdImportClaudeDescription": "Claude Code 설정과 MCP 서버의 검토 가능한 가져오기 계획을 생성합니다",
|
||||
"CmdClearDescription": "대화 기록을 지웁니다",
|
||||
"CmdColorDescription": "이 채팅의 입력창 색상을 설정",
|
||||
"CmdColorSet": "이 채팅의 입력창 색상: {color}",
|
||||
"CmdColorReset": "이 채팅의 입력창 색상을 테마 기본값으로 되돌렸습니다",
|
||||
"CmdColorUnknown": "알 수 없는 색상입니다. 다음 중 선택하세요: {colors} 또는 off.",
|
||||
"CmdCompactDescription": "컨텍스트 압축을 실행해 공간을 확보합니다",
|
||||
"CmdPurgeDescription": "에이전트가 대화 기록을 선별적으로 정리해 컨텍스트 공간을 확보하게 합니다",
|
||||
"CmdConfigDescription": "설정을 확인하고 변경합니다 (고급 정보로 원시 키 제공)",
|
||||
@@ -812,7 +825,7 @@
|
||||
"KbSendDraft": "대기 중이면 전송, 실행 중이면 대기열에 추가; 입력창이 비어 있으면 다음 후속 메시지를 즉시 전송",
|
||||
"KbSteerCurrentTurn": "현재 턴 조정 (대기 중에는 일반 전송)",
|
||||
"KbCloseMenu": "메뉴 닫기, 요청 취소, 초안 버리기, 또는 입력 지우기",
|
||||
"KbCancelOrExit": "요청 취소, 유휴 상태면 종료",
|
||||
"KbCancelOrExit": "취소, 초안 지우기 또는 유휴 상태에서 종료",
|
||||
"KbShellControls": "실행 중인 셸 대기를 /jobs로 이동",
|
||||
"KbExitEmpty": "입력창이 비어 있으면 종료",
|
||||
"KbCommandPalette": "명령 팔레트 열기",
|
||||
@@ -1280,6 +1293,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ 기록 · {details} 상세 · Esc 중지",
|
||||
"ApprovalOwnerHeader": "에이전트: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g 에이전트로 이동",
|
||||
"UserInputActionRequired": "조치 필요",
|
||||
"UserInputQuestionProgress": "질문 {current}/{total}",
|
||||
"UserInputOtherLabel": "기타",
|
||||
"UserInputOtherDescription": "직접 입력해서 답변하기",
|
||||
"UserInputConfirmLabel": "선택 확인",
|
||||
"UserInputConfirmDescription": "선택한 {count}개 제출",
|
||||
"UserInputCustomPrompt": "직접 입력:",
|
||||
"UserInputCustomPlaceholder": "(답변을 입력하세요)",
|
||||
"UserInputHintSubmit": "제출",
|
||||
"UserInputHintBack": "뒤로",
|
||||
"UserInputHintMove": "이동",
|
||||
"UserInputHintToggle": "토글",
|
||||
"UserInputHintSelectConfirm": "선택/확인",
|
||||
"UserInputHintCancel": "취소",
|
||||
"UserInputHintQuickPick": "빠른 선택",
|
||||
"UserInputHintConfirm": "확인",
|
||||
"UserInputHintDigit": "숫자",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ 기록 · {details} 상세 · Esc 숨기기",
|
||||
"PendingApprovalInAgent": "{agent}에서 승인이 필요합니다 — /agents",
|
||||
"ApprovalTruncationHint": " … 잘림 · 전체 상세는 {details}",
|
||||
@@ -1376,6 +1406,8 @@
|
||||
"ToolReceiptLinesSingular": "1줄",
|
||||
"ToolReceiptLinesPlural": "{count}줄",
|
||||
"ToolDescriptionNote": "설명",
|
||||
"ToolStatusWaitingForApproval": "승인 대기 중",
|
||||
"ToolStatusQueued": "대기 중",
|
||||
"CmdConstitutionAccountApplies": "계정 프로필 규약: 적용됨 (로그인 상태, Engine이 매 턴 로드)",
|
||||
"CmdConstitutionAccountSignedOut": "계정 프로필 규약: 로그인되지 않음",
|
||||
"CmdConstitutionAccountCheckFailed": "계정 프로필 규약: 계정 확인 실패 ({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "프로젝트, 프로필, 환경, 관리 설정 또는 조직 요구 사항이 이 값을 제어",
|
||||
"ConfigHintManagedAllowShell": "프로젝트, 프로필, 환경 또는 관리 설정이 shell 접근을 제어",
|
||||
"ConfigHintAllowShell": "켜면 에이전트 모드에서 shell 도구를 노출. 권한 규칙은 계속 적용",
|
||||
"ConfigHintComposerMultilineMode": "끔: Enter로 전송, Shift+Enter로 줄 추가. 켬: Enter로 줄 추가, Shift+Enter로 전송",
|
||||
"ConfigHintComposerMultilineMode": "끔: Enter로 전송, Alt+Enter / Shift+Enter로 줄 추가. 켬: Enter로 줄 추가, Alt+Enter / Shift+Enter로 전송",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off. 정확한 변경은 Alt/Option+V 세부 정보에 남음",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "계속 계획하기",
|
||||
"PlanHandoffKeepPlanningDetail": "Plan 에 머뭅니다. 아무것도 변경되지 않습니다.",
|
||||
"PlanHandoffProceed": "계획대로 진행해 주세요.",
|
||||
"PlanHandoffTodoNote": "이 계획을 todo_write로 추적하세요. 목록에 \"{title}\" 항목을 유지하고 계획이 끝나면 완료로 표시하세요.",
|
||||
"PetAppearanceLegacy": "펫이 이전 버전으로 실행 중입니다. Codewhale을 업데이트한 뒤 펫을 다시 시작하면 고래 스타일과 텍스트 언어를 선택할 수 있습니다.",
|
||||
"PluginOAuthBrowser": "Enter를 눌러 브라우저에서 로그인하세요. Esc를 누르면 돌아갑니다. API 키는 필요하지 않습니다.",
|
||||
"PluginOAuthReady": "{provider}: 로그인했습니다. 모델을 선택하세요.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "이번 실행의 샌드박스 정책입니다. danger-full-access 이외의 --sandbox(또는 설정의 동등한 sandbox_mode)는 신뢰 모드를 끕니다.",
|
||||
"ExecAfterHelp": "예시:\n codewhale exec \"이 함수를 설명해 줘\"\n codewhale exec --auto \"ls로 crates/ 목록을 보여 줘\"\n codewhale exec --auto --output-format stream-json \"실패한 테스트를 수정해 줘\"\n\n일반 `codewhale exec`는 모델의 일회성 응답입니다. 다른 실행과 같은 시스템\n프롬프트로 도구 없이 Engine 턴을 한 번 실행합니다. 대화형 입력 없이\n도구를 쓰는 에이전트를 실행하려면 `--auto`를 사용하세요. 도구는 `--auto`,\n`--yolo`, `--allowed-tools`를 지정하거나 세션을 재개할 때만 제공됩니다.\n`--max-turns`, `--disallowed-tools`, `--sandbox` 같은 제한이나 출력 형식은\n도구를 추가하지 않습니다. 공급자의 출력 한도로 잘린 응답은 같은 턴에서\n이어지며, `--max-turns`로 달리 지정하지 않으면 모델 단계는 최대 8회입니다.\n--auto의 기본값은 Full Access입니다. 셸 샌드박스가 없고 신뢰 모드가 켜집니다. 셸 샌드박스와 파일 도구의 워크스페이스 제한을 유지하려면 --sandbox workspace-write를 사용하세요. 설정된 더 엄격한 정책도 적용됩니다. 샌드박스에서 거부된 작업을 재시도하려면 --allow-sandbox-elevation 또는 --sandbox danger-full-access가 필요합니다.\n도구를 제공하는 실행, stream-json 실행, 재개한 세션은 저장되며 codewhale sessions, exec --continue, codewhale receipts <id>에서 사용할 수 있습니다. 응답만 제공하는 일반 text/--json 실행은 압축 시 복구 데이터가 필요한 경우를 제외하고 저장되지 않습니다.\ndanger-full-access 이외의 --sandbox(또는 설정의 동등한 sandbox_mode)는 신뢰 모드를 끕니다.\n",
|
||||
"ExecPromptHelp": "모델에 보낼 프롬프트입니다. -로 시작하거나 exec 플래그를 일반 텍스트로 포함하는 프롬프트 앞에는 --를 넣으세요. 표준 입력을 읽으려면 --prompt-file -를 사용하세요.",
|
||||
"ExecFlagAfterPrompt": "{flag}가 프롬프트 뒤에 있어 텍스트로 처리됩니다. 플래그는 프롬프트 앞에 넣고, 일반 텍스트로 사용할 플래그는 -- 뒤에 넣으세요."
|
||||
"ExecFlagAfterPrompt": "{flag}가 프롬프트 뒤에 있어 텍스트로 처리됩니다. 플래그는 프롬프트 앞에 넣고, 일반 텍스트로 사용할 플래그는 -- 뒤에 넣으세요.",
|
||||
"InstructionNoticeHeader": "상시 지침이 이 세션이 시작되기 전에 잘리거나 건너뛰어졌습니다:",
|
||||
"InstructionNoticeFooter": "소스별 전체 내역은 /context report를 실행하세요.",
|
||||
"InstructionNoticeMore": "…외 {count}건. 전체 목록은 /context report에 있습니다.",
|
||||
"InstructionNoticeLine": "지침: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " {total_bytes}바이트 중 {kept_bytes}바이트, 추정 ~{total_tokens}토큰 중 ~{kept_tokens}토큰 유지",
|
||||
"InstructionNoticeDroppedBytes": " {bytes}바이트 제외",
|
||||
"InstructionNoticeFallback": "; 대체: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "지침: 지침을 잘라낸 뒤에도 정적 접두부가 정적 접두부 한도를 추정 ~{tokens}토큰 초과합니다. `trim` 정책으로 계속합니다 — static_prefix_guard_percent를 높이거나 도구 범위를 줄이세요.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "지침: 정적 접두부를 한도 아래로 유지하려고 {scope} 범위를 추정 ~{tokens}토큰 잘랐습니다",
|
||||
"InstructionDetailDroppedBroadest": "가장 넓은 범위의 세그먼트로 제외됨",
|
||||
"InstructionDetailNoRoom": "추정 {tokens}토큰의 범위 예산으로는 내용을 담을 여유가 없었습니다",
|
||||
"InstructionDetailRulesExhausted": "추정 {tokens}토큰의 프로젝트 범위 예산을 지침이 모두 사용했습니다",
|
||||
"InstructionDetailOverCeiling": "{bytes}바이트 한도를 초과합니다",
|
||||
"InstructionDetailUnreadable": "읽을 수 없음: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "더 넓은 범위에서 이미 로드됨",
|
||||
"InstructionDetailRulesCap": "디렉터리당 규칙 파일 한도 {count}개를 초과합니다",
|
||||
"InstructionDetailSymlinkedRules": "심볼릭 링크 규칙 디렉터리는 거부합니다",
|
||||
"InstructionBudgetRefused": "지침 예산이 이 요청을 거부했습니다: {reason}",
|
||||
"InstructionBudgetOverGuard": "정적 접두부 ~{measured} 추정 토큰이 한도 {guard}를 초과합니다"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "somente raciocínio {attempt}/{limit}; sem resposta nem chamada de ferramenta; {action}",
|
||||
"OutputLimitAnswerRetryBody": "limite de saída {attempt}/{limit}; sem resposta nem chamada de ferramenta; {action}",
|
||||
"AnswerRetryActionBare": "solicitando a resposta novamente",
|
||||
"AnswerRetryActionNudge": "solicitando a resposta novamente com um lembrete",
|
||||
"AnswerRetryCounter": "somente raciocínio/limite de saída",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Recomendado",
|
||||
"OrcaRouterOfferAction": "Ver oferta da OrcaRouter em /provider ou /model",
|
||||
"OrcaRouterOfferBody": "Receba 30% de crédito extra para DeepSeek V4.1 Flash nas recargas com a oferta Codewhale × OrcaRouter. Confira os termos atuais na página da oferta.",
|
||||
"OrcaRouterOfferDisclosure": "A Codewhale recebe uma comissão por indicação da OrcaRouter.",
|
||||
"ShellWaitNoActive": "Não há espera ativa por um comando do shell.",
|
||||
"ShellWaitInteractive": "Comandos interativos não podem continuar em segundo plano. Cancele o turno ou aguarde a conclusão.",
|
||||
"ShellWaitUnavailable": "Esta ferramenta não pode continuar em segundo plano. Cancele o turno ou aguarde a conclusão.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ salvar para depois · /provider conecta",
|
||||
"ComposerHintSendNow": "↵ enviar agora",
|
||||
"ComposerHintSendIntoTurn": "↵ enviar nesta rodada",
|
||||
"ComposerHintLongDraft": "{shortcut}: editor externo",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ Enviando nesta rodada: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ Não foi possível enviar nesta rodada: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ Editando o acompanhamento: ",
|
||||
@@ -467,6 +477,9 @@
|
||||
"CmdImportClaudeDescription": "Planejar uma importação revisável das configurações e servidores MCP do Claude Code",
|
||||
"CmdClearDescription": "Limpar o histórico da conversa",
|
||||
"CmdColorDescription": "Definir a cor da caixa de mensagem desta conversa",
|
||||
"CmdColorSet": "Cor da caixa de mensagem desta conversa: {color}",
|
||||
"CmdColorReset": "Cor da caixa de mensagem desta conversa redefinida para o padrão do tema",
|
||||
"CmdColorUnknown": "Cor desconhecida. Escolha uma de: {colors} ou off.",
|
||||
"CmdCompactDescription": "Compactar o contexto para liberar espaço",
|
||||
"CmdPurgeDescription": "Deixe o agente podar cirurgicamente o histórico para liberar espaço de contexto",
|
||||
"CmdConfigDescription": "Inspecionar e alterar configurações (chaves brutas no detalhe avançado)",
|
||||
@@ -812,7 +825,7 @@
|
||||
"KbSendDraft": "Enviar quando ocioso; enfileirar quando ocupado; com o compositor vazio, enviar agora o próximo follow-up",
|
||||
"KbSteerCurrentTurn": "Direcionar o turno atual (envia normalmente quando ocioso)",
|
||||
"KbCloseMenu": "Fechar menu, cancelar requisição, descartar rascunho ou limpar entrada",
|
||||
"KbCancelOrExit": "Cancelar requisição ou sair quando ocioso",
|
||||
"KbCancelOrExit": "Cancelar, limpar rascunho ou sair quando ocioso",
|
||||
"KbShellControls": "Mover a espera do shell para /jobs",
|
||||
"KbExitEmpty": "Sair quando entrada vazia",
|
||||
"KbCommandPalette": "Abrir paleta de comandos",
|
||||
@@ -1278,6 +1291,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ revisar · {details} detalhes · Esc parar",
|
||||
"ApprovalOwnerHeader": "Agente: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g ir para o agente",
|
||||
"UserInputActionRequired": "Ação necessária",
|
||||
"UserInputQuestionProgress": "Pergunta {current} de {total}",
|
||||
"UserInputOtherLabel": "Outro",
|
||||
"UserInputOtherDescription": "Digite uma resposta personalizada",
|
||||
"UserInputConfirmLabel": "Confirmar seleção",
|
||||
"UserInputConfirmDescription": "Enviar seleção ({count})",
|
||||
"UserInputCustomPrompt": "Resposta personalizada:",
|
||||
"UserInputCustomPlaceholder": "(digite sua resposta)",
|
||||
"UserInputHintSubmit": "enviar",
|
||||
"UserInputHintBack": "voltar",
|
||||
"UserInputHintMove": "mover",
|
||||
"UserInputHintToggle": "alternar",
|
||||
"UserInputHintSelectConfirm": "marcar/confirmar",
|
||||
"UserInputHintCancel": "cancelar",
|
||||
"UserInputHintQuickPick": "seleção rápida",
|
||||
"UserInputHintConfirm": "confirmar",
|
||||
"UserInputHintDigit": "nº",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ revisar · {details} detalhes · Esc ocultar",
|
||||
"PendingApprovalInAgent": "Aprovação necessária em {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … truncado · pressione {details} para ver todos os detalhes",
|
||||
@@ -1374,6 +1404,8 @@
|
||||
"ToolReceiptLinesSingular": "1 linha",
|
||||
"ToolReceiptLinesPlural": "{count} linhas",
|
||||
"ToolDescriptionNote": "nota",
|
||||
"ToolStatusWaitingForApproval": "aguardando aprovação",
|
||||
"ToolStatusQueued": "na fila",
|
||||
"CmdConstitutionAccountApplies": "Constituição do perfil da conta: aplicada (conectado; carregada pelo Engine a cada turno)",
|
||||
"CmdConstitutionAccountSignedOut": "Constituição do perfil da conta: não conectado",
|
||||
"CmdConstitutionAccountCheckFailed": "Constituição do perfil da conta: falha na verificação ({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "um projeto, perfil, ambiente, config gerenciado ou exigência da organização controla este valor",
|
||||
"ConfigHintManagedAllowShell": "um projeto, perfil, ambiente ou config gerenciado controla o acesso ao shell",
|
||||
"ConfigHintAllowShell": "ligado expõe ferramentas de shell no modo Agente; as regras de permissão continuam valendo",
|
||||
"ConfigHintComposerMultilineMode": "desligado: Enter envia, Shift+Enter adiciona linha; ligado: Enter adiciona linha, Shift+Enter envia",
|
||||
"ConfigHintComposerMultilineMode": "desligado: Enter envia, Alt+Enter / Shift+Enter adiciona linha; ligado: Enter adiciona linha, Alt+Enter / Shift+Enter envia",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off; a mudança exata permanece nos detalhes de Alt/Option+V",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Continuar planejando",
|
||||
"PlanHandoffKeepPlanningDetail": "Permanecer em Plan. Nada é alterado.",
|
||||
"PlanHandoffProceed": "Pode seguir com o plano.",
|
||||
"PlanHandoffTodoNote": "Acompanhe este plano com todo_write: mantenha o item \"{title}\" na sua lista e marque-o como concluído quando o plano terminar.",
|
||||
"PetAppearanceLegacy": "O pet está usando uma versão anterior. Reinicie-o após atualizar o Codewhale para escolher o estilo da baleia e o idioma do texto.",
|
||||
"PluginOAuthBrowser": "Pressione Enter para entrar pelo navegador ou Esc para voltar. Não é necessária uma chave de API.",
|
||||
"PluginOAuthReady": "{provider}: sessão iniciada. Escolha um modelo.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Política de sandbox desta execução; qualquer --sandbox diferente de danger-full-access (ou sandbox_mode equivalente na configuração) desativa o modo de confiança.",
|
||||
"ExecAfterHelp": "Exemplos:\n codewhale exec \"explique esta função\"\n codewhale exec --auto \"liste crates/ com ls\"\n codewhale exec --auto --output-format stream-json \"corrija o teste que falha\"\n\n`codewhale exec` sem opções produz uma única resposta do modelo: um turno do\nEngine com o mesmo prompt de sistema das demais execuções e sem ferramentas.\nUse `--auto` para executar um agente com ferramentas sem interação.\nAs ferramentas só são oferecidas com `--auto`, `--yolo`, `--allowed-tools`\nou ao retomar uma sessão; limites como `--max-turns`, `--disallowed-tools`\nou `--sandbox`, assim como o formato de saída, nunca adicionam ferramentas.\nUma resposta cortada pelo limite de saída do provedor continua no mesmo turno,\ncom no máximo 8 etapas do modelo, salvo indicação diferente de `--max-turns`.\n--auto usa Full Access por padrão: sem sandbox do shell e com modo de confiança ativado. Use --sandbox workspace-write para manter o sandbox do shell e as restrições das ferramentas de arquivos ao workspace; políticas mais restritivas configuradas também se aplicam. Repetir uma operação negada pelo sandbox exige --allow-sandbox-elevation ou --sandbox danger-full-access.\nExecuções que oferecem ferramentas, execuções stream-json e sessões retomadas são salvas para codewhale sessions, exec --continue e codewhale receipts <id>. Execuções text/--json que apenas respondem não são salvas, a menos que a compactação precise de dados de recuperação.\nQualquer --sandbox diferente de danger-full-access (ou sandbox_mode equivalente na configuração) desativa o modo de confiança.\n",
|
||||
"ExecPromptHelp": "Prompt enviado ao modelo. Coloque -- antes de um prompt que comece com - ou contenha uma opção literal de exec; use --prompt-file - para ler a entrada padrão.",
|
||||
"ExecFlagAfterPrompt": "{flag} aparece após o prompt e seria tratado como texto. Coloque as opções antes do prompt ou, para usá-las como texto literal, após --."
|
||||
"ExecFlagAfterPrompt": "{flag} aparece após o prompt e seria tratado como texto. Coloque as opções antes do prompt ou, para usá-las como texto literal, após --.",
|
||||
"InstructionNoticeHeader": "Instruções permanentes foram cortadas ou ignoradas antes do início desta sessão:",
|
||||
"InstructionNoticeFooter": "Execute /context report para ver o detalhamento completo por fonte.",
|
||||
"InstructionNoticeMore": "…e mais {count}; a lista completa está em /context report.",
|
||||
"InstructionNoticeLine": "instruções: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " mantidos {kept_bytes} de {total_bytes} bytes e ~{kept_tokens} de ~{total_tokens} tokens estimados",
|
||||
"InstructionNoticeDroppedBytes": " {bytes} bytes descartados",
|
||||
"InstructionNoticeFallback": "; alternativa: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "instruções: o prefixo estático ainda está ~{tokens} tokens estimados acima do limite do prefixo estático após o corte das instruções; continuando com a política `trim` — aumente static_prefix_guard_percent ou reduza a superfície de ferramentas.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "instruções: o escopo {scope} foi cortado em ~{tokens} tokens estimados para manter o prefixo estático abaixo do limite",
|
||||
"InstructionDetailDroppedBroadest": "descartado como segmento do escopo mais amplo",
|
||||
"InstructionDetailNoRoom": "o orçamento do escopo de {tokens} tokens estimados não deixou espaço para conteúdo",
|
||||
"InstructionDetailRulesExhausted": "o orçamento do escopo do projeto de {tokens} tokens estimados foi esgotado pelas instruções",
|
||||
"InstructionDetailOverCeiling": "maior que o limite de {bytes} bytes",
|
||||
"InstructionDetailUnreadable": "não foi possível ler: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "já carregado de um escopo mais amplo",
|
||||
"InstructionDetailRulesCap": "acima do limite de {count} arquivos de regras por diretório",
|
||||
"InstructionDetailSymlinkedRules": "recusando diretório de regras que é um link simbólico",
|
||||
"InstructionBudgetRefused": "o orçamento de instruções recusou esta solicitação: {reason}",
|
||||
"InstructionBudgetOverGuard": "o prefixo estático de ~{measured} tokens estimados excede o limite {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "только рассуждение {attempt}/{limit}; нет ответа или вызова инструмента; {action}",
|
||||
"OutputLimitAnswerRetryBody": "лимит вывода {attempt}/{limit}; нет ответа или вызова инструмента; {action}",
|
||||
"AnswerRetryActionBare": "повторно запрашивается ответ",
|
||||
"AnswerRetryActionNudge": "повторно запрашивается ответ с подсказкой",
|
||||
"AnswerRetryCounter": "только рассуждение/лимит вывода",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Рекомендуем",
|
||||
"OrcaRouterOfferAction": "Посмотреть предложение OrcaRouter в /provider или /model",
|
||||
"OrcaRouterOfferBody": "Получите 30% дополнительного кредита для DeepSeek V4.1 Flash при пополнении по предложению Codewhale × OrcaRouter. Актуальные условия указаны на странице предложения.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale получает от OrcaRouter комиссию за рекомендации.",
|
||||
"ShellWaitNoActive": "Сейчас нет ожидания команды оболочки.",
|
||||
"ShellWaitInteractive": "Интерактивные команды не могут продолжать работу в фоне. Отмените ход или дождитесь завершения команды.",
|
||||
"ShellWaitUnavailable": "Этот инструмент не может продолжать работу в фоне. Отмените ход или дождитесь его завершения.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ сохранить на потом · /provider подключает",
|
||||
"ComposerHintSendNow": "↵ отправить сейчас",
|
||||
"ComposerHintSendIntoTurn": "↵ отправить в этот ход",
|
||||
"ComposerHintLongDraft": "{shortcut}: внешний редактор",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ Отправка в этот ход: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ Не удалось отправить в этот ход: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ Редактируется следующее сообщение: ",
|
||||
@@ -464,6 +474,9 @@
|
||||
"CmdImportClaudeDescription": "Спланировать проверяемый импорт настроек Claude Code и серверов MCP",
|
||||
"CmdClearDescription": "Очистить историю диалога",
|
||||
"CmdColorDescription": "Задать цвет поля ввода для этого чата",
|
||||
"CmdColorSet": "Цвет поля ввода для этого чата: {color}",
|
||||
"CmdColorReset": "Цвет поля ввода для этого чата сброшен до значения темы по умолчанию",
|
||||
"CmdColorUnknown": "Неизвестный цвет. Выберите один из: {colors} или off.",
|
||||
"CmdCompactDescription": "Запустить сжатие контекста, чтобы освободить место",
|
||||
"CmdPurgeDescription": "Позволить агенту точечно сократить историю диалога и освободить контекст",
|
||||
"CmdConfigDescription": "Просмотр и изменение настроек (сырые ключи — для продвинутых)",
|
||||
@@ -795,7 +808,7 @@
|
||||
"KbSendDraft": "Отправить, когда свободно; в очередь, когда занято; пустое поле ввода отправляет следующее сообщение из очереди сразу",
|
||||
"KbSteerCurrentTurn": "Направить текущий ход (в простое отправляет как обычно)",
|
||||
"KbCloseMenu": "Закрыть меню, отменить запрос, отбросить черновик или очистить ввод",
|
||||
"KbCancelOrExit": "Отменить запрос или выйти в простое",
|
||||
"KbCancelOrExit": "Отменить, очистить черновик или выйти в простое",
|
||||
"KbShellControls": "Перенести ожидание запущенного шелла в /jobs",
|
||||
"KbExitEmpty": "Выйти при пустом вводе",
|
||||
"KbCommandPalette": "Открыть палитру команд",
|
||||
@@ -1257,6 +1270,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ просмотр · {details} детали · Esc стоп",
|
||||
"ApprovalOwnerHeader": "Агент: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g перейти к агенту",
|
||||
"UserInputActionRequired": "Требуется действие",
|
||||
"UserInputQuestionProgress": "Вопрос {current} из {total}",
|
||||
"UserInputOtherLabel": "Другое",
|
||||
"UserInputOtherDescription": "Введите свой вариант ответа",
|
||||
"UserInputConfirmLabel": "Подтвердить выбор",
|
||||
"UserInputConfirmDescription": "Отправить выбранное ({count})",
|
||||
"UserInputCustomPrompt": "Свой ответ:",
|
||||
"UserInputCustomPlaceholder": "(введите ответ)",
|
||||
"UserInputHintSubmit": "отправить",
|
||||
"UserInputHintBack": "назад",
|
||||
"UserInputHintMove": "перейти",
|
||||
"UserInputHintToggle": "отметить",
|
||||
"UserInputHintSelectConfirm": "выбрать/отправить",
|
||||
"UserInputHintCancel": "отмена",
|
||||
"UserInputHintQuickPick": "быстрый выбор",
|
||||
"UserInputHintConfirm": "подтвердить",
|
||||
"UserInputHintDigit": "№",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ просмотр · {details} детали · Esc скрыть",
|
||||
"PendingApprovalInAgent": "Нужно одобрение в {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … обрезано · нажмите {details} для полных деталей",
|
||||
@@ -1353,6 +1383,8 @@
|
||||
"ToolReceiptLinesSingular": "1 строка",
|
||||
"ToolReceiptLinesPlural": "{count} строк",
|
||||
"ToolDescriptionNote": "примечание",
|
||||
"ToolStatusWaitingForApproval": "ожидает одобрения",
|
||||
"ToolStatusQueued": "в очереди",
|
||||
"CmdConstitutionAccountApplies": "Конституция профиля аккаунта: применяется (вход выполнен; Engine загружает её на каждом ходе)",
|
||||
"CmdConstitutionAccountSignedOut": "Конституция профиля аккаунта: вход не выполнен",
|
||||
"CmdConstitutionAccountCheckFailed": "Конституция профиля аккаунта: проверка аккаунта не удалась ({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "это значение задаёт проект, профиль, окружение, управляемый config или требование организации",
|
||||
"ConfigHintManagedAllowShell": "доступ к shell задаёт проект, профиль, окружение или управляемый config",
|
||||
"ConfigHintAllowShell": "вкл открывает инструменты shell в режиме агента; правила разрешений по-прежнему действуют",
|
||||
"ConfigHintComposerMultilineMode": "выкл: Enter отправляет, Shift+Enter добавляет строку; вкл: Enter добавляет строку, Shift+Enter отправляет",
|
||||
"ConfigHintComposerMultilineMode": "выкл: Enter отправляет, Alt+Enter / Shift+Enter добавляет строку; вкл: Enter добавляет строку, Alt+Enter / Shift+Enter отправляет",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off; точное изменение остаётся в подробностях по Alt/Option+V",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Продолжить планирование",
|
||||
"PlanHandoffKeepPlanningDetail": "Остаться в Plan. Ничего не меняется.",
|
||||
"PlanHandoffProceed": "Приступайте к плану.",
|
||||
"PlanHandoffTodoNote": "Отслеживайте этот план через todo_write: оставьте в списке пункт \"{title}\" и отметьте его как выполненный, когда план будет завершён.",
|
||||
"PetAppearanceLegacy": "Питомец работает на старой версии. После обновления Codewhale перезапустите его, чтобы выбрать стиль кита и язык текста.",
|
||||
"PluginOAuthBrowser": "Нажмите Enter для входа через браузер или Esc для возврата. Ключ API не нужен.",
|
||||
"PluginOAuthReady": "{provider}: вход выполнен. Выберите модель.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Политика песочницы для этого запуска; любое значение --sandbox, кроме danger-full-access (или соответствующее значение sandbox_mode в конфигурации), отключает режим доверия.",
|
||||
"ExecAfterHelp": "Примеры:\n codewhale exec \"объясни эту функцию\"\n codewhale exec --auto \"покажи содержимое crates/ с помощью ls\"\n codewhale exec --auto --output-format stream-json \"исправь падающий тест\"\n\nОбычный `codewhale exec` выдаёт один ответ модели: один ход Engine с тем же\nсистемным промптом, что и в других запусках, без инструментов. Для запуска\nагента с инструментами без взаимодействия используйте `--auto`. Инструменты\nдоступны только с `--auto`, `--yolo`, `--allowed-tools` или при продолжении\nсессии; ограничения вроде `--max-turns`, `--disallowed-tools`, `--sandbox` и\nформат вывода никогда не добавляют инструменты. Ответ, прерванный лимитом\nвывода провайдера, продолжается в том же ходе, не более 8 шагов модели,\nесли `--max-turns` не задаёт другой предел.\nПо умолчанию --auto использует Full Access: без песочницы оболочки, с включённым режимом доверия. Используйте --sandbox workspace-write, чтобы сохранить песочницу оболочки и ограничения файловых инструментов рабочей областью; более строгие настроенные политики также действуют. Повтор операции, отклонённой песочницей, требует --allow-sandbox-elevation или --sandbox danger-full-access.\nЗапуски с доступными инструментами, запуски stream-json и возобновлённые сессии сохраняются для codewhale sessions, exec --continue и codewhale receipts <id>. Обычные запуски text/--json только с ответом не сохраняются, если сжатие контекста не требует данных для восстановления.\nЛюбое значение --sandbox, кроме danger-full-access (или соответствующее значение sandbox_mode в конфигурации), отключает режим доверия.\n",
|
||||
"ExecPromptHelp": "Запрос для модели. Поставьте -- перед запросом, который начинается с - или содержит флаг exec как обычный текст; --prompt-file - читает стандартный ввод.",
|
||||
"ExecFlagAfterPrompt": "{flag} указан после запроса и будет воспринят как текст. Укажите флаги перед запросом, а если флаг нужен как обычный текст — после --."
|
||||
"ExecFlagAfterPrompt": "{flag} указан после запроса и будет воспринят как текст. Укажите флаги перед запросом, а если флаг нужен как обычный текст — после --.",
|
||||
"InstructionNoticeHeader": "Постоянные инструкции были урезаны или пропущены до начала этой сессии:",
|
||||
"InstructionNoticeFooter": "Выполните /context report, чтобы увидеть полную разбивку по источникам.",
|
||||
"InstructionNoticeMore": "…и ещё {count}; полный список в /context report.",
|
||||
"InstructionNoticeLine": "инструкции: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " сохранено {kept_bytes} из {total_bytes} байт и ~{kept_tokens} из ~{total_tokens} оценочных токенов",
|
||||
"InstructionNoticeDroppedBytes": " отброшено {bytes} байт",
|
||||
"InstructionNoticeFallback": "; замена: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "инструкции: статический префикс всё ещё превышает его предел на ~{tokens} оценочных токенов после сокращения инструкций; работа продолжается с политикой `trim` — увеличьте static_prefix_guard_percent или сократите набор инструментов.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "инструкции: область {scope} сокращена на ~{tokens} оценочных токенов, чтобы статический префикс уложился в предел",
|
||||
"InstructionDetailDroppedBroadest": "отброшено как сегмент самой широкой области",
|
||||
"InstructionDetailNoRoom": "бюджет области в {tokens} оценочных токенов не оставил места для содержимого",
|
||||
"InstructionDetailRulesExhausted": "бюджет области проекта в {tokens} оценочных токенов исчерпан инструкциями",
|
||||
"InstructionDetailOverCeiling": "больше предела в {bytes} байт",
|
||||
"InstructionDetailUnreadable": "не удалось прочитать: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "уже загружено из более широкой области",
|
||||
"InstructionDetailRulesCap": "сверх предела в {count} файлов правил на каталог",
|
||||
"InstructionDetailSymlinkedRules": "отклонено: каталог правил является символической ссылкой",
|
||||
"InstructionBudgetRefused": "бюджет инструкций отклонил этот запрос: {reason}",
|
||||
"InstructionBudgetOverGuard": "статический префикс ~{measured} оценочных токенов превышает предел {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "лише міркування {attempt}/{limit}; немає відповіді чи виклику інструмента; {action}",
|
||||
"OutputLimitAnswerRetryBody": "ліміт виводу {attempt}/{limit}; немає відповіді чи виклику інструмента; {action}",
|
||||
"AnswerRetryActionBare": "повторно запитується відповідь",
|
||||
"AnswerRetryActionNudge": "повторно запитується відповідь із підказкою",
|
||||
"AnswerRetryCounter": "лише міркування/ліміт виводу",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Рекомендуємо",
|
||||
"OrcaRouterOfferAction": "Переглянути пропозицію OrcaRouter у /provider або /model",
|
||||
"OrcaRouterOfferBody": "Отримайте 30% додаткового кредиту для DeepSeek V4.1 Flash за поповнення з пропозицією Codewhale × OrcaRouter. Актуальні умови наведено на сторінці пропозиції.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale отримує від OrcaRouter комісію за рекомендації.",
|
||||
"ShellWaitNoActive": "Зараз немає очікування команди оболонки.",
|
||||
"ShellWaitInteractive": "Інтерактивні команди не можуть продовжувати роботу у фоні. Скасуйте хід або дочекайтеся завершення команди.",
|
||||
"ShellWaitUnavailable": "Цей інструмент не може продовжувати роботу у фоні. Скасуйте хід або дочекайтеся його завершення.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ зберегти на потім · /provider підключає",
|
||||
"ComposerHintSendNow": "↵ надіслати зараз",
|
||||
"ComposerHintSendIntoTurn": "↵ надіслати в цей хід",
|
||||
"ComposerHintLongDraft": "{shortcut}: зовнішній редактор",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ Надсилання в цей хід: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ Не вдалося надіслати в цей хід: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ Редагується наступне повідомлення: ",
|
||||
@@ -464,6 +474,9 @@
|
||||
"CmdImportClaudeDescription": "Спланувати перевірний імпорт налаштувань Claude Code і серверів MCP",
|
||||
"CmdClearDescription": "Очистити історію розмови",
|
||||
"CmdColorDescription": "Встановити колір поля введення для цього чату",
|
||||
"CmdColorSet": "Колір поля введення для цього чату: {color}",
|
||||
"CmdColorReset": "Колір поля введення для цього чату скинуто до типового для теми",
|
||||
"CmdColorUnknown": "Невідомий колір. Виберіть один із: {colors} або off.",
|
||||
"CmdCompactDescription": "Запустити стиснення контексту, щоб звільнити місце",
|
||||
"CmdPurgeDescription": "Дозволити агенту вибірково скоротити історію розмови, щоб звільнити контекст",
|
||||
"CmdConfigDescription": "Переглянути й змінити налаштування (сирі ключі — як додаткова деталь)",
|
||||
@@ -795,7 +808,7 @@
|
||||
"KbSendDraft": "Надіслати, коли вільно; у чергу, коли зайнято; порожній композер одразу надсилає наступне повідомлення з черги",
|
||||
"KbSteerCurrentTurn": "Скерувати поточний крок (коли вільно — надсилає як зазвичай)",
|
||||
"KbCloseMenu": "Закрити меню, скасувати запит, відкинути чернетку або очистити ввід",
|
||||
"KbCancelOrExit": "Скасувати запит або вийти, коли вільно",
|
||||
"KbCancelOrExit": "Скасувати, очистити чернетку або вийти, коли вільно",
|
||||
"KbShellControls": "Перенести очікування запущеної оболонки до /jobs",
|
||||
"KbExitEmpty": "Вийти, коли ввід порожній",
|
||||
"KbCommandPalette": "Відкрити палітру команд",
|
||||
@@ -1257,6 +1270,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ перегляд · {details} деталі · Esc зупинити",
|
||||
"ApprovalOwnerHeader": "Агент: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g перейти до агента",
|
||||
"UserInputActionRequired": "Потрібна дія",
|
||||
"UserInputQuestionProgress": "Питання {current} з {total}",
|
||||
"UserInputOtherLabel": "Інше",
|
||||
"UserInputOtherDescription": "Введіть власну відповідь",
|
||||
"UserInputConfirmLabel": "Підтвердити вибір",
|
||||
"UserInputConfirmDescription": "Надіслати вибране ({count})",
|
||||
"UserInputCustomPrompt": "Власна відповідь:",
|
||||
"UserInputCustomPlaceholder": "(введіть відповідь)",
|
||||
"UserInputHintSubmit": "надіслати",
|
||||
"UserInputHintBack": "назад",
|
||||
"UserInputHintMove": "рухатися",
|
||||
"UserInputHintToggle": "позначити",
|
||||
"UserInputHintSelectConfirm": "вибрати/підтв.",
|
||||
"UserInputHintCancel": "скасувати",
|
||||
"UserInputHintQuickPick": "швидкий вибір",
|
||||
"UserInputHintConfirm": "підтвердити",
|
||||
"UserInputHintDigit": "№",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ перегляд · {details} деталі · Esc сховати",
|
||||
"PendingApprovalInAgent": "Потрібне схвалення в {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … обрізано · натисніть {details} для повних деталей",
|
||||
@@ -1353,6 +1383,8 @@
|
||||
"ToolReceiptLinesSingular": "1 рядок",
|
||||
"ToolReceiptLinesPlural": "{count} рядків",
|
||||
"ToolDescriptionNote": "примітка",
|
||||
"ToolStatusWaitingForApproval": "очікує схвалення",
|
||||
"ToolStatusQueued": "у черзі",
|
||||
"CmdConstitutionAccountApplies": "Конституція профілю облікового запису: застосовується (вхід виконано; Engine завантажує її на кожному кроці)",
|
||||
"CmdConstitutionAccountSignedOut": "Конституція профілю облікового запису: вхід не виконано",
|
||||
"CmdConstitutionAccountCheckFailed": "Конституція профілю облікового запису: перевірка не вдалася ({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "це значення задає проєкт, профіль, середовище, керований config або вимога організації",
|
||||
"ConfigHintManagedAllowShell": "доступ до shell задає проєкт, профіль, середовище або керований config",
|
||||
"ConfigHintAllowShell": "увімк відкриває інструменти shell у режимі агента; правила дозволів і далі діють",
|
||||
"ConfigHintComposerMultilineMode": "вимк: Enter надсилає, Shift+Enter додає рядок; увімк: Enter додає рядок, Shift+Enter надсилає",
|
||||
"ConfigHintComposerMultilineMode": "вимк: Enter надсилає, Alt+Enter / Shift+Enter додає рядок; увімк: Enter додає рядок, Alt+Enter / Shift+Enter надсилає",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off; точна зміна залишається в подробицях за Alt/Option+V",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Планувати далі",
|
||||
"PlanHandoffKeepPlanningDetail": "Залишитися в Plan. Нічого не змінюється.",
|
||||
"PlanHandoffProceed": "Беріться до плану.",
|
||||
"PlanHandoffTodoNote": "Відстежуйте цей план через todo_write: залиште в списку пункт \"{title}\" і позначте його як виконаний, коли план буде завершено.",
|
||||
"PetAppearanceLegacy": "Улюбленець працює на старій версії. Після оновлення Codewhale перезапустіть його, щоб вибрати стиль кита й мову тексту.",
|
||||
"PluginOAuthBrowser": "Натисніть Enter для входу через браузер або Esc для повернення. Ключ API не потрібен.",
|
||||
"PluginOAuthReady": "{provider}: вхід виконано. Виберіть модель.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Політика пісочниці для цього запуску; будь-яке значення --sandbox, крім danger-full-access (або відповідне значення sandbox_mode у конфігурації), вимикає режим довіри.",
|
||||
"ExecAfterHelp": "Приклади:\n codewhale exec \"поясни цю функцію\"\n codewhale exec --auto \"покажи вміст crates/ за допомогою ls\"\n codewhale exec --auto --output-format stream-json \"виправ тест, що не проходить\"\n\nЗвичайний `codewhale exec` дає одну відповідь моделі: один хід Engine з тією\nсамою системною підказкою, що й інші запуски, без інструментів. Використовуйте\n`--auto`, щоб запускати агента з інструментами без взаємодії. Інструменти\nдоступні лише з `--auto`, `--yolo`, `--allowed-tools` або під час поновлення\nсесії; обмеження на кшталт `--max-turns`, `--disallowed-tools`, `--sandbox` і\nформат виводу ніколи не додають інструментів. Відповідь, перервана лімітом\nвиводу провайдера, продовжується в тому самому ході, щонайбільше 8 кроків\nмоделі, якщо `--max-turns` не задає іншу межу.\nТипово --auto використовує Full Access: без пісочниці оболонки, з увімкненим режимом довіри. Використовуйте --sandbox workspace-write, щоб зберегти пісочницю оболонки та обмеження файлових інструментів робочою областю; суворіші налаштовані політики також діють. Повтор операції, відхиленої пісочницею, потребує --allow-sandbox-elevation або --sandbox danger-full-access.\nЗапуски з доступними інструментами, запуски stream-json і поновлені сесії зберігаються для codewhale sessions, exec --continue і codewhale receipts <id>. Звичайні запуски text/--json лише з відповіддю не зберігаються, якщо стиснення контексту не потребує даних для відновлення.\nБудь-яке значення --sandbox, крім danger-full-access (або відповідне значення sandbox_mode у конфігурації), вимикає режим довіри.\n",
|
||||
"ExecPromptHelp": "Запит для моделі. Поставте -- перед запитом, який починається з - або містить прапорець exec як звичайний текст; --prompt-file - читає стандартний ввід.",
|
||||
"ExecFlagAfterPrompt": "{flag} вказано після запиту, тож його буде сприйнято як текст. Указуйте прапорці перед запитом, а якщо прапорець потрібен як звичайний текст — після --."
|
||||
"ExecFlagAfterPrompt": "{flag} вказано після запиту, тож його буде сприйнято як текст. Указуйте прапорці перед запитом, а якщо прапорець потрібен як звичайний текст — після --.",
|
||||
"InstructionNoticeHeader": "Постійні інструкції було скорочено або пропущено до початку цього сеансу:",
|
||||
"InstructionNoticeFooter": "Виконайте /context report, щоб побачити повну розбивку за джерелами.",
|
||||
"InstructionNoticeMore": "…і ще {count}; повний список у /context report.",
|
||||
"InstructionNoticeLine": "інструкції: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " збережено {kept_bytes} із {total_bytes} байтів і ~{kept_tokens} із ~{total_tokens} оціночних токенів",
|
||||
"InstructionNoticeDroppedBytes": " відкинуто {bytes} байтів",
|
||||
"InstructionNoticeFallback": "; заміна: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "інструкції: статичний префікс усе ще перевищує свій ліміт на ~{tokens} оціночних токенів після скорочення інструкцій; роботу продовжено з політикою `trim` — збільште static_prefix_guard_percent або скоротіть набір інструментів.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "інструкції: область {scope} скорочено на ~{tokens} оціночних токенів, щоб статичний префікс вклався в ліміт",
|
||||
"InstructionDetailDroppedBroadest": "відкинуто як сегмент найширшої області",
|
||||
"InstructionDetailNoRoom": "бюджет області в {tokens} оціночних токенів не залишив місця для вмісту",
|
||||
"InstructionDetailRulesExhausted": "бюджет області проєкту в {tokens} оціночних токенів вичерпано інструкціями",
|
||||
"InstructionDetailOverCeiling": "більше за ліміт у {bytes} байтів",
|
||||
"InstructionDetailUnreadable": "не вдалося прочитати: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "уже завантажено з ширшої області",
|
||||
"InstructionDetailRulesCap": "понад ліміт у {count} файлів правил на каталог",
|
||||
"InstructionDetailSymlinkedRules": "відхилено: каталог правил є символічним посиланням",
|
||||
"InstructionBudgetRefused": "бюджет інструкцій відхилив цей запит: {reason}",
|
||||
"InstructionBudgetOverGuard": "статичний префікс ~{measured} оціночних токенів перевищує ліміт {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "chỉ có suy luận {attempt}/{limit}; không có câu trả lời hay lệnh gọi công cụ; {action}",
|
||||
"OutputLimitAnswerRetryBody": "giới hạn đầu ra {attempt}/{limit}; không có câu trả lời hay lệnh gọi công cụ; {action}",
|
||||
"AnswerRetryActionBare": "đang yêu cầu trả lời lại",
|
||||
"AnswerRetryActionNudge": "đang yêu cầu trả lời lại kèm lời nhắc",
|
||||
"AnswerRetryCounter": "chỉ suy luận/giới hạn đầu ra",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · Đề xuất",
|
||||
"OrcaRouterOfferAction": "Xem ưu đãi OrcaRouter trong /provider hoặc /model",
|
||||
"OrcaRouterOfferBody": "Nhận thêm 30% tín dụng DeepSeek V4.1 Flash khi nạp tiền với ưu đãi Codewhale × OrcaRouter. Xem điều kiện hiện hành trên trang ưu đãi.",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale nhận phí giới thiệu từ OrcaRouter.",
|
||||
"ShellWaitNoActive": "Không có lệnh shell nào đang được chờ.",
|
||||
"ShellWaitInteractive": "Lệnh tương tác không thể tiếp tục chạy nền. Hãy hủy lượt hoặc chờ lệnh hoàn tất.",
|
||||
"ShellWaitUnavailable": "Công cụ này không thể tiếp tục chạy nền. Hãy hủy lượt hoặc chờ công cụ hoàn tất.",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ lưu để gửi sau · /provider kết nối",
|
||||
"ComposerHintSendNow": "↵ gửi ngay",
|
||||
"ComposerHintSendIntoTurn": "↵ gửi vào lượt này",
|
||||
"ComposerHintLongDraft": "{shortcut}: trình sửa ngoài",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ Đang gửi vào lượt này: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ Không gửi được vào lượt này: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ Đang sửa tin theo sau: ",
|
||||
@@ -467,6 +477,9 @@
|
||||
"CmdImportClaudeDescription": "Lập kế hoạch nhập có thể xem lại cho cài đặt và máy chủ MCP của Claude Code",
|
||||
"CmdClearDescription": "Xóa lịch sử trò chuyện",
|
||||
"CmdColorDescription": "Đặt màu ô soạn tin cho cuộc trò chuyện này",
|
||||
"CmdColorSet": "Màu ô soạn tin của cuộc trò chuyện này: {color}",
|
||||
"CmdColorReset": "Đã đặt lại màu ô soạn tin của cuộc trò chuyện này về mặc định của chủ đề",
|
||||
"CmdColorUnknown": "Màu không hợp lệ. Chọn một trong: {colors}, hoặc off.",
|
||||
"CmdCompactDescription": "Kích hoạt nén ngữ cảnh để giải phóng không gian",
|
||||
"CmdPurgeDescription": "Cho agent cắt gọn lịch sử trò chuyện để giải phóng ngữ cảnh",
|
||||
"CmdConfigDescription": "Xem và đổi cài đặt (khóa thô ở chi tiết nâng cao)",
|
||||
@@ -812,7 +825,7 @@
|
||||
"KbSendDraft": "Gửi khi rảnh; xếp hàng khi bận; khung soạn thảo trống sẽ gửi ngay follow-up tiếp theo trong hàng đợi",
|
||||
"KbSteerCurrentTurn": "Điều hướng lượt hiện tại (gửi bình thường khi rảnh)",
|
||||
"KbCloseMenu": "Đóng menu, hủy yêu cầu, hủy bản nháp hoặc xóa sạch đầu vào",
|
||||
"KbCancelOrExit": "Hủy yêu cầu, hoặc thoát khi rảnh",
|
||||
"KbCancelOrExit": "Hủy, xóa bản nháp hoặc thoát khi rảnh",
|
||||
"KbShellControls": "Chuyển phiên chờ shell sang /jobs",
|
||||
"KbExitEmpty": "Thoát khi khung nhập trống",
|
||||
"KbCommandPalette": "Mở bảng lệnh (command palette)",
|
||||
@@ -1278,6 +1291,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ xem lại · {details} chi tiết · Esc dừng",
|
||||
"ApprovalOwnerHeader": "Agent: {agent} · {role}",
|
||||
"ApprovalGoToAgent": "g tới agent",
|
||||
"UserInputActionRequired": "Cần bạn thao tác",
|
||||
"UserInputQuestionProgress": "Câu hỏi {current}/{total}",
|
||||
"UserInputOtherLabel": "Khác",
|
||||
"UserInputOtherDescription": "Nhập câu trả lời tùy chỉnh",
|
||||
"UserInputConfirmLabel": "Xác nhận lựa chọn",
|
||||
"UserInputConfirmDescription": "Gửi lựa chọn ({count})",
|
||||
"UserInputCustomPrompt": "Câu trả lời tùy chỉnh:",
|
||||
"UserInputCustomPlaceholder": "(nhập câu trả lời của bạn)",
|
||||
"UserInputHintSubmit": "gửi",
|
||||
"UserInputHintBack": "quay lại",
|
||||
"UserInputHintMove": "di chuyển",
|
||||
"UserInputHintToggle": "bật/tắt",
|
||||
"UserInputHintSelectConfirm": "chọn/xác nhận",
|
||||
"UserInputHintCancel": "hủy",
|
||||
"UserInputHintQuickPick": "chọn nhanh",
|
||||
"UserInputHintConfirm": "xác nhận",
|
||||
"UserInputHintDigit": "chữ số",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ xem lại · {details} chi tiết · Esc ẩn",
|
||||
"PendingApprovalInAgent": "Cần phê duyệt trong {agent} — /agents",
|
||||
"ApprovalTruncationHint": " … đã rút gọn · nhấn {details} để xem đầy đủ",
|
||||
@@ -1374,6 +1404,8 @@
|
||||
"ToolReceiptLinesSingular": "1 dòng",
|
||||
"ToolReceiptLinesPlural": "{count} dòng",
|
||||
"ToolDescriptionNote": "ghi chú",
|
||||
"ToolStatusWaitingForApproval": "đang chờ phê duyệt",
|
||||
"ToolStatusQueued": "đang xếp hàng",
|
||||
"CmdConstitutionAccountApplies": "Hiến chương hồ sơ tài khoản: áp dụng (đã đăng nhập; Engine tải mỗi lượt)",
|
||||
"CmdConstitutionAccountSignedOut": "Hiến chương hồ sơ tài khoản: chưa đăng nhập",
|
||||
"CmdConstitutionAccountCheckFailed": "Hiến chương hồ sơ tài khoản: kiểm tra tài khoản thất bại ({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "một dự án, hồ sơ, môi trường, config được quản lý hoặc yêu cầu của tổ chức kiểm soát giá trị này",
|
||||
"ConfigHintManagedAllowShell": "một dự án, hồ sơ, môi trường hoặc config được quản lý kiểm soát quyền truy cập shell",
|
||||
"ConfigHintAllowShell": "bật sẽ cung cấp công cụ shell ở chế độ Tác nhân; quy tắc quyền vẫn áp dụng",
|
||||
"ConfigHintComposerMultilineMode": "tắt: Enter gửi, Shift+Enter thêm dòng; bật: Enter thêm dòng, Shift+Enter gửi",
|
||||
"ConfigHintComposerMultilineMode": "tắt: Enter gửi, Alt+Enter / Shift+Enter thêm dòng; bật: Enter thêm dòng, Alt+Enter / Shift+Enter gửi",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off; thay đổi chính xác vẫn ở chi tiết Alt/Option+V",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "Tiếp tục lập kế hoạch",
|
||||
"PlanHandoffKeepPlanningDetail": "Ở lại Plan. Không có gì thay đổi.",
|
||||
"PlanHandoffProceed": "Hãy làm theo kế hoạch.",
|
||||
"PlanHandoffTodoNote": "Theo dõi kế hoạch này bằng todo_write: giữ mục \"{title}\" trong danh sách của bạn và đánh dấu đã hoàn tất khi kế hoạch xong.",
|
||||
"PetAppearanceLegacy": "Thú cưng đang dùng phiên bản cũ. Khởi động lại sau khi cập nhật Codewhale để chọn kiểu cá voi và ngôn ngữ văn bản.",
|
||||
"PluginOAuthBrowser": "Nhấn Enter để đăng nhập bằng trình duyệt hoặc Esc để quay lại. Không cần khóa API.",
|
||||
"PluginOAuthReady": "{provider}: đã đăng nhập. Hãy chọn mô hình.",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "Chính sách sandbox cho lần chạy này; mọi --sandbox khác danger-full-access (hoặc sandbox_mode tương đương trong cấu hình) đều tắt chế độ tin cậy.",
|
||||
"ExecAfterHelp": "Ví dụ:\n codewhale exec \"giải thích hàm này\"\n codewhale exec --auto \"liệt kê crates/ bằng ls\"\n codewhale exec --auto --output-format stream-json \"sửa bài kiểm thử đang lỗi\"\n\n`codewhale exec` thông thường tạo một câu trả lời của mô hình: một lượt Engine\nvới cùng lời nhắc hệ thống như các lần chạy khác và không có công cụ. Dùng\n`--auto` để chạy tác nhân có công cụ mà không cần tương tác. Công cụ chỉ được\ncung cấp khi có `--auto`, `--yolo`, `--allowed-tools` hoặc khi tiếp tục phiên;\ncác giới hạn như `--max-turns`, `--disallowed-tools`, `--sandbox` và định dạng\nđầu ra không bao giờ thêm công cụ. Câu trả lời bị ngắt do giới hạn đầu ra của\nnhà cung cấp được tiếp tục trong cùng lượt, tối đa 8 bước mô hình trừ khi\n`--max-turns` quy định khác.\n--auto mặc định dùng Full Access: không có sandbox shell và bật chế độ tin cậy. Dùng --sandbox workspace-write để duy trì sandbox shell và giới hạn công cụ tệp trong không gian làm việc; các chính sách chặt chẽ hơn đã cấu hình cũng được áp dụng. Thử lại thao tác bị sandbox từ chối cần --allow-sandbox-elevation hoặc --sandbox danger-full-access.\nCác lần chạy có cung cấp công cụ, lần chạy stream-json và phiên được tiếp tục đều được lưu để dùng với codewhale sessions, exec --continue và codewhale receipts <id>. Các lần chạy text/--json thông thường chỉ trả lời sẽ không được lưu, trừ khi việc nén ngữ cảnh cần dữ liệu khôi phục.\nMọi --sandbox khác danger-full-access (hoặc sandbox_mode tương đương trong cấu hình) đều tắt chế độ tin cậy.\n",
|
||||
"ExecPromptHelp": "Lời nhắc gửi đến mô hình. Đặt -- trước lời nhắc bắt đầu bằng - hoặc chứa cờ exec dưới dạng văn bản; dùng --prompt-file - để đọc đầu vào chuẩn.",
|
||||
"ExecFlagAfterPrompt": "{flag} nằm sau lời nhắc và sẽ bị xem là văn bản. Hãy đặt cờ trước lời nhắc hoặc đặt cờ cần dùng dưới dạng văn bản sau --."
|
||||
"ExecFlagAfterPrompt": "{flag} nằm sau lời nhắc và sẽ bị xem là văn bản. Hãy đặt cờ trước lời nhắc hoặc đặt cờ cần dùng dưới dạng văn bản sau --.",
|
||||
"InstructionNoticeHeader": "Các chỉ dẫn thường trực đã bị cắt bớt hoặc bỏ qua trước khi phiên này bắt đầu:",
|
||||
"InstructionNoticeFooter": "Chạy /context report để xem chi tiết đầy đủ theo từng nguồn.",
|
||||
"InstructionNoticeMore": "…và {count} mục nữa; danh sách đầy đủ nằm trong /context report.",
|
||||
"InstructionNoticeLine": "chỉ dẫn: {scope} {label} ({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " giữ {kept_bytes}/{total_bytes} byte và ~{kept_tokens}/~{total_tokens} token ước tính",
|
||||
"InstructionNoticeDroppedBytes": " đã bỏ {bytes} byte",
|
||||
"InstructionNoticeFallback": "; thay thế: {source}",
|
||||
"InstructionNoticeStaticGuardOver": "chỉ dẫn: tiền tố tĩnh vẫn vượt giới hạn tiền tố tĩnh ~{tokens} token ước tính sau khi cắt chỉ dẫn; tiếp tục theo chính sách `trim` — hãy tăng static_prefix_guard_percent hoặc thu hẹp bề mặt công cụ.",
|
||||
"InstructionNoticeStaticGuardTrimmed": "chỉ dẫn: phạm vi {scope} bị cắt ~{tokens} token ước tính để giữ tiền tố tĩnh dưới giới hạn",
|
||||
"InstructionDetailDroppedBroadest": "bị bỏ vì là đoạn thuộc phạm vi rộng nhất",
|
||||
"InstructionDetailNoRoom": "ngân sách phạm vi {tokens} token ước tính không còn chỗ cho nội dung",
|
||||
"InstructionDetailRulesExhausted": "ngân sách phạm vi dự án {tokens} token ước tính đã bị chỉ dẫn dùng hết",
|
||||
"InstructionDetailOverCeiling": "lớn hơn mức tối đa {bytes} byte",
|
||||
"InstructionDetailUnreadable": "không đọc được: {reason}",
|
||||
"InstructionDetailAlreadyLoaded": "đã được nạp từ phạm vi rộng hơn",
|
||||
"InstructionDetailRulesCap": "vượt giới hạn {count} tệp quy tắc mỗi thư mục",
|
||||
"InstructionDetailSymlinkedRules": "từ chối thư mục quy tắc là liên kết tượng trưng",
|
||||
"InstructionBudgetRefused": "ngân sách chỉ dẫn đã từ chối yêu cầu này: {reason}",
|
||||
"InstructionBudgetOverGuard": "tiền tố tĩnh ~{measured} token ước tính vượt giới hạn {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "仅有推理 {attempt}/{limit};没有回答或工具调用;{action}",
|
||||
"OutputLimitAnswerRetryBody": "达到输出上限 {attempt}/{limit};没有回答或工具调用;{action}",
|
||||
"AnswerRetryActionBare": "重新请求回答",
|
||||
"AnswerRetryActionNudge": "附带提示后重新请求回答",
|
||||
"AnswerRetryCounter": "仅有推理/输出上限",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · 推荐",
|
||||
"OrcaRouterOfferAction": "在 /provider 或 /model 中查看 OrcaRouter 优惠",
|
||||
"OrcaRouterOfferBody": "通过 Codewhale × OrcaRouter 优惠,充值可额外获得 30% 的 DeepSeek V4.1 Flash 额度。最新条款请查看优惠页面。",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale 会从 OrcaRouter 获得推荐佣金。",
|
||||
"ShellWaitNoActive": "当前没有正在等待的 shell 命令。",
|
||||
"ShellWaitInteractive": "交互式命令无法在后台继续运行。请取消当前回合,或等待命令完成。",
|
||||
"ShellWaitUnavailable": "此工具无法在后台继续运行。请取消当前回合,或等待它完成。",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ 先保存 · /provider 连接",
|
||||
"ComposerHintSendNow": "↵ 立即发送",
|
||||
"ComposerHintSendIntoTurn": "↵ 发进本回合",
|
||||
"ComposerHintLongDraft": "{shortcut}:外部编辑器",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ 正在发进本回合: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ 留待下一回合: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ 正在编辑后续消息: ",
|
||||
@@ -467,6 +477,9 @@
|
||||
"CmdImportClaudeDescription": "生成可审阅的 Claude Code 设置与 MCP 服务器导入计划",
|
||||
"CmdClearDescription": "清除对话历史",
|
||||
"CmdColorDescription": "设置此对话输入框的颜色",
|
||||
"CmdColorSet": "此对话输入框的颜色:{color}",
|
||||
"CmdColorReset": "此对话输入框的颜色已重置为主题默认值",
|
||||
"CmdColorUnknown": "未知颜色。可选:{colors},或 off。",
|
||||
"CmdCompactDescription": "触发上下文压缩以释放空间",
|
||||
"CmdPurgeDescription": "让 Agent 分析对话历史,精确保留有用信息并移除冗余内容",
|
||||
"CmdConfigDescription": "查看与修改设置(原始键在高级详情)",
|
||||
@@ -812,7 +825,7 @@
|
||||
"KbSendDraft": "空闲时发送,忙碌时排队;输入框为空时立即发送下一条排队消息",
|
||||
"KbSteerCurrentTurn": "调整当前回合(空闲时正常发送)",
|
||||
"KbCloseMenu": "关闭菜单、取消请求、丢弃草稿或清空输入",
|
||||
"KbCancelOrExit": "取消请求,或空闲时退出",
|
||||
"KbCancelOrExit": "取消请求、清空草稿,或空闲时退出",
|
||||
"KbShellControls": "将正在等待的 shell 移到 /jobs",
|
||||
"KbExitEmpty": "输入框为空时退出",
|
||||
"KbCommandPalette": "打开命令面板",
|
||||
@@ -1280,6 +1293,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ 回看 · {details} 详情 · Esc 停止",
|
||||
"ApprovalOwnerHeader": "智能体:{agent} · {role}",
|
||||
"ApprovalGoToAgent": "g 前往智能体",
|
||||
"UserInputActionRequired": "需要你操作",
|
||||
"UserInputQuestionProgress": "问题 {current}/{total}",
|
||||
"UserInputOtherLabel": "其他",
|
||||
"UserInputOtherDescription": "输入自定义回复",
|
||||
"UserInputConfirmLabel": "确认所选",
|
||||
"UserInputConfirmDescription": "提交已选 {count} 项",
|
||||
"UserInputCustomPrompt": "自定义回复:",
|
||||
"UserInputCustomPlaceholder": "(输入你的回复)",
|
||||
"UserInputHintSubmit": "提交",
|
||||
"UserInputHintBack": "返回",
|
||||
"UserInputHintMove": "移动",
|
||||
"UserInputHintToggle": "切换",
|
||||
"UserInputHintSelectConfirm": "选择/确认",
|
||||
"UserInputHintCancel": "取消",
|
||||
"UserInputHintQuickPick": "快速选择",
|
||||
"UserInputHintConfirm": "确认",
|
||||
"UserInputHintDigit": "数字",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ 回看 · {details} 详情 · Esc 隐藏",
|
||||
"PendingApprovalInAgent": "{agent} 需要批准 — /agents",
|
||||
"ApprovalTruncationHint": " … 已截断 · 按 {details} 查看完整内容",
|
||||
@@ -1376,6 +1406,8 @@
|
||||
"ToolReceiptLinesSingular": "1 行",
|
||||
"ToolReceiptLinesPlural": "{count} 行",
|
||||
"ToolDescriptionNote": "说明",
|
||||
"ToolStatusWaitingForApproval": "等待审批",
|
||||
"ToolStatusQueued": "已排队",
|
||||
"CmdConstitutionAccountApplies": "账户资料宪章:生效(已登录;Engine 在每轮加载)",
|
||||
"CmdConstitutionAccountSignedOut": "账户资料宪章:未登录",
|
||||
"CmdConstitutionAccountCheckFailed": "账户资料宪章:账户检查失败({error})",
|
||||
@@ -2339,7 +2371,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "项目、配置文件、环境、托管配置或组织要求控制此值",
|
||||
"ConfigHintManagedAllowShell": "项目、配置文件、环境或托管配置控制 shell 访问",
|
||||
"ConfigHintAllowShell": "开启后在 Work 模式中提供 shell 工具;权限规则仍然适用",
|
||||
"ConfigHintComposerMultilineMode": "关:Enter 发送,Shift+Enter 换行;开:Enter 换行,Shift+Enter 发送",
|
||||
"ConfigHintComposerMultilineMode": "关:Enter 发送,Alt+Enter / Shift+Enter 换行;开:Enter 换行,Alt+Enter / Shift+Enter 发送",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off;精确更改仍在 Alt/Option+V 详情中",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "继续规划",
|
||||
"PlanHandoffKeepPlanningDetail": "留在 Plan。不做任何更改。",
|
||||
"PlanHandoffProceed": "按计划开始执行。",
|
||||
"PlanHandoffTodoNote": "使用 todo_write 跟踪此计划:在列表中保留条目「{title}」,并在计划完成后将其标记为已完成。",
|
||||
"PetAppearanceLegacy": "宠物仍在使用旧版本。更新 Codewhale 后重启宠物,即可选择鲸鱼样式和文字语言。",
|
||||
"PluginOAuthBrowser": "按 Enter 打开浏览器登录,按 Esc 返回。不需要 API Key。",
|
||||
"PluginOAuthReady": "{provider}:已登录,请选择模型。",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "本次运行的沙盒策略;danger-full-access 以外的任何 --sandbox(或配置中等效的 sandbox_mode)都会关闭信任模式。",
|
||||
"ExecAfterHelp": "示例:\n codewhale exec \"解释这个函数\"\n codewhale exec --auto \"用 ls 列出 crates/ 的内容\"\n codewhale exec --auto --output-format stream-json \"修复失败的测试\"\n\n普通的 `codewhale exec` 仅获取一次模型回答:使用与其他运行相同的系统提示词,\n执行一轮 Engine,不提供工具。使用 `--auto` 可运行非交互式工具代理。\n只有指定 `--auto`、`--yolo`、`--allowed-tools` 或恢复会话时才会提供工具;\n`--max-turns`、`--disallowed-tools`、`--sandbox` 等限制以及输出格式\n都不会添加工具。因提供商输出上限而中断的回答会在同一轮中继续,\n除非通过 `--max-turns` 另行指定,否则最多进行 8 个模型步骤。\n--auto 默认使用 Full Access:无 shell 沙盒,开启信任模式。使用 --sandbox workspace-write 可保留 shell 沙盒和文件工具的工作区限制;已配置的更严格策略也会生效。重试被沙盒拒绝的操作需要 --allow-sandbox-elevation 或 --sandbox danger-full-access。\n提供工具的运行、stream-json 运行以及恢复的会话都会保存,可通过 codewhale sessions、exec --continue 和 codewhale receipts <id> 使用。仅回答的普通 text/--json 运行不会保存,除非上下文压缩需要保留恢复数据。\ndanger-full-access 以外的任何 --sandbox(或配置中等效的 sandbox_mode)都会关闭信任模式。\n",
|
||||
"ExecPromptHelp": "发送给模型的提示词。若以 - 开头或包含作为文本的 exec 标志,请在提示词前加 --;使用 --prompt-file - 读取标准输入。",
|
||||
"ExecFlagAfterPrompt": "{flag} 位于提示词之后,会被当作文本。请将标志放在提示词之前,或将作为文本的标志放在 -- 之后。"
|
||||
"ExecFlagAfterPrompt": "{flag} 位于提示词之后,会被当作文本。请将标志放在提示词之前,或将作为文本的标志放在 -- 之后。",
|
||||
"InstructionNoticeHeader": "常驻指令在本会话开始前已被截断或跳过:",
|
||||
"InstructionNoticeFooter": "运行 /context report 查看按来源划分的完整明细。",
|
||||
"InstructionNoticeMore": "…另有 {count} 项;完整列表见 /context report。",
|
||||
"InstructionNoticeLine": "指令:{scope} {label}({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " 保留 {total_bytes} 字节中的 {kept_bytes} 字节,以及约 {total_tokens} 估算 token 中的约 {kept_tokens}",
|
||||
"InstructionNoticeDroppedBytes": " 丢弃 {bytes} 字节",
|
||||
"InstructionNoticeFallback": ";替代:{source}",
|
||||
"InstructionNoticeStaticGuardOver": "指令:截断指令后,静态前缀仍超出静态前缀上限约 {tokens} 估算 token;按策略 `trim` 继续——请调高 static_prefix_guard_percent 或收窄工具范围。",
|
||||
"InstructionNoticeStaticGuardTrimmed": "指令:{scope} 范围已截断约 {tokens} 估算 token,以使静态前缀保持在上限内",
|
||||
"InstructionDetailDroppedBroadest": "作为最宽范围的片段被丢弃",
|
||||
"InstructionDetailNoRoom": "{tokens} 估算 token 的范围预算已无空间容纳内容",
|
||||
"InstructionDetailRulesExhausted": "{tokens} 估算 token 的项目范围预算已被指令用尽",
|
||||
"InstructionDetailOverCeiling": "超过 {bytes} 字节上限",
|
||||
"InstructionDetailUnreadable": "无法读取:{reason}",
|
||||
"InstructionDetailAlreadyLoaded": "已从更宽范围加载",
|
||||
"InstructionDetailRulesCap": "超过每个目录 {count} 个规则文件的上限",
|
||||
"InstructionDetailSymlinkedRules": "拒绝符号链接的规则目录",
|
||||
"InstructionBudgetRefused": "指令预算拒绝了此请求:{reason}",
|
||||
"InstructionBudgetOverGuard": "静态前缀约 {measured} 估算 token 超出上限 {guard}"
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"ReasoningOnlyRetryBody": "僅有推理 {attempt}/{limit};沒有回答或工具呼叫;{action}",
|
||||
"OutputLimitAnswerRetryBody": "達到輸出上限 {attempt}/{limit};沒有回答或工具呼叫;{action}",
|
||||
"AnswerRetryActionBare": "重新請求回答",
|
||||
"AnswerRetryActionNudge": "附加提示後重新請求回答",
|
||||
"AnswerRetryCounter": "僅有推理/輸出上限",
|
||||
"OrcaRouterOfferRecommended": "OrcaRouter · 推薦",
|
||||
"OrcaRouterOfferAction": "在 /provider 或 /model 中查看 OrcaRouter 優惠",
|
||||
"OrcaRouterOfferBody": "透過 Codewhale × OrcaRouter 優惠,儲值可額外獲得 30% 的 DeepSeek V4.1 Flash 額度。最新條款請查看優惠頁面。",
|
||||
"OrcaRouterOfferDisclosure": "Codewhale 會從 OrcaRouter 獲得推薦佣金。",
|
||||
"ShellWaitNoActive": "目前沒有正在等待的 shell 命令。",
|
||||
"ShellWaitInteractive": "互動式命令無法在背景繼續執行。請取消目前回合,或等待命令完成。",
|
||||
"ShellWaitUnavailable": "此工具無法在背景繼續執行。請取消目前回合,或等待它完成。",
|
||||
@@ -185,6 +194,7 @@
|
||||
"ComposerHintOfflineConnect": "↵ 先儲存 · /provider 連線",
|
||||
"ComposerHintSendNow": "↵ 立即傳送",
|
||||
"ComposerHintSendIntoTurn": "↵ 送進本回合",
|
||||
"ComposerHintLongDraft": "{shortcut}:外部編輯器",
|
||||
"PendingSendingIntoTurnPrefix": " ↳ 正在送進本回合: ",
|
||||
"PendingCouldNotSendIntoTurnPrefix": " ↳ 留待下一回合: ",
|
||||
"PendingEditingFollowUpPrefix": " ↳ 正在編輯後續訊息: ",
|
||||
@@ -465,6 +475,9 @@
|
||||
"CmdImportClaudeDescription": "產生可審閱的 Claude Code 設定與 MCP 伺服器匯入計畫",
|
||||
"CmdClearDescription": "清除對話歷史",
|
||||
"CmdColorDescription": "設定此對話輸入框的顏色",
|
||||
"CmdColorSet": "此對話輸入框的顏色:{color}",
|
||||
"CmdColorReset": "此對話輸入框的顏色已重設為主題預設值",
|
||||
"CmdColorUnknown": "未知顏色。可選:{colors},或 off。",
|
||||
"CmdCompactDescription": "觸發上下文壓縮以釋放空間",
|
||||
"CmdPurgeDescription": "讓 Agent 分析對話歷史,精確保留有用資訊並移除冗餘內容",
|
||||
"CmdConfigDescription": "檢視與修改設定(原始鍵在高級詳情)",
|
||||
@@ -810,7 +823,7 @@
|
||||
"KbSendDraft": "閒置時傳送,忙碌時排隊;輸入框為空時立即傳送下一條排隊訊息",
|
||||
"KbSteerCurrentTurn": "調整目前回合(閒置時正常傳送)",
|
||||
"KbCloseMenu": "關閉選單、取消請求、丟棄草稿或清空輸入",
|
||||
"KbCancelOrExit": "取消請求,或閒置時退出",
|
||||
"KbCancelOrExit": "取消請求、清空草稿,或閒置時退出",
|
||||
"KbShellControls": "將正在等待的 shell 移到 /jobs",
|
||||
"KbExitEmpty": "輸入框為空時退出",
|
||||
"KbCommandPalette": "開啟指令面板",
|
||||
@@ -1278,6 +1291,23 @@
|
||||
"ApprovalControlsHint": "Pg↑/↓ 回看 · {details} 詳情 · Esc 停止",
|
||||
"ApprovalOwnerHeader": "代理:{agent} · {role}",
|
||||
"ApprovalGoToAgent": "g 前往代理",
|
||||
"UserInputActionRequired": "需要你操作",
|
||||
"UserInputQuestionProgress": "問題 {current}/{total}",
|
||||
"UserInputOtherLabel": "其他",
|
||||
"UserInputOtherDescription": "輸入自訂回覆",
|
||||
"UserInputConfirmLabel": "確認所選",
|
||||
"UserInputConfirmDescription": "提交已選 {count} 項",
|
||||
"UserInputCustomPrompt": "自訂回覆:",
|
||||
"UserInputCustomPlaceholder": "(輸入你的回覆)",
|
||||
"UserInputHintSubmit": "提交",
|
||||
"UserInputHintBack": "返回",
|
||||
"UserInputHintMove": "移動",
|
||||
"UserInputHintToggle": "切換",
|
||||
"UserInputHintSelectConfirm": "選擇/確認",
|
||||
"UserInputHintCancel": "取消",
|
||||
"UserInputHintQuickPick": "快速選擇",
|
||||
"UserInputHintConfirm": "確認",
|
||||
"UserInputHintDigit": "數字",
|
||||
"ApprovalControlsHintChild": "Pg↑/↓ 回看 · {details} 詳情 · Esc 隱藏",
|
||||
"PendingApprovalInAgent": "{agent} 需要核准 — /agents",
|
||||
"ApprovalTruncationHint": " … 已截斷 · 按 {details} 查看完整內容",
|
||||
@@ -1374,6 +1404,8 @@
|
||||
"ToolReceiptLinesSingular": "1 行",
|
||||
"ToolReceiptLinesPlural": "{count} 行",
|
||||
"ToolDescriptionNote": "說明",
|
||||
"ToolStatusWaitingForApproval": "等待核准",
|
||||
"ToolStatusQueued": "已排隊",
|
||||
"CmdConstitutionAccountApplies": "帳戶資料憲章:生效(已登入;Engine 在每輪載入)",
|
||||
"CmdConstitutionAccountSignedOut": "帳戶資料憲章:未登入",
|
||||
"CmdConstitutionAccountCheckFailed": "帳戶資料憲章:帳戶檢查失敗({error})",
|
||||
@@ -2337,7 +2369,7 @@
|
||||
"ConfigHintManagedApprovalPolicy": "專案、設定檔、環境、受管設定或組織要求控制此值",
|
||||
"ConfigHintManagedAllowShell": "專案、設定檔、環境或受管設定控制 shell 存取",
|
||||
"ConfigHintAllowShell": "開啟後在 Work 模式中提供 shell 工具;權限規則仍然適用",
|
||||
"ConfigHintComposerMultilineMode": "關:Enter 送出,Shift+Enter 換行;開:Enter 換行,Shift+Enter 送出",
|
||||
"ConfigHintComposerMultilineMode": "關:Enter 送出,Alt+Enter / Shift+Enter 換行;開:Enter 換行,Alt+Enter / Shift+Enter 送出",
|
||||
"ConfigHintBooleanValues": "on/off, true/false, yes/no, 1/0",
|
||||
"ConfigHintDensity": "compact | comfortable | spacious",
|
||||
"ConfigHintInlineDiffs": "full | summary | off;精確變更仍在 Alt/Option+V 詳情中",
|
||||
@@ -2571,6 +2603,7 @@
|
||||
"PlanHandoffKeepPlanning": "繼續規劃",
|
||||
"PlanHandoffKeepPlanningDetail": "留在 Plan。不做任何變更。",
|
||||
"PlanHandoffProceed": "依計畫開始執行。",
|
||||
"PlanHandoffTodoNote": "使用 todo_write 追蹤此計畫:在清單中保留項目「{title}」,並在計畫完成後將其標記為已完成。",
|
||||
"PetAppearanceLegacy": "寵物仍在使用舊版本。更新 Codewhale 後重新啟動寵物,即可選擇鯨魚樣式和文字語言。",
|
||||
"PluginOAuthBrowser": "按 Enter 開啟瀏覽器登入,按 Esc 返回。不需要 API Key。",
|
||||
"PluginOAuthReady": "{provider}:已登入,請選擇模型。",
|
||||
@@ -2581,5 +2614,24 @@
|
||||
"ExecSandboxHelp": "本次執行的沙盒原則;danger-full-access 以外的任何 --sandbox(或設定中等效的 sandbox_mode)都會關閉信任模式。",
|
||||
"ExecAfterHelp": "範例:\n codewhale exec \"解釋這個函式\"\n codewhale exec --auto \"用 ls 列出 crates/ 的內容\"\n codewhale exec --auto --output-format stream-json \"修正失敗的測試\"\n\n一般的 `codewhale exec` 僅取得一次模型回覆:使用與其他執行相同的系統提示詞,\n執行一輪 Engine,不提供工具。使用 `--auto` 可執行非互動式工具代理。\n只有指定 `--auto`、`--yolo`、`--allowed-tools` 或還原工作階段時才會提供工具;\n`--max-turns`、`--disallowed-tools`、`--sandbox` 等限制及輸出格式\n都不會新增工具。因供應商輸出上限而中斷的回覆會在同一輪中繼續,\n除非透過 `--max-turns` 另行指定,否則最多進行 8 個模型步驟。\n--auto 預設使用 Full Access:無 shell 沙盒,開啟信任模式。使用 --sandbox workspace-write 可保留 shell 沙盒與檔案工具的工作區限制;已設定的更嚴格政策也會生效。重試遭沙盒拒絕的操作需要 --allow-sandbox-elevation 或 --sandbox danger-full-access。\n提供工具的執行、stream-json 執行及還原的工作階段都會儲存,可透過 codewhale sessions、exec --continue 與 codewhale receipts <id> 使用。僅回覆的一般 text/--json 執行不會儲存,除非內容壓縮需要保留復原資料。\ndanger-full-access 以外的任何 --sandbox(或設定中等效的 sandbox_mode)都會關閉信任模式。\n",
|
||||
"ExecPromptHelp": "傳送給模型的提示詞。若以 - 開頭或包含作為文字的 exec 旗標,請在提示詞前加 --;使用 --prompt-file - 讀取標準輸入。",
|
||||
"ExecFlagAfterPrompt": "{flag} 位於提示詞之後,會被視為文字。請將旗標放在提示詞之前,或將作為文字的旗標放在 -- 之後。"
|
||||
"ExecFlagAfterPrompt": "{flag} 位於提示詞之後,會被視為文字。請將旗標放在提示詞之前,或將作為文字的旗標放在 -- 之後。",
|
||||
"InstructionNoticeHeader": "常駐指令在此工作階段開始前已被截斷或略過:",
|
||||
"InstructionNoticeFooter": "執行 /context report 以查看依來源劃分的完整明細。",
|
||||
"InstructionNoticeMore": "…另有 {count} 項;完整清單見 /context report。",
|
||||
"InstructionNoticeLine": "指令:{scope} {label}({origin}, {action}, {limit})",
|
||||
"InstructionNoticeTrimmed": " 保留 {total_bytes} 位元組中的 {kept_bytes} 位元組,以及約 {total_tokens} 估算 token 中的約 {kept_tokens}",
|
||||
"InstructionNoticeDroppedBytes": " 捨棄 {bytes} 位元組",
|
||||
"InstructionNoticeFallback": ";替代:{source}",
|
||||
"InstructionNoticeStaticGuardOver": "指令:截斷指令後,靜態前綴仍超出靜態前綴上限約 {tokens} 估算 token;依政策 `trim` 繼續——請調高 static_prefix_guard_percent 或縮小工具範圍。",
|
||||
"InstructionNoticeStaticGuardTrimmed": "指令:{scope} 範圍已截斷約 {tokens} 估算 token,以使靜態前綴維持在上限內",
|
||||
"InstructionDetailDroppedBroadest": "作為最廣範圍的片段被捨棄",
|
||||
"InstructionDetailNoRoom": "{tokens} 估算 token 的範圍預算已無空間容納內容",
|
||||
"InstructionDetailRulesExhausted": "{tokens} 估算 token 的專案範圍預算已被指令用盡",
|
||||
"InstructionDetailOverCeiling": "超過 {bytes} 位元組上限",
|
||||
"InstructionDetailUnreadable": "無法讀取:{reason}",
|
||||
"InstructionDetailAlreadyLoaded": "已從更廣範圍載入",
|
||||
"InstructionDetailRulesCap": "超過每個目錄 {count} 個規則檔案的上限",
|
||||
"InstructionDetailSymlinkedRules": "拒絕符號連結的規則目錄",
|
||||
"InstructionBudgetRefused": "指令預算拒絕了此請求:{reason}",
|
||||
"InstructionBudgetOverGuard": "靜態前綴約 {measured} 估算 token 超出上限 {guard}"
|
||||
}
|
||||
@@ -120,6 +120,17 @@ impl Locale {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MessageId {
|
||||
ReasoningOnlyRetryBody,
|
||||
OutputLimitAnswerRetryBody,
|
||||
AnswerRetryActionBare,
|
||||
AnswerRetryActionNudge,
|
||||
AnswerRetryCounter,
|
||||
|
||||
OrcaRouterOfferRecommended,
|
||||
OrcaRouterOfferAction,
|
||||
OrcaRouterOfferBody,
|
||||
OrcaRouterOfferDisclosure,
|
||||
|
||||
MobileStreamReplayFailed,
|
||||
MobileStreamCatchUpFailed,
|
||||
MobileStreamRuntimeShutdown,
|
||||
@@ -474,6 +485,9 @@ pub enum MessageId {
|
||||
CmdImportClaudeDescription,
|
||||
CmdClearDescription,
|
||||
CmdColorDescription,
|
||||
CmdColorSet,
|
||||
CmdColorReset,
|
||||
CmdColorUnknown,
|
||||
CmdCompactDescription,
|
||||
CmdPurgeDescription,
|
||||
CmdConfigDescription,
|
||||
@@ -1528,6 +1542,23 @@ pub enum MessageId {
|
||||
ApprovalControlsHint,
|
||||
ApprovalOwnerHeader,
|
||||
ApprovalGoToAgent,
|
||||
UserInputActionRequired,
|
||||
UserInputQuestionProgress,
|
||||
UserInputOtherLabel,
|
||||
UserInputOtherDescription,
|
||||
UserInputConfirmLabel,
|
||||
UserInputConfirmDescription,
|
||||
UserInputCustomPrompt,
|
||||
UserInputCustomPlaceholder,
|
||||
UserInputHintSubmit,
|
||||
UserInputHintBack,
|
||||
UserInputHintMove,
|
||||
UserInputHintToggle,
|
||||
UserInputHintSelectConfirm,
|
||||
UserInputHintCancel,
|
||||
UserInputHintQuickPick,
|
||||
UserInputHintConfirm,
|
||||
UserInputHintDigit,
|
||||
ApprovalControlsHintChild,
|
||||
PendingApprovalInAgent,
|
||||
ApprovalTruncationHint,
|
||||
@@ -1629,6 +1660,8 @@ pub enum MessageId {
|
||||
ToolReceiptLinesSingular,
|
||||
ToolReceiptLinesPlural,
|
||||
ToolDescriptionNote,
|
||||
ToolStatusWaitingForApproval,
|
||||
ToolStatusQueued,
|
||||
CmdConstitutionAccountApplies,
|
||||
CmdConstitutionAccountSignedOut,
|
||||
CmdConstitutionAccountCheckFailed,
|
||||
@@ -2590,6 +2623,7 @@ pub enum MessageId {
|
||||
ComposerHintOfflineConnect,
|
||||
ComposerHintSendNow,
|
||||
ComposerHintSendIntoTurn,
|
||||
ComposerHintLongDraft,
|
||||
PendingSendingIntoTurnPrefix,
|
||||
PendingCouldNotSendIntoTurnPrefix,
|
||||
PendingEditingFollowUpPrefix,
|
||||
@@ -2816,6 +2850,26 @@ pub enum MessageId {
|
||||
PluginOAuthReady,
|
||||
PluginOAuthLocalLogout,
|
||||
PlanHandoffProceed,
|
||||
PlanHandoffTodoNote,
|
||||
InstructionNoticeHeader,
|
||||
InstructionNoticeFooter,
|
||||
InstructionNoticeMore,
|
||||
InstructionNoticeLine,
|
||||
InstructionNoticeTrimmed,
|
||||
InstructionNoticeDroppedBytes,
|
||||
InstructionNoticeFallback,
|
||||
InstructionNoticeStaticGuardOver,
|
||||
InstructionNoticeStaticGuardTrimmed,
|
||||
InstructionDetailDroppedBroadest,
|
||||
InstructionDetailNoRoom,
|
||||
InstructionDetailRulesExhausted,
|
||||
InstructionDetailOverCeiling,
|
||||
InstructionDetailUnreadable,
|
||||
InstructionDetailAlreadyLoaded,
|
||||
InstructionDetailRulesCap,
|
||||
InstructionDetailSymlinkedRules,
|
||||
InstructionBudgetRefused,
|
||||
InstructionBudgetOverGuard,
|
||||
ExecAutoHelp,
|
||||
ExecSandboxHelp,
|
||||
ExecAfterHelp,
|
||||
@@ -2823,8 +2877,16 @@ pub enum MessageId {
|
||||
ExecFlagAfterPrompt,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ReasoningOnlyRetryBody,
|
||||
MessageId::OutputLimitAnswerRetryBody,
|
||||
MessageId::AnswerRetryActionBare,
|
||||
MessageId::AnswerRetryActionNudge,
|
||||
MessageId::AnswerRetryCounter,
|
||||
MessageId::OrcaRouterOfferRecommended,
|
||||
MessageId::OrcaRouterOfferAction,
|
||||
MessageId::OrcaRouterOfferBody,
|
||||
MessageId::OrcaRouterOfferDisclosure,
|
||||
MessageId::MobileStreamReplayFailed,
|
||||
MessageId::MobileStreamCatchUpFailed,
|
||||
MessageId::MobileStreamRuntimeShutdown,
|
||||
@@ -3171,6 +3233,9 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::CmdTurnInspectDescription,
|
||||
MessageId::CmdClearDescription,
|
||||
MessageId::CmdColorDescription,
|
||||
MessageId::CmdColorSet,
|
||||
MessageId::CmdColorReset,
|
||||
MessageId::CmdColorUnknown,
|
||||
MessageId::CmdCompactDescription,
|
||||
MessageId::CmdPurgeDescription,
|
||||
MessageId::CmdConfigDescription,
|
||||
@@ -4192,6 +4257,23 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ApprovalControlsHint,
|
||||
MessageId::ApprovalOwnerHeader,
|
||||
MessageId::ApprovalGoToAgent,
|
||||
MessageId::UserInputActionRequired,
|
||||
MessageId::UserInputQuestionProgress,
|
||||
MessageId::UserInputOtherLabel,
|
||||
MessageId::UserInputOtherDescription,
|
||||
MessageId::UserInputConfirmLabel,
|
||||
MessageId::UserInputConfirmDescription,
|
||||
MessageId::UserInputCustomPrompt,
|
||||
MessageId::UserInputCustomPlaceholder,
|
||||
MessageId::UserInputHintSubmit,
|
||||
MessageId::UserInputHintBack,
|
||||
MessageId::UserInputHintMove,
|
||||
MessageId::UserInputHintToggle,
|
||||
MessageId::UserInputHintSelectConfirm,
|
||||
MessageId::UserInputHintCancel,
|
||||
MessageId::UserInputHintQuickPick,
|
||||
MessageId::UserInputHintConfirm,
|
||||
MessageId::UserInputHintDigit,
|
||||
MessageId::ApprovalControlsHintChild,
|
||||
MessageId::PendingApprovalInAgent,
|
||||
MessageId::ApprovalTruncationHint,
|
||||
@@ -4288,6 +4370,8 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ToolReceiptLinesSingular,
|
||||
MessageId::ToolReceiptLinesPlural,
|
||||
MessageId::ToolDescriptionNote,
|
||||
MessageId::ToolStatusWaitingForApproval,
|
||||
MessageId::ToolStatusQueued,
|
||||
MessageId::CmdConstitutionAccountApplies,
|
||||
MessageId::CmdConstitutionAccountSignedOut,
|
||||
MessageId::CmdConstitutionAccountCheckFailed,
|
||||
@@ -5185,6 +5269,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ComposerHintOfflineConnect,
|
||||
MessageId::ComposerHintSendNow,
|
||||
MessageId::ComposerHintSendIntoTurn,
|
||||
MessageId::ComposerHintLongDraft,
|
||||
MessageId::PendingSendingIntoTurnPrefix,
|
||||
MessageId::PendingCouldNotSendIntoTurnPrefix,
|
||||
MessageId::PendingEditingFollowUpPrefix,
|
||||
@@ -5404,6 +5489,26 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::PluginOAuthReady,
|
||||
MessageId::PluginOAuthLocalLogout,
|
||||
MessageId::PlanHandoffProceed,
|
||||
MessageId::PlanHandoffTodoNote,
|
||||
MessageId::InstructionNoticeHeader,
|
||||
MessageId::InstructionNoticeFooter,
|
||||
MessageId::InstructionNoticeMore,
|
||||
MessageId::InstructionNoticeLine,
|
||||
MessageId::InstructionNoticeTrimmed,
|
||||
MessageId::InstructionNoticeDroppedBytes,
|
||||
MessageId::InstructionNoticeFallback,
|
||||
MessageId::InstructionNoticeStaticGuardOver,
|
||||
MessageId::InstructionNoticeStaticGuardTrimmed,
|
||||
MessageId::InstructionDetailDroppedBroadest,
|
||||
MessageId::InstructionDetailNoRoom,
|
||||
MessageId::InstructionDetailRulesExhausted,
|
||||
MessageId::InstructionDetailOverCeiling,
|
||||
MessageId::InstructionDetailUnreadable,
|
||||
MessageId::InstructionDetailAlreadyLoaded,
|
||||
MessageId::InstructionDetailRulesCap,
|
||||
MessageId::InstructionDetailSymlinkedRules,
|
||||
MessageId::InstructionBudgetRefused,
|
||||
MessageId::InstructionBudgetOverGuard,
|
||||
MessageId::ExecAutoHelp,
|
||||
MessageId::ExecSandboxHelp,
|
||||
MessageId::ExecAfterHelp,
|
||||
@@ -5585,7 +5690,6 @@ where
|
||||
Locale::En
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn truncate_to_width(text: &str, max_width: usize) -> String {
|
||||
if max_width == 0 {
|
||||
return String::new();
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "codewhale-lsp"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Codewhale language server client and diagnostics"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = ["codewhale-runtime/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
codewhale-config = { path = "../config", version = "0.10.2" }
|
||||
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
|
||||
codewhale-util = { path = "../util", version = "0.10.2" }
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc.workspace = true
|
||||
@@ -39,7 +39,7 @@ use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::diagnostics::{Diagnostic, Severity};
|
||||
use crate::utils::spawn_supervised;
|
||||
use codewhale_util::utils::spawn_supervised;
|
||||
|
||||
const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const MAX_LSP_HEADER_BYTES: usize = 8 * 1024;
|
||||
@@ -692,7 +692,7 @@ fn parse_publish_diagnostics(value: &Value) -> Option<DiagnosticMessage> {
|
||||
}
|
||||
|
||||
/// Encode an absolute filesystem path without following its links.
|
||||
pub(crate) fn uri_from_path(path: &Path) -> String {
|
||||
pub fn uri_from_path(path: &Path) -> String {
|
||||
reqwest::Url::from_file_path(path)
|
||||
.map(|url| url.to_string())
|
||||
.unwrap_or_default()
|
||||
@@ -715,7 +715,8 @@ pub(super) fn path_from_uri(uri: &str) -> Option<PathBuf> {
|
||||
url.to_file_path().ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[cfg_attr(not(test), allow(dead_code, unused_imports))]
|
||||
pub(super) mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -968,7 +969,7 @@ pub(super) mod tests {
|
||||
assert!(parse_publish_diagnostics(&payload).is_none());
|
||||
}
|
||||
#[cfg(unix)]
|
||||
pub(crate) const STDIO_FIXTURE: &str = r#"
|
||||
pub const STDIO_FIXTURE: &str = r#"
|
||||
import json, os, select, sys, time
|
||||
mode, pid_path = sys.argv[1:]
|
||||
input_stream = os.fdopen(0, 'rb', buffering=0)
|
||||
@@ -1128,7 +1129,7 @@ while True:
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn language_server_does_not_inherit_parent_secret_env() {
|
||||
use crate::test_support::{EnvVarGuard, lock_test_env};
|
||||
use codewhale_runtime::test_support::{EnvVarGuard, lock_test_env};
|
||||
let _env_lock = lock_test_env();
|
||||
let _secret = EnvVarGuard::set("CODEWHALE_TEST_LSP_SECRET", "lsp-secret-value");
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
File renamed without changes.
@@ -112,7 +112,7 @@ impl Default for LspConfig {
|
||||
impl LspConfig {
|
||||
/// Resolve `(command, args)` for `lang`. User-supplied overrides take
|
||||
/// precedence over the built-in registry.
|
||||
pub(crate) fn resolve_command(&self, lang: Language) -> Option<(String, Vec<String>)> {
|
||||
pub fn resolve_command(&self, lang: Language) -> Option<(String, Vec<String>)> {
|
||||
if let Some(parts) = self.servers.get(lang.as_key())
|
||||
&& let Some((first, rest)) = parts.split_first()
|
||||
{
|
||||
@@ -153,32 +153,32 @@ pub struct LspManager {
|
||||
/// best-effort post-edit hook, this preserves the distinction between an
|
||||
/// honest empty result, a server/read error, and a timeout.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LintReadResult {
|
||||
pub(crate) file: PathBuf,
|
||||
pub(crate) status: LintReadStatus,
|
||||
pub(crate) items: Vec<Diagnostic>,
|
||||
pub(crate) freshness: DiagnosticFreshness,
|
||||
pub struct LintReadResult {
|
||||
pub file: PathBuf,
|
||||
pub status: LintReadStatus,
|
||||
pub items: Vec<Diagnostic>,
|
||||
pub freshness: DiagnosticFreshness,
|
||||
/// Number of diagnostics after severity selection but before the
|
||||
/// configured per-file cap was applied. Unknown when the request did not
|
||||
/// complete successfully.
|
||||
pub(crate) total_diagnostic_count: Option<usize>,
|
||||
pub(crate) truncated: bool,
|
||||
pub total_diagnostic_count: Option<usize>,
|
||||
pub truncated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum LintReadStatus {
|
||||
pub enum LintReadStatus {
|
||||
Success,
|
||||
Error(String),
|
||||
Timeout { wait_ms: u64 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize)]
|
||||
pub(crate) struct DiagnosticFreshness {
|
||||
pub(crate) source_revision: Option<String>,
|
||||
pub(crate) document_version: Option<i64>,
|
||||
pub(crate) diagnostic_version: Option<i64>,
|
||||
pub struct DiagnosticFreshness {
|
||||
pub source_revision: Option<String>,
|
||||
pub document_version: Option<i64>,
|
||||
pub diagnostic_version: Option<i64>,
|
||||
/// Only an exact publication version match proves the text was checked.
|
||||
pub(crate) freshness: Option<&'static str>,
|
||||
pub freshness: Option<&'static str>,
|
||||
}
|
||||
|
||||
struct DiagnosticPollSuccess {
|
||||
@@ -238,7 +238,8 @@ impl LspManager {
|
||||
// Match the existing workspace file serving ceiling. Decode only
|
||||
// bounded UTF-8 bytes from the same no-follow workspace opener.
|
||||
const MAX_DOCUMENT_BYTES: u64 = 16 * 1024 * 1024;
|
||||
let file = crate::fleet::files::WorkspaceFile::open(&canonical_root, relative, false)?;
|
||||
let file =
|
||||
codewhale_util::files::WorkspaceFile::open(&canonical_root, relative, false)?;
|
||||
let mut bytes = Vec::new();
|
||||
file.open_file()?
|
||||
.take(MAX_DOCUMENT_BYTES + 1)
|
||||
@@ -258,7 +259,7 @@ impl LspManager {
|
||||
|
||||
/// Inject a fake transport for a language. Used by tests so we never
|
||||
/// fork a real LSP server in CI.
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn install_test_transport(&self, lang: Language, transport: Arc<dyn LspTransport>) {
|
||||
self.test_transports.lock().await.insert(lang, transport);
|
||||
}
|
||||
@@ -748,7 +749,7 @@ impl LspManager {
|
||||
};
|
||||
let relative = match target.strip_prefix(&self.workspace).or_else(|_| target.strip_prefix(&root)) {
|
||||
Ok(relative) if !relative.as_os_str().is_empty()
|
||||
&& relative.components().all(|component| matches!(component, std::path::Component::Normal(part) if !crate::snapshot::is_git_metadata_name(part))) => relative,
|
||||
&& relative.components().all(|component| matches!(component, std::path::Component::Normal(part) if !codewhale_util::git_metadata::is_git_metadata_name(part))) => relative,
|
||||
_ => { omitted += 1; continue; }
|
||||
};
|
||||
let Some(path) = semantic_relative_path(relative) else {
|
||||
@@ -799,7 +800,7 @@ impl LspManager {
|
||||
/// transport pool. Every attempted file gets an explicit outcome, and the
|
||||
/// same configured severity policy as post-edit diagnostics is applied:
|
||||
/// errors are always retained and warnings depend on `include_warnings`.
|
||||
pub(crate) async fn diagnostics_for_paths(
|
||||
pub async fn diagnostics_for_paths(
|
||||
&self,
|
||||
files: &[PathBuf],
|
||||
) -> Result<Vec<LintReadResult>, String> {
|
||||
@@ -906,7 +907,9 @@ fn semantic_relative_path(path: &Path) -> Option<String> {
|
||||
let parts = path
|
||||
.components()
|
||||
.map(|component| match component {
|
||||
std::path::Component::Normal(part) if !crate::snapshot::is_git_metadata_name(part) => {
|
||||
std::path::Component::Normal(part)
|
||||
if !codewhale_util::git_metadata::is_git_metadata_name(part) =>
|
||||
{
|
||||
part.to_str().filter(|part| !part.contains('\\'))
|
||||
}
|
||||
_ => None,
|
||||
@@ -1021,28 +1024,29 @@ impl LspManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[cfg_attr(not(test), allow(dead_code, unused_imports))]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// Fake transport: returns a fixed list of diagnostics. Used by
|
||||
/// integration tests so we never spawn a real LSP server in CI.
|
||||
pub(crate) struct FakeTransport {
|
||||
pub struct FakeTransport {
|
||||
items: Vec<Diagnostic>,
|
||||
calls: AtomicUsize,
|
||||
}
|
||||
|
||||
impl FakeTransport {
|
||||
pub(crate) fn new(items: Vec<Diagnostic>) -> Self {
|
||||
pub fn new(items: Vec<Diagnostic>) -> Self {
|
||||
Self {
|
||||
items,
|
||||
calls: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn call_count(&self) -> usize {
|
||||
pub fn call_count(&self) -> usize {
|
||||
self.calls.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
@@ -1054,7 +1058,7 @@ pub(crate) mod tests {
|
||||
_path: &Path,
|
||||
_text: &str,
|
||||
_wait: Duration,
|
||||
) -> anyhow::Result<crate::lsp::client::DiagnosticPublication> {
|
||||
) -> anyhow::Result<crate::client::DiagnosticPublication> {
|
||||
self.calls.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(self.items.clone().into())
|
||||
}
|
||||
File renamed without changes.
@@ -17,5 +17,7 @@ workspace = true
|
||||
[dependencies]
|
||||
codewhale-config = { path = "../config", version = "0.10.2" }
|
||||
codewhale-core = { path = "../core", version = "0.10.2" }
|
||||
codewhale-sanitize = { path = "../sanitize", version = "0.10.2" }
|
||||
serde.workspace = true
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
thiserror.workspace = true
|
||||
@@ -0,0 +1,28 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// What a decision model answered and what it cost. Probabilities are basis
|
||||
/// points (0..=10000) so the receipt stays `Eq` and never re-renders floats.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AutoRouteDecisionEvidence {
|
||||
/// The tier the decision model chose (`fast` | `strong`).
|
||||
pub choice: String,
|
||||
pub probabilities_bp: BTreeMap<String, u16>,
|
||||
pub confidence_bp: u16,
|
||||
pub min_confidence_bp: u16,
|
||||
/// `[auto] cost_saving` kept the fast tier because the strong tier's
|
||||
/// probability was below the cost-saving floor.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub cost_saving_kept_fast: bool,
|
||||
/// The reasoning effort the decision applied, if any.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thinking: Option<String>,
|
||||
/// `usage.cost` exactly as the provider reported it (USD decimal).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider_reported_cost_usd: Option<String>,
|
||||
pub latency_ms: u64,
|
||||
/// The dated model snapshot the provider echoed, when present.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub response_model: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
use crate::{ContentBlock, Message};
|
||||
|
||||
/// Split a `data:<media-type>;base64,<payload>` URL.
|
||||
///
|
||||
/// Anthropic's Messages API models an image as a tagged `source` rather than a
|
||||
/// URL, so the native route has to take the data URL back apart. Returns
|
||||
/// `None` for `http(s)` URLs and for anything malformed, which the caller
|
||||
/// renders as a remote source or a visible degradation respectively.
|
||||
#[must_use]
|
||||
pub fn parse_data_url(url: &str) -> Option<(&str, &str)> {
|
||||
let rest = url.strip_prefix("data:")?;
|
||||
let (header, payload) = rest.split_once(',')?;
|
||||
let media_type = header.strip_suffix(";base64")?;
|
||||
if media_type.is_empty() || payload.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((media_type, payload))
|
||||
}
|
||||
|
||||
/// Whether a URL is one a provider can fetch for itself.
|
||||
#[must_use]
|
||||
pub fn is_remote_image_url(url: &str) -> bool {
|
||||
url.starts_with("https://") || url.starts_with("http://")
|
||||
}
|
||||
|
||||
pub const ELIDED_IMAGE_NOTE: &str = "[Image not attached to this request: this model accepts only a limited number of images per request, so the oldest are left out. It remains in the session; do not guess what it showed.]";
|
||||
|
||||
pub const ELIDED_TOOL_IMAGE_NOTE: &str = "[Tool image not attached to this request: this model accepts only a limited number of images per request, so the oldest are left out; do not guess what it showed. Take a fresh observation (for example a new screenshot) if you need the current state; do not repeat an action to recover its image.]";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct ImageWindow {
|
||||
pub total: usize,
|
||||
pub elided: usize,
|
||||
pub cap: usize,
|
||||
}
|
||||
|
||||
fn block_carries_image(block: &ContentBlock) -> bool {
|
||||
match block {
|
||||
ContentBlock::ImageUrl { .. } => true,
|
||||
ContentBlock::ToolResult { content_blocks, .. } => {
|
||||
content_blocks.as_ref().is_some_and(|blocks| {
|
||||
blocks.iter().any(|value| {
|
||||
value.get("type").and_then(serde_json::Value::as_str) == Some("image")
|
||||
})
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn count_request_images(messages: &[Message]) -> usize {
|
||||
messages
|
||||
.iter()
|
||||
.flat_map(|message| &message.content)
|
||||
.filter(|block| block_carries_image(block))
|
||||
.count()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn images_to_elide(total: usize, cap: usize) -> usize {
|
||||
if total <= cap {
|
||||
return 0;
|
||||
}
|
||||
let chunk = (cap / 4).max(1);
|
||||
((total - cap).div_ceil(chunk) * chunk).min(total)
|
||||
}
|
||||
|
||||
pub fn cap_request_images(messages: &mut [Message], cap: usize) -> ImageWindow {
|
||||
let total = count_request_images(messages);
|
||||
let mut window = cap_request_images_with_notes(
|
||||
messages,
|
||||
total - images_to_elide(total, cap),
|
||||
ELIDED_IMAGE_NOTE,
|
||||
ELIDED_TOOL_IMAGE_NOTE,
|
||||
);
|
||||
window.cap = cap;
|
||||
window
|
||||
}
|
||||
|
||||
pub fn cap_request_images_with_notes(
|
||||
messages: &mut [Message],
|
||||
cap: usize,
|
||||
image_note: &str,
|
||||
tool_note: &str,
|
||||
) -> ImageWindow {
|
||||
let total = count_request_images(messages);
|
||||
let elided = total.saturating_sub(cap);
|
||||
let mut remaining = elided;
|
||||
for block in messages.iter_mut().flat_map(|message| &mut message.content) {
|
||||
if remaining == 0 {
|
||||
break;
|
||||
}
|
||||
if !block_carries_image(block) {
|
||||
continue;
|
||||
}
|
||||
match block {
|
||||
ContentBlock::ImageUrl { .. } => {
|
||||
*block = ContentBlock::Text {
|
||||
text: image_note.to_string(),
|
||||
cache_control: None,
|
||||
};
|
||||
}
|
||||
ContentBlock::ToolResult {
|
||||
content,
|
||||
content_blocks,
|
||||
..
|
||||
} => {
|
||||
if let Some(blocks) = content_blocks.as_mut() {
|
||||
blocks.retain(|value| {
|
||||
value.get("type").and_then(serde_json::Value::as_str) != Some("image")
|
||||
});
|
||||
if blocks.is_empty() {
|
||||
*content_blocks = None;
|
||||
}
|
||||
}
|
||||
*content = format!("{content}\n{tool_note}");
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
remaining -= 1;
|
||||
}
|
||||
ImageWindow { total, elided, cap }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn tool_result_text_with_omission(content: &str, omitted: usize) -> String {
|
||||
if omitted == 0 {
|
||||
return content.to_string();
|
||||
}
|
||||
format!(
|
||||
"{content}\n[{omitted} tool-result image block(s) omitted: invalid, unsupported, oversized, or additional.]"
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,14 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod auto_route_evidence;
|
||||
pub mod image_window;
|
||||
pub mod reasoning_effort_tier;
|
||||
pub mod schema_canonicalize;
|
||||
pub mod schema_sanitize;
|
||||
pub mod tool_projection;
|
||||
pub mod turn_classify;
|
||||
|
||||
/// Context window used only for legacy DeepSeek model IDs that do not name a
|
||||
/// newer V4 alias and do not carry an explicit `*k` suffix.
|
||||
pub const LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS: u32 = 128_000;
|
||||
@@ -484,7 +492,6 @@ where
|
||||
Ok(Option::<String>::deserialize(deserializer)?.unwrap_or_default())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
/// Streaming event types for SSE responses.
|
||||
@@ -522,7 +529,6 @@ pub enum StreamEvent {
|
||||
Error { error: serde_json::Value },
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
/// Content block types used in streaming starts.
|
||||
@@ -590,7 +596,6 @@ pub enum Delta {
|
||||
ReasoningStateDelta { state: OpaqueReasoningState },
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
/// Delta payload for message-level updates.
|
||||
pub struct MessageDelta {
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,25 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Canonical reasoning-effort tiers recorded as configuration facts. This is
|
||||
/// deliberately an enum rather than free-form text so Work Graph activity can
|
||||
/// never become a side channel for model reasoning.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ReasoningEffortTier {
|
||||
Off,
|
||||
Minimal,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
#[serde(rename = "xhigh")]
|
||||
XHigh,
|
||||
Ultra,
|
||||
Auto,
|
||||
Max,
|
||||
/// Thinking is enabled, but the provider route exposes no supported
|
||||
/// effort tiers. This is an effective receipt, never a requested setting.
|
||||
ThinkingEnabledGranularityUnavailable,
|
||||
/// The configured route exposes no verified reasoning-control contract.
|
||||
/// This is an effective receipt, never a requested setting.
|
||||
Unavailable,
|
||||
}
|
||||
File renamed without changes.
@@ -15,8 +15,6 @@ use std::collections::HashSet;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use codewhale_models::Tool;
|
||||
|
||||
/// Sanitize a JSON Schema in-place for DeepSeek strict-tool compatibility.
|
||||
///
|
||||
/// Applies a sequence of normalisations chosen to be semantics-preserving:
|
||||
@@ -42,25 +40,6 @@ pub fn sanitize(schema: &mut Value) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare a complete active tool set for DeepSeek strict function-calling.
|
||||
///
|
||||
/// Each tool is evaluated independently: compatible schemas are sanitized and
|
||||
/// marked strict, while incompatible schemas remain unchanged and non-strict.
|
||||
/// Returns `true` only when every tool in the set can use strict mode.
|
||||
pub fn prepare_tools_for_strict_mode(tools: &mut [Tool]) -> bool {
|
||||
let mut all_strict = true;
|
||||
for tool in tools {
|
||||
if strict_schema_supported(&tool.input_schema) {
|
||||
sanitize_for_strict(&mut tool.input_schema);
|
||||
tool.strict = Some(true);
|
||||
} else {
|
||||
tool.strict = None;
|
||||
all_strict = false;
|
||||
}
|
||||
}
|
||||
all_strict
|
||||
}
|
||||
|
||||
/// Sanitize a schema for DeepSeek strict function-calling.
|
||||
///
|
||||
/// This extends the general sanitizer with the official strict-mode object
|
||||
@@ -123,7 +102,7 @@ pub fn sanitize_for_responses(schema: &mut Value) -> Option<String> {
|
||||
combine_constraint_notes(constraint_note, dependent_note)
|
||||
}
|
||||
|
||||
fn strict_schema_supported(schema: &Value) -> bool {
|
||||
pub fn strict_schema_supported(schema: &Value) -> bool {
|
||||
let mut normalized = schema.clone();
|
||||
sanitize(&mut normalized);
|
||||
!has_strict_incompatible_composition(&normalized, true)
|
||||
@@ -472,20 +451,6 @@ mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
fn test_tool(name: &str, input_schema: Value) -> Tool {
|
||||
Tool {
|
||||
tool_type: None,
|
||||
name: name.to_string(),
|
||||
description: name.to_string(),
|
||||
input_schema,
|
||||
allowed_callers: None,
|
||||
defer_loading: None,
|
||||
input_examples: None,
|
||||
strict: None,
|
||||
cache_control: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collapses_nullable_anyof() {
|
||||
let mut schema = json!({
|
||||
@@ -839,139 +804,6 @@ mod tests {
|
||||
assert_eq!(score["maximum"], 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_mode_applies_per_tool_in_mixed_catalog() {
|
||||
let mut tools = vec![
|
||||
test_tool(
|
||||
"lookup",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
),
|
||||
test_tool(
|
||||
"either",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"type": "string"},
|
||||
"b": {"type": "string"}
|
||||
},
|
||||
"anyOf": [
|
||||
{"required": ["a"]},
|
||||
{"required": ["b"]}
|
||||
]
|
||||
}),
|
||||
),
|
||||
test_tool(
|
||||
"nested",
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
assert!(!prepare_tools_for_strict_mode(&mut tools));
|
||||
assert_eq!(tools[0].strict, Some(true));
|
||||
assert_eq!(tools[0].input_schema["required"], json!(["query"]));
|
||||
assert_eq!(tools[0].input_schema["additionalProperties"], false);
|
||||
assert_eq!(tools[1].strict, None);
|
||||
assert!(tools[1].input_schema.get("anyOf").is_some());
|
||||
assert_eq!(tools[2].strict, None);
|
||||
assert!(
|
||||
tools[2].input_schema["properties"]["value"]
|
||||
.get("oneOf")
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_mode_rejects_nested_unsupported_composition() {
|
||||
let mut tools = vec![Tool {
|
||||
tool_type: None,
|
||||
name: "nested".to_string(),
|
||||
description: "Nested oneOf".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"oneOf": [
|
||||
{"type": "string"},
|
||||
{"type": "integer"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}),
|
||||
allowed_callers: None,
|
||||
defer_loading: None,
|
||||
input_examples: None,
|
||||
strict: None,
|
||||
cache_control: None,
|
||||
}];
|
||||
|
||||
assert!(!prepare_tools_for_strict_mode(&mut tools));
|
||||
assert_eq!(tools[0].strict, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_mode_leaves_dependent_schema_tools_non_strict() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {"type": "string"},
|
||||
"message": {"type": "string"}
|
||||
},
|
||||
"dependentSchemas": {
|
||||
"action": {
|
||||
"properties": {"message": {}},
|
||||
"required": ["message"]
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut tools = vec![test_tool("agent", schema.clone())];
|
||||
|
||||
assert!(!prepare_tools_for_strict_mode(&mut tools));
|
||||
assert_eq!(tools[0].strict, None);
|
||||
assert_eq!(tools[0].input_schema, schema);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strict_mode_marks_compatible_tools_strict() {
|
||||
let mut tools = vec![Tool {
|
||||
tool_type: None,
|
||||
name: "lookup".to_string(),
|
||||
description: "Lookup".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
allowed_callers: None,
|
||||
defer_loading: None,
|
||||
input_examples: None,
|
||||
strict: None,
|
||||
cache_control: None,
|
||||
}];
|
||||
|
||||
assert!(prepare_tools_for_strict_mode(&mut tools));
|
||||
assert_eq!(tools[0].strict, Some(true));
|
||||
assert_eq!(tools[0].input_schema["required"], json!(["query"]));
|
||||
assert_eq!(tools[0].input_schema["additionalProperties"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_sanitize_removes_root_composition_from_apply_patch_shape() {
|
||||
let mut schema = json!({
|
||||
@@ -1084,30 +916,6 @@ mod tests {
|
||||
assert_eq!(note, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xai_sanitize_flattens_apply_patch_root_one_of() {
|
||||
// The exact shape that produced the live 400:
|
||||
// "apply_patch: tool parameter root must be an object type (root
|
||||
// schema is an anyOf/oneOf union with a non-object branch)".
|
||||
use crate::tools::spec::ToolSpec as _;
|
||||
let mut schema = crate::tools::apply_patch::ApplyPatchTool.input_schema();
|
||||
assert!(schema.get("oneOf").is_some(), "fixture must match the tool");
|
||||
|
||||
let note = sanitize_for_xai_parameters(&mut schema);
|
||||
|
||||
assert_eq!(schema["type"], "object");
|
||||
assert!(schema.get("oneOf").is_none());
|
||||
assert!(schema.get("anyOf").is_none());
|
||||
assert!(schema["properties"].get("patch").is_some());
|
||||
assert!(schema["properties"].get("changes").is_some());
|
||||
assert_eq!(
|
||||
note.as_deref(),
|
||||
Some(
|
||||
"Exactly one of these parameter groups must be provided: `changes` | `patch` | `replace`."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_sanitize_plain_object_has_no_constraint_note() {
|
||||
let mut schema = json!({
|
||||
@@ -2157,8 +1965,6 @@ fn validate_mfjs_expanded_depth(
|
||||
#[cfg(test)]
|
||||
mod kimi_tests {
|
||||
use super::*;
|
||||
use crate::tools::apply_patch::ApplyPatchTool;
|
||||
use crate::tools::spec::ToolSpec;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -2424,27 +2230,6 @@ mod kimi_tests {
|
||||
assert_eq!(schema["properties"]["name"]["type"], "string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_parameters_flattens_actual_apply_patch_root_and_returns_constraint_note() {
|
||||
let mut schema = ApplyPatchTool.input_schema();
|
||||
|
||||
let note = sanitize_for_kimi_parameters(&mut schema).unwrap();
|
||||
|
||||
assert_eq!(schema["type"], "object");
|
||||
assert!(schema.get("oneOf").is_none());
|
||||
assert!(schema.get("anyOf").is_none());
|
||||
assert!(schema.get("allOf").is_none());
|
||||
assert_eq!(schema["properties"]["patch"]["type"], "string");
|
||||
assert_eq!(schema["properties"]["replace"]["type"], "array");
|
||||
assert_eq!(schema["properties"]["changes"]["type"], "array");
|
||||
assert_eq!(
|
||||
note.as_deref(),
|
||||
Some(
|
||||
"Exactly one of these parameter groups must be provided: `changes` | `patch` | `replace`."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kimi_parameters_preserves_nested_anyof_branches() {
|
||||
let mut schema = json!({
|
||||
@@ -0,0 +1,36 @@
|
||||
pub const TOOL_PROJECTION_WARNING_MAX_NAMES: usize = 8;
|
||||
pub const TOOL_PROJECTION_WARNING_MAX_NAME_CHARS: usize = 64;
|
||||
|
||||
/// Return a privacy-safe, display-bounded sample of omitted wire tool names.
|
||||
///
|
||||
/// MCP catalogs may contain thousands of tools and their names are supplied by
|
||||
/// external servers. Keep the exact count separately, but never let one route
|
||||
/// diagnostic flood a transcript, toast, or runtime record.
|
||||
#[must_use]
|
||||
pub fn bounded_tool_projection_warning_names(omitted_tool_names: &[String]) -> Vec<String> {
|
||||
omitted_tool_names
|
||||
.iter()
|
||||
.take(TOOL_PROJECTION_WARNING_MAX_NAMES)
|
||||
.map(|name| {
|
||||
let cleaned: String = name
|
||||
.chars()
|
||||
.filter(|c| !c.is_control() && !codewhale_sanitize::sanitize::is_bidi_control(*c))
|
||||
.collect();
|
||||
let collapsed = cleaned.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
let collapsed = if collapsed.is_empty() {
|
||||
"<unnamed>".to_string()
|
||||
} else {
|
||||
collapsed
|
||||
};
|
||||
if collapsed.chars().count() <= TOOL_PROJECTION_WARNING_MAX_NAME_CHARS {
|
||||
return collapsed;
|
||||
}
|
||||
let mut out: String = collapsed
|
||||
.chars()
|
||||
.take(TOOL_PROJECTION_WARNING_MAX_NAME_CHARS - 1)
|
||||
.collect();
|
||||
out.push('…');
|
||||
out
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use crate::{ContentBlock, Message, Role};
|
||||
|
||||
/// Detection marker for a new-format checkpoint: the opening words of
|
||||
/// [`SUMMARY_HEADER`]. A new checkpoint is recognised only when its first
|
||||
/// block starts with this marker and its second block is the provenance
|
||||
/// block, so a user message that merely quotes the phrase stays a user
|
||||
/// message.
|
||||
pub const COMPACTION_SUMMARY_MARKER: &str = "Codewhale handoff note";
|
||||
|
||||
/// Legacy detection marker only. Checkpoints written before the handoff note
|
||||
/// opened with this sentence. Saved sessions that still carry it must be
|
||||
/// recognised so the next pass replaces that checkpoint instead of stacking a
|
||||
/// second one.
|
||||
pub const LEGACY_V2_COMPACTION_SUMMARY_MARKER: &str =
|
||||
"Another language model started to solve this problem";
|
||||
|
||||
pub const COMPACTION_CHECKPOINT_PROVENANCE: &str = "<!-- codewhale.compaction-checkpoint.v1 -->";
|
||||
|
||||
/// Request-time recognition: exactly the header text block plus the
|
||||
/// provenance block. User text merely quoting a marker keeps its original
|
||||
/// wire position. Checkpoints saved before the handoff note still pass, so
|
||||
/// their position survives restore and the next pass replaces them.
|
||||
pub fn is_wire_compaction_checkpoint_message(message: &Message) -> bool {
|
||||
let [
|
||||
ContentBlock::Text {
|
||||
text,
|
||||
cache_control: None,
|
||||
},
|
||||
ContentBlock::Text {
|
||||
text: provenance,
|
||||
cache_control: None,
|
||||
},
|
||||
] = message.content.as_slice()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
message.role == Role::User
|
||||
&& (text.starts_with(COMPACTION_SUMMARY_MARKER)
|
||||
|| text.starts_with(LEGACY_V2_COMPACTION_SUMMARY_MARKER))
|
||||
&& provenance == COMPACTION_CHECKPOINT_PROVENANCE
|
||||
}
|
||||
|
||||
pub const RESTORED_CHECKPOINT_TURN_META: &str = concat!(
|
||||
"<turn_meta>\n",
|
||||
"Input provenance: subagent_handoff (non-authoritative)\n",
|
||||
"Restore projection: subagent_checkpoint_v1\n",
|
||||
"</turn_meta>",
|
||||
);
|
||||
|
||||
pub const RESTORED_COMPLETION_HEADER: &str = "[Codewhale restored sub-agent checkpoint]";
|
||||
|
||||
pub const RESTORED_COMPLETIONS_HEADER: &str = "[Codewhale restored sub-agent checkpoints]";
|
||||
|
||||
pub const RESTORED_RUNNING_HEADER: &str = "[Codewhale restored sub-agent runtime checkpoint]";
|
||||
|
||||
pub const RESTORED_TOPOLOGY_HEADER: &str = "[Codewhale restored Agent topology checkpoint]";
|
||||
|
||||
/// Return the user-safe display body for an already projected checkpoint.
|
||||
/// The exact metadata marker keeps arbitrary user-authored text on the normal
|
||||
/// conversation path.
|
||||
pub fn restored_subagent_checkpoint_display(message: &Message) -> Option<&str> {
|
||||
if message.role != "user" {
|
||||
return None;
|
||||
}
|
||||
let [
|
||||
ContentBlock::Text {
|
||||
text,
|
||||
cache_control: first_cache,
|
||||
},
|
||||
ContentBlock::Text {
|
||||
text: turn_meta,
|
||||
cache_control: meta_cache,
|
||||
},
|
||||
] = message.content.as_slice()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if first_cache.is_some()
|
||||
|| meta_cache.is_some()
|
||||
|| turn_meta != RESTORED_CHECKPOINT_TURN_META
|
||||
|| ![
|
||||
RESTORED_COMPLETION_HEADER,
|
||||
RESTORED_COMPLETIONS_HEADER,
|
||||
RESTORED_RUNNING_HEADER,
|
||||
RESTORED_TOPOLOGY_HEADER,
|
||||
]
|
||||
.iter()
|
||||
.any(|header| text.starts_with(header))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(text)
|
||||
}
|
||||
|
||||
/// Classification used when locating a user-authored turn in the session log.
|
||||
///
|
||||
/// Runtime and tool messages are skipped because their provider-compatible
|
||||
/// `role = "user"` is not user authority. Unsupported user content is a real
|
||||
/// turn boundary, however, so callers must not skip it and edit an older turn.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UserTurnPromptKind {
|
||||
/// Assistant messages, tool results, and runtime-owned control messages.
|
||||
NotPrompt,
|
||||
/// A genuine user turn containing editable text.
|
||||
Editable,
|
||||
/// A genuine user turn without editable text, such as an image-only turn.
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
/// Classify `message` for edit-last-turn and admitted-display handling.
|
||||
#[must_use]
|
||||
pub fn classify_user_turn_prompt(message: &Message) -> UserTurnPromptKind {
|
||||
if message.role != Role::User {
|
||||
return UserTurnPromptKind::NotPrompt;
|
||||
}
|
||||
if message.content.iter().any(|block| {
|
||||
matches!(
|
||||
block,
|
||||
ContentBlock::ToolResult { .. }
|
||||
| ContentBlock::ToolSearchToolResult { .. }
|
||||
| ContentBlock::CodeExecutionToolResult { .. }
|
||||
)
|
||||
}) {
|
||||
return UserTurnPromptKind::NotPrompt;
|
||||
}
|
||||
if is_runtime_owned_user_message(message) || is_wire_compaction_checkpoint_message(message) {
|
||||
return UserTurnPromptKind::NotPrompt;
|
||||
}
|
||||
|
||||
let turn_metadata_index = turn_metadata_text(message).map(|(index, _)| index);
|
||||
if message.content.iter().enumerate().any(|(index, block)| {
|
||||
Some(index) != turn_metadata_index && matches!(block, ContentBlock::Text { .. })
|
||||
}) {
|
||||
UserTurnPromptKind::Editable
|
||||
} else {
|
||||
UserTurnPromptKind::Unsupported
|
||||
}
|
||||
}
|
||||
|
||||
/// True when a `role = "user"` message is runtime-owned rather than
|
||||
/// user-authored. Runtime authority is accepted only from the engine-owned
|
||||
/// structural `<turn_meta>` block, never from arbitrary user text that happens
|
||||
/// to resemble a runtime envelope or metadata marker.
|
||||
pub fn is_runtime_owned_user_message(message: &Message) -> bool {
|
||||
restored_subagent_checkpoint_display(message).is_some()
|
||||
|| has_non_authoritative_turn_provenance(message)
|
||||
}
|
||||
|
||||
/// Return engine-owned metadata in either the current trailing shape or the
|
||||
/// historical leading shape. Requiring a separate prompt block prevents a
|
||||
/// user who submits `<turn_meta>…</turn_meta>` as ordinary text from minting
|
||||
/// authority.
|
||||
pub fn turn_metadata_text(message: &Message) -> Option<(usize, &str)> {
|
||||
if message.content.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
if let Some(ContentBlock::Text {
|
||||
text,
|
||||
cache_control: None,
|
||||
}) = message.content.last()
|
||||
{
|
||||
let trimmed = text.trim();
|
||||
if is_complete_turn_metadata(trimmed) {
|
||||
return Some((message.content.len() - 1, trimmed));
|
||||
}
|
||||
}
|
||||
// Sessions written before the metadata-tail migration used
|
||||
// `[turn_meta, prompt, ...]`. Match the same conservative legacy shape as
|
||||
// the transcript renderer: a metadata envelope first and ordinary text
|
||||
// last. A single user-authored metadata example is never hidden.
|
||||
let ContentBlock::Text {
|
||||
text,
|
||||
cache_control: None,
|
||||
} = message.content.first()?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let trimmed = text.trim();
|
||||
let trailing_text_is_ordinary = matches!(
|
||||
message.content.last(),
|
||||
Some(ContentBlock::Text { text, .. }) if !is_complete_turn_metadata(text.trim())
|
||||
);
|
||||
(is_complete_turn_metadata(trimmed) && trailing_text_is_ordinary).then_some((0, trimmed))
|
||||
}
|
||||
|
||||
fn is_complete_turn_metadata(text: &str) -> bool {
|
||||
text.starts_with("<turn_meta>") && text.ends_with("</turn_meta>")
|
||||
}
|
||||
|
||||
/// Recognize both the current condensed provenance line and the legacy
|
||||
/// provenance/authority pair. Any explicitly non-authoritative provenance is
|
||||
/// runtime-owned; this stays correct as new provenance variants are added.
|
||||
fn has_non_authoritative_turn_provenance(message: &Message) -> bool {
|
||||
let Some((_, metadata)) = turn_metadata_text(message) else {
|
||||
return false;
|
||||
};
|
||||
let mut has_provenance = false;
|
||||
let mut condensed_non_authoritative = false;
|
||||
let mut legacy_non_authoritative = false;
|
||||
for line in metadata.lines().map(str::trim) {
|
||||
if let Some(value) = line.strip_prefix("Input provenance: ") {
|
||||
has_provenance = true;
|
||||
condensed_non_authoritative |= value.ends_with(" (non-authoritative)");
|
||||
}
|
||||
legacy_non_authoritative |= line == "Input authority: non_authoritative";
|
||||
}
|
||||
condensed_non_authoritative || (has_provenance && legacy_non_authoritative)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "codewhale-net"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Codewhale outbound network policy, guarded destinations and proxy resolution"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
codewhale-command-contract = { path = "../command-contract", version = "0.10.2" }
|
||||
codewhale-config = { path = "../config", version = "0.10.2" }
|
||||
codewhale-release = { path = "../release", version = "0.10.2" }
|
||||
codewhale-runtime = { path = "../runtime", version = "0.10.2" }
|
||||
codewhale-tools = { path = "../tools", version = "0.10.2" }
|
||||
http = "1"
|
||||
hyper-util = { version = "=0.1.21", features = ["client-proxy"] }
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
@@ -0,0 +1,630 @@
|
||||
use crate::network_policy::NetworkPolicyDecider;
|
||||
use codewhale_tools::ToolError;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Wall-clock bound for one pre-flight DNS resolution. The resolution runs
|
||||
/// before the guarded request (and once per redirect), so a hung resolver
|
||||
/// would otherwise stall the tool far beyond the documented request timeout
|
||||
/// envelope of the calling fetch tool.
|
||||
pub const DNS_PREFLIGHT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// DNS pin returned when a hostname was resolved to a validated public IP.
|
||||
/// Callers should pass this to `reqwest::ClientBuilder::resolve` so the
|
||||
/// connection uses the pre-validated address instead of re-resolving.
|
||||
pub type DnsPin = Option<(String, IpAddr)>;
|
||||
|
||||
/// Build the transport used after a destination has passed SSRF validation.
|
||||
/// Ambient HTTP(S)/SOCKS proxies are deliberately disabled: a proxy would
|
||||
/// receive the original hostname, resolve it again outside this process, and
|
||||
/// bypass the validated DNS pin.
|
||||
pub fn guarded_reqwest_client_builder() -> reqwest::ClientBuilder {
|
||||
codewhale_release::tls::reqwest_client_builder().no_proxy()
|
||||
}
|
||||
|
||||
/// Check if an IP address is loopback, private, link-local, cloud-metadata,
|
||||
/// multicast, or reserved — all addresses that should not be reachable via
|
||||
/// an LLM-initiated fetch request (SSRF prevention).
|
||||
pub fn is_restricted_ip(ip: &IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(v4) => {
|
||||
v4.is_loopback()
|
||||
|| v4.is_private()
|
||||
|| v4.is_link_local()
|
||||
|| v4.is_multicast()
|
||||
|| v4.is_broadcast()
|
||||
|| v4.is_unspecified()
|
||||
// 100.64.0.0/10 — Carrier-grade NAT (CGNAT / shared address space)
|
||||
|| matches!(v4.octets(), [100, 64..=127, ..])
|
||||
// 169.254.169.254 — cloud metadata (AWS/GCP/Azure)
|
||||
|| *ip == IpAddr::V4(std::net::Ipv4Addr::new(169, 254, 169, 254))
|
||||
// 198.18.0.0/15 — IETF benchmark testing
|
||||
|| matches!(v4.octets(), [198, 18..=19, ..])
|
||||
// 240.0.0.0/4 — reserved (former Class E)
|
||||
|| v4.octets()[0] >= 240
|
||||
}
|
||||
IpAddr::V6(v6) => {
|
||||
// IPv4-mapped IPv6 addresses (::ffff:a.b.c.d) — unwrap and check as IPv4
|
||||
// to prevent bypass via ::ffff:127.0.0.1 etc.
|
||||
if v6.is_unspecified()
|
||||
|| matches!(v6.octets(), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, ..])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(v4) = v6.to_ipv4_mapped() {
|
||||
return is_restricted_ip(&IpAddr::V4(v4));
|
||||
}
|
||||
// A NAT64 gateway reaches the embedded IPv4 target, so the prefix
|
||||
// must not hide a restricted address.
|
||||
if let Some(v4) = nat64_ipv4(*v6)
|
||||
&& is_restricted_ip(&IpAddr::V4(v4))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
v6.is_loopback()
|
||||
|| v6.is_multicast()
|
||||
|| matches!(v6.segments(), [0xfc00..=0xfdff, ..]) // ULA fc00::/7
|
||||
|| matches!(v6.segments(), [0xfe80..=0xfebf, ..]) // Link-local fe80::/10
|
||||
|| matches!(v6.segments(), [0xfec0..=0xfeff, ..]) // Site-local fec0::/10
|
||||
|| matches!(v6.segments(), [0x2002, ..]) // 6to4 2002::/16
|
||||
|| matches!(v6.segments(), [0x2001, 0x0000, ..]) // Teredo 2001::/32
|
||||
|| matches!(v6.segments(), [0, 0, 0, 0, 0, 0, ..]) // IPv4-compatible ::a.b.c.d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The IPv4 target of a well-known-prefix NAT64 address (`64:ff9b::/96`).
|
||||
fn nat64_ipv4(v6: std::net::Ipv6Addr) -> Option<std::net::Ipv4Addr> {
|
||||
let segments = v6.segments();
|
||||
if segments[0] == 0x0064 && segments[1] == 0xff9b && segments[2..6].iter().all(|s| *s == 0) {
|
||||
let octets = v6.octets();
|
||||
return Some(std::net::Ipv4Addr::new(
|
||||
octets[12], octets[13], octets[14], octets[15],
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn fetch_target_host(url: &reqwest::Url) -> Result<String, ToolError> {
|
||||
if url.scheme() != "http" && url.scheme() != "https" {
|
||||
return Err(ToolError::invalid_input(
|
||||
"only http:// and https:// URLs are supported",
|
||||
));
|
||||
}
|
||||
|
||||
url.host_str()
|
||||
.map(str::to_ascii_lowercase)
|
||||
.ok_or_else(|| ToolError::invalid_input("URL must include a host"))
|
||||
}
|
||||
|
||||
pub async fn validate_fetch_destination(
|
||||
host: String,
|
||||
decider: Option<&NetworkPolicyDecider>,
|
||||
tool: &str,
|
||||
resolve: bool,
|
||||
) -> Result<DnsPin, ToolError> {
|
||||
// SSRF protection: reject localhost names and private/link-local/loopback
|
||||
// IP literals; when `resolve` is set, also resolve the hostname and reject
|
||||
// restricted answers. Prevents LLM-prompted requests to cloud metadata
|
||||
// (169.254.169.254), localhost services, and internal networks.
|
||||
let bare = host.strip_suffix('.').unwrap_or(&host);
|
||||
if bare == "localhost" || bare == "localhost.localdomain" || bare.ends_with(".localhost") {
|
||||
return Err(ToolError::permission_denied(
|
||||
"requests to localhost are not allowed",
|
||||
));
|
||||
}
|
||||
// Normalize bracketed IPv6 literals before the literal-IP check so they
|
||||
// route through the same restricted-IP policy as unbracketed forms
|
||||
// (GHSA-88gh-2526-gfrr).
|
||||
let ip_candidate = host
|
||||
.strip_prefix('[')
|
||||
.and_then(|s| s.strip_suffix(']'))
|
||||
.unwrap_or(host.as_str());
|
||||
if let Ok(ip) = ip_candidate.parse::<IpAddr>() {
|
||||
if is_restricted_ip(&ip) {
|
||||
return Err(ToolError::permission_denied(format!(
|
||||
"IP {ip} is a restricted address (private/loopback/link-local)"
|
||||
)));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
if !resolve {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let addrs = tokio::time::timeout(
|
||||
DNS_PREFLIGHT_TIMEOUT,
|
||||
tokio::net::lookup_host((host.as_str(), 0u16)),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ToolError::permission_denied(format!("timed out resolving host before {tool} request"))
|
||||
})?
|
||||
.map_err(|e| {
|
||||
ToolError::permission_denied(format!("could not resolve host before {tool} request: {e}"))
|
||||
})?;
|
||||
let mut first_valid: Option<IpAddr> = None;
|
||||
for addr in addrs {
|
||||
validate_dns_resolved_ip(&host, &addr.ip(), decider, tool)?;
|
||||
if first_valid.is_none() {
|
||||
first_valid = Some(addr.ip());
|
||||
}
|
||||
}
|
||||
|
||||
let Some(validated_ip) = first_valid else {
|
||||
return Err(ToolError::permission_denied(format!(
|
||||
"host resolved to no addresses before {tool} request"
|
||||
)));
|
||||
};
|
||||
Ok(Some((host, validated_ip)))
|
||||
}
|
||||
|
||||
pub fn validate_dns_resolved_ip(
|
||||
host: &str,
|
||||
ip: &IpAddr,
|
||||
decider: Option<&NetworkPolicyDecider>,
|
||||
tool: &str,
|
||||
) -> Result<(), ToolError> {
|
||||
if !is_restricted_ip(ip) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// A fake-IP exception requires both an explicitly trusted hostname and an
|
||||
// explicitly trusted placeholder CIDR. The CIDR parser admits only subnets
|
||||
// inside 198.18.0.0/15, so real private/loopback/link-local/metadata/ULA
|
||||
// addresses remain blocked even when the hostname is trusted.
|
||||
if let Some(decider) = decider
|
||||
&& decider.is_trusted_fakeip_addr(ip)
|
||||
&& decider.trusts_proxy_fakeip_host(host)
|
||||
{
|
||||
decider.record_trusted_proxy_fakeip_allow(host, tool);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(ToolError::permission_denied(format!(
|
||||
"resolved IP {ip} is a restricted address (private/loopback/link-local)"
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[cfg(not(windows))]
|
||||
use std::io::{Read, Write};
|
||||
#[cfg(not(windows))]
|
||||
use std::net::{Ipv4Addr, SocketAddr, TcpListener};
|
||||
#[cfg(not(windows))]
|
||||
use std::process::Command;
|
||||
#[cfg(not(windows))]
|
||||
use std::sync::Arc;
|
||||
#[cfg(not(windows))]
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
#[cfg(not(windows))]
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum AmbientProxyKind {
|
||||
Http,
|
||||
HttpsConnect,
|
||||
SocksRemoteDns,
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn spawn_accept_probe(
|
||||
stop: Arc<AtomicBool>,
|
||||
respond_http: bool,
|
||||
drain_http_headers: bool,
|
||||
) -> (u16, std::thread::JoinHandle<bool>) {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind probe listener");
|
||||
let port = listener.local_addr().expect("probe address").port();
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.expect("nonblocking probe listener");
|
||||
let handle = std::thread::spawn(move || {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
match listener.accept() {
|
||||
Ok((mut stream, _)) => {
|
||||
// Accepted sockets inherit nonblocking from the listener
|
||||
// on several Unixes. Restore blocking mode before timed
|
||||
// header reads so a not-yet-ready socket is not treated
|
||||
// as a hard failure (WouldBlock / EAGAIN).
|
||||
stream
|
||||
.set_nonblocking(false)
|
||||
.expect("blocking probe stream");
|
||||
if drain_http_headers {
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.expect("probe read timeout");
|
||||
let mut request = Vec::new();
|
||||
let mut chunk = [0_u8; 1024];
|
||||
while !request.windows(4).any(|window| window == b"\r\n\r\n") {
|
||||
let read = match stream.read(&mut chunk) {
|
||||
Ok(n) => n,
|
||||
Err(err)
|
||||
if matches!(
|
||||
err.kind(),
|
||||
std::io::ErrorKind::WouldBlock
|
||||
| std::io::ErrorKind::TimedOut
|
||||
| std::io::ErrorKind::Interrupted
|
||||
) =>
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Err(err) => panic!("read probe request: {err}"),
|
||||
};
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
request.extend_from_slice(&chunk[..read]);
|
||||
if Instant::now() >= deadline {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if respond_http {
|
||||
let _ = stream.write_all(
|
||||
b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {}
|
||||
Err(err) => panic!("probe accept failed: {err}"),
|
||||
}
|
||||
if stop.load(Ordering::SeqCst) || Instant::now() >= deadline {
|
||||
return false;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
});
|
||||
(port, handle)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn run_ambient_proxy_probe(kind: AmbientProxyKind, guarded: bool) -> (bool, bool) {
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let (target_port, target_handle) = spawn_accept_probe(
|
||||
Arc::clone(&stop),
|
||||
matches!(
|
||||
kind,
|
||||
AmbientProxyKind::Http | AmbientProxyKind::SocksRemoteDns
|
||||
),
|
||||
matches!(
|
||||
kind,
|
||||
AmbientProxyKind::Http | AmbientProxyKind::SocksRemoteDns
|
||||
),
|
||||
);
|
||||
let (proxy_port, proxy_handle) = spawn_accept_probe(Arc::clone(&stop), true, false);
|
||||
|
||||
let mut command = Command::new(std::env::current_exe().expect("current test executable"));
|
||||
command.args([
|
||||
"--exact",
|
||||
"guard::tests::guarded_transport_proxy_probe_child",
|
||||
"--ignored",
|
||||
"--nocapture",
|
||||
]);
|
||||
for key in [
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"NO_PROXY",
|
||||
"no_proxy",
|
||||
"REQUEST_METHOD",
|
||||
] {
|
||||
command.env_remove(key);
|
||||
}
|
||||
command
|
||||
.env("CODEWHALE_PROXY_PROBE_CHILD", "1")
|
||||
.env(
|
||||
"CODEWHALE_PROXY_PROBE_GUARDED",
|
||||
if guarded { "1" } else { "0" },
|
||||
)
|
||||
.env("CODEWHALE_PROXY_PROBE_TARGET_PORT", target_port.to_string());
|
||||
match kind {
|
||||
AmbientProxyKind::Http => {
|
||||
let proxy = format!("http://127.0.0.1:{proxy_port}");
|
||||
command
|
||||
.env("CODEWHALE_PROXY_PROBE_SCHEME", "http")
|
||||
.env("HTTP_PROXY", &proxy)
|
||||
.env("http_proxy", proxy);
|
||||
}
|
||||
AmbientProxyKind::HttpsConnect => {
|
||||
let proxy = format!("http://127.0.0.1:{proxy_port}");
|
||||
command
|
||||
.env("CODEWHALE_PROXY_PROBE_SCHEME", "https")
|
||||
.env("HTTPS_PROXY", &proxy)
|
||||
.env("https_proxy", proxy);
|
||||
}
|
||||
AmbientProxyKind::SocksRemoteDns => {
|
||||
let proxy = format!("socks5h://127.0.0.1:{proxy_port}");
|
||||
command
|
||||
.env("CODEWHALE_PROXY_PROBE_SCHEME", "http")
|
||||
.env("ALL_PROXY", &proxy)
|
||||
.env("all_proxy", proxy);
|
||||
}
|
||||
}
|
||||
|
||||
let output = command.output().expect("run proxy probe child");
|
||||
stop.store(true, Ordering::SeqCst);
|
||||
let target_hit = target_handle.join().expect("join target probe");
|
||||
let proxy_hit = proxy_handle.join().expect("join proxy probe");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"proxy probe child failed for {kind:?} guarded={guarded}:\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
(target_hit, proxy_hit)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[tokio::test]
|
||||
#[ignore = "subprocess helper for ambient proxy regression test"]
|
||||
async fn guarded_transport_proxy_probe_child() {
|
||||
if std::env::var_os("CODEWHALE_PROXY_PROBE_CHILD").is_none() {
|
||||
return;
|
||||
}
|
||||
let guarded = std::env::var("CODEWHALE_PROXY_PROBE_GUARDED").as_deref() == Ok("1");
|
||||
let scheme = std::env::var("CODEWHALE_PROXY_PROBE_SCHEME").expect("probe scheme");
|
||||
let target_port = std::env::var("CODEWHALE_PROXY_PROBE_TARGET_PORT")
|
||||
.expect("probe target port")
|
||||
.parse::<u16>()
|
||||
.expect("numeric target port");
|
||||
let host = "guarded-proxy-probe.example.invalid";
|
||||
let builder = if guarded {
|
||||
guarded_reqwest_client_builder()
|
||||
} else {
|
||||
codewhale_release::tls::reqwest_client_builder()
|
||||
};
|
||||
let client = builder
|
||||
.timeout(Duration::from_secs(2))
|
||||
.resolve(
|
||||
host,
|
||||
SocketAddr::new(Ipv4Addr::LOCALHOST.into(), target_port),
|
||||
)
|
||||
.build()
|
||||
.expect("build proxy probe client");
|
||||
let result = client
|
||||
.get(format!("{scheme}://{host}:{target_port}/"))
|
||||
.send()
|
||||
.await;
|
||||
if guarded && scheme == "http" {
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"guarded HTTP target should answer: {result:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn guarded_transport_bypasses_ambient_http_https_and_remote_dns_socks_proxies() {
|
||||
for kind in [
|
||||
AmbientProxyKind::Http,
|
||||
AmbientProxyKind::HttpsConnect,
|
||||
AmbientProxyKind::SocksRemoteDns,
|
||||
] {
|
||||
let (unguarded_target, unguarded_proxy) = run_ambient_proxy_probe(kind, false);
|
||||
assert!(
|
||||
unguarded_proxy && !unguarded_target,
|
||||
"control client must demonstrate ambient {kind:?} proxy interception"
|
||||
);
|
||||
|
||||
let (guarded_target, guarded_proxy) = run_ambient_proxy_probe(kind, true);
|
||||
assert!(
|
||||
guarded_target && !guarded_proxy,
|
||||
"guarded client must preserve its DNS pin and bypass ambient {kind:?} proxy"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_private_localhost_literal() {
|
||||
assert!(is_restricted_ip(&"127.0.0.1".parse().unwrap()));
|
||||
assert!(is_restricted_ip(&"::1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_private_rfc1918() {
|
||||
assert!(is_restricted_ip(&"10.0.0.1".parse().unwrap()));
|
||||
assert!(is_restricted_ip(&"172.16.0.1".parse().unwrap()));
|
||||
assert!(is_restricted_ip(&"192.168.1.1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_cloud_metadata() {
|
||||
assert!(is_restricted_ip(&"169.254.169.254".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_link_local() {
|
||||
assert!(is_restricted_ip(&"169.254.1.1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_cgnat() {
|
||||
assert!(is_restricted_ip(&"100.64.0.1".parse().unwrap()));
|
||||
assert!(!is_restricted_ip(&"100.63.0.1".parse().unwrap()));
|
||||
assert!(!is_restricted_ip(&"100.128.0.1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_ipv6_ula() {
|
||||
assert!(is_restricted_ip(&"fc00::1".parse().unwrap()));
|
||||
assert!(is_restricted_ip(&"fd12:3456::1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_ipv4_mapped_ipv6() {
|
||||
// ::ffff:127.0.0.1 — IPv4-mapped IPv6 loopback bypass
|
||||
assert!(is_restricted_ip(&"::ffff:127.0.0.1".parse().unwrap()));
|
||||
assert!(is_restricted_ip(&"::ffff:10.0.0.1".parse().unwrap()));
|
||||
assert!(is_restricted_ip(&"::ffff:169.254.169.254".parse().unwrap()));
|
||||
assert!(is_restricted_ip(&"::ffff:192.168.1.1".parse().unwrap()));
|
||||
// :: (unspecified)
|
||||
assert!(is_restricted_ip(&"::".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_public_ips() {
|
||||
assert!(!is_restricted_ip(&"8.8.8.8".parse().unwrap()));
|
||||
assert!(!is_restricted_ip(&"1.1.1.1".parse().unwrap()));
|
||||
assert!(!is_restricted_ip(&"93.184.216.34".parse().unwrap()));
|
||||
assert!(!is_restricted_ip(&"2606:4700::1".parse().unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_dns_result_is_denied_without_proxy_opt_in() {
|
||||
let ip = "198.18.0.1".parse().unwrap();
|
||||
|
||||
let err = validate_dns_resolved_ip("github.com", &ip, None, "fetch_url")
|
||||
.expect_err("fake-IP DNS result must be denied by default");
|
||||
|
||||
assert!(format!("{err}").contains("resolved IP 198.18.0.1 is a restricted address"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_host_and_fakeip_cidr_allow_matching_placeholder() {
|
||||
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
|
||||
|
||||
let policy = NetworkPolicy {
|
||||
default: Decision::Allow.into(),
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
proxy: vec!["github.com".to_string()],
|
||||
proxy_fake_ip_cidrs: vec!["198.18.0.0/15".to_string()],
|
||||
audit: false,
|
||||
};
|
||||
let decider = NetworkPolicyDecider::new(policy, None);
|
||||
let ip = "198.18.0.1".parse().unwrap();
|
||||
|
||||
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url")
|
||||
.expect("matching host and fake-IP CIDR should allow the placeholder");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_host_without_fakeip_cidr_does_not_allow_restricted_dns() {
|
||||
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
|
||||
|
||||
let policy = NetworkPolicy {
|
||||
default: Decision::Allow.into(),
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
proxy: vec!["github.com".to_string()],
|
||||
proxy_fake_ip_cidrs: Vec::new(),
|
||||
audit: false,
|
||||
};
|
||||
let decider = NetworkPolicyDecider::new(policy, None);
|
||||
let ip = "198.18.0.1".parse().unwrap();
|
||||
|
||||
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url")
|
||||
.expect_err("hostname trust alone must not allow a restricted address");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fakeip_cidr_without_proxy_host_does_not_allow_restricted_dns() {
|
||||
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
|
||||
|
||||
let policy = NetworkPolicy {
|
||||
default: Decision::Allow.into(),
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
proxy: Vec::new(),
|
||||
proxy_fake_ip_cidrs: vec!["198.18.0.0/15".to_string()],
|
||||
audit: false,
|
||||
};
|
||||
let decider = NetworkPolicyDecider::new(policy, None);
|
||||
let ip = "198.18.0.1".parse().unwrap();
|
||||
|
||||
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url")
|
||||
.expect_err("fake-IP CIDR alone must not allow an untrusted hostname");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_host_never_exempts_real_private_or_local_addresses() {
|
||||
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
|
||||
|
||||
let policy = NetworkPolicy {
|
||||
default: Decision::Allow.into(),
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
proxy: vec!["github.com".to_string()],
|
||||
proxy_fake_ip_cidrs: vec![
|
||||
"198.18.0.0/15".to_string(),
|
||||
"127.0.0.0/8".to_string(),
|
||||
"10.0.0.0/8".to_string(),
|
||||
"169.254.0.0/16".to_string(),
|
||||
],
|
||||
audit: false,
|
||||
};
|
||||
let decider = NetworkPolicyDecider::new(policy, None);
|
||||
|
||||
for ip in [
|
||||
"127.0.0.1",
|
||||
"10.0.0.1",
|
||||
"192.168.1.1",
|
||||
"169.254.169.254",
|
||||
"fc00::1",
|
||||
] {
|
||||
let ip = ip.parse().unwrap();
|
||||
assert!(
|
||||
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url").is_err(),
|
||||
"{ip} must remain restricted"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_opt_in_does_not_allow_unlisted_host() {
|
||||
use crate::network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider};
|
||||
|
||||
let policy = NetworkPolicy {
|
||||
default: Decision::Allow.into(),
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
proxy: vec!["github.com".to_string()],
|
||||
proxy_fake_ip_cidrs: vec!["198.18.0.0/15".to_string()],
|
||||
audit: false,
|
||||
};
|
||||
let decider = NetworkPolicyDecider::new(policy, None);
|
||||
let ip = "198.18.0.1".parse().unwrap();
|
||||
|
||||
let err = validate_dns_resolved_ip("example.com", &ip, Some(&decider), "fetch_url")
|
||||
.expect_err("proxy opt-in must be scoped to configured hosts");
|
||||
|
||||
assert!(format!("{err}").contains("resolved IP 198.18.0.1 is a restricted address"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_dns_allow_is_audited() {
|
||||
use crate::network_policy::{
|
||||
Decision, NetworkAuditor, NetworkPolicy, NetworkPolicyDecider,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
let dir = tempdir().expect("tempdir");
|
||||
let auditor = NetworkAuditor::new(dir.path().join("audit.log"), true);
|
||||
let policy = NetworkPolicy {
|
||||
default: Decision::Allow.into(),
|
||||
allow: Vec::new(),
|
||||
deny: Vec::new(),
|
||||
proxy: vec!["github.com".to_string()],
|
||||
proxy_fake_ip_cidrs: vec!["198.18.0.0/15".to_string()],
|
||||
audit: true,
|
||||
};
|
||||
let decider = NetworkPolicyDecider::new(policy, Some(auditor));
|
||||
let ip = "198.18.0.1".parse().unwrap();
|
||||
|
||||
validate_dns_resolved_ip("github.com", &ip, Some(&decider), "fetch_url")
|
||||
.expect("proxy DNS allow");
|
||||
|
||||
let body = std::fs::read_to_string(dir.path().join("audit.log")).expect("audit log");
|
||||
assert!(body.contains("github.com"));
|
||||
assert!(body.contains("TrustedProxyFakeIp-Allow"));
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ use std::path::{Path, PathBuf};
|
||||
/// Maximum size accepted for any single Claude source file. Generous for
|
||||
/// real `~/.claude.json` files with many projects; bounded so a pathological
|
||||
/// file cannot exhaust memory. Matches the bundle reader's order of magnitude.
|
||||
pub(crate) const MAX_SOURCE_BYTES: u64 = 5 * 1024 * 1024;
|
||||
pub const MAX_SOURCE_BYTES: u64 = 5 * 1024 * 1024;
|
||||
|
||||
/// Env keys safe to propose for the portable bundle (lowercase). Deliberately
|
||||
/// a short allowlist: timeouts, output caps, and editor/telemetry choices.
|
||||
@@ -66,7 +66,7 @@ fn read_bounded_json(path: &Path) -> Result<Value, CommandSourceStatus> {
|
||||
}
|
||||
|
||||
/// Read the Claude sources under `home` (usually `~`).
|
||||
pub(crate) fn read_sources(
|
||||
pub fn read_sources(
|
||||
home: &Path,
|
||||
) -> (
|
||||
Vec<(PathBuf, CommandSourceStatus)>,
|
||||
@@ -109,7 +109,7 @@ fn safe_env_value(value: &Value) -> Option<String> {
|
||||
|
||||
/// Build the plan from already-read sources plus MCP candidates discovered by
|
||||
/// the existing external-import machinery (so server parsing stays single-sourced).
|
||||
pub(crate) fn build_plan(
|
||||
pub fn build_plan(
|
||||
sources: Vec<(PathBuf, CommandSourceStatus)>,
|
||||
claude: Option<Value>,
|
||||
settings: Option<Value>,
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod guard;
|
||||
pub mod import_claude;
|
||||
pub mod network_policy;
|
||||
pub mod proxy;
|
||||
@@ -1,8 +1,3 @@
|
||||
// Several public helpers in this module are exposed for future slash-command
|
||||
// wiring (`/network allow <host>`, `/network deny <host>`) and for the
|
||||
// approval-modal hook that v0.7.x adds incrementally. Dead-code warnings
|
||||
// would otherwise be noisy until those call sites land.
|
||||
#![allow(dead_code)]
|
||||
// Audit-write failure must route through `tracing::*`, not raw stderr —
|
||||
// see `runtime_log` for the scroll-demon rationale.
|
||||
#![deny(clippy::print_stdout)]
|
||||
@@ -443,7 +438,7 @@ impl NetworkAuditor {
|
||||
))
|
||||
}
|
||||
|
||||
/// Append one line. Best-effort: errors are logged via `eprintln!` but
|
||||
/// Append one line. Best-effort: errors are logged via `tracing` but
|
||||
/// never bubble back to the caller.
|
||||
pub fn record(&self, host: &str, tool: &str, decision_label: &str) {
|
||||
if !self.enabled {
|
||||
@@ -0,0 +1,190 @@
|
||||
use anyhow::Result;
|
||||
|
||||
/// Redact the userinfo segment (`username[:password]@…` portion) from
|
||||
/// a proxy URL so it can be safely included in `tracing::warn!` output
|
||||
/// without leaking the
|
||||
/// password into the on-disk log. URLs without userinfo are returned
|
||||
/// unchanged. Garbage input (no `://` scheme separator) is also returned
|
||||
/// unchanged — the malformed-URL warning path is the only caller, so an
|
||||
/// unparseable input is already the failure case.
|
||||
fn redact_proxy_userinfo(proxy_url: &str) -> String {
|
||||
let Some(scheme_end) = proxy_url.find("://") else {
|
||||
return proxy_url.to_string();
|
||||
};
|
||||
let after_scheme = scheme_end + 3;
|
||||
// The userinfo segment ends at the next `@`, but only if that `@`
|
||||
// comes before the next `/`, `?`, or `#` (otherwise the `@` is in a
|
||||
// path / query and the URL has no userinfo at all).
|
||||
let rest = &proxy_url[after_scheme..];
|
||||
let at_idx = rest.find('@');
|
||||
let path_idx = rest.find(['/', '?', '#']);
|
||||
let userinfo_end = match (at_idx, path_idx) {
|
||||
(Some(a), Some(p)) if a < p => Some(a),
|
||||
(Some(a), None) => Some(a),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(end) = userinfo_end {
|
||||
let mut out = String::with_capacity(proxy_url.len());
|
||||
out.push_str(&proxy_url[..after_scheme]);
|
||||
out.push_str("***@");
|
||||
out.push_str(&rest[end + 1..]);
|
||||
out
|
||||
} else {
|
||||
proxy_url.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the operator's proxy route for this exact request using the same
|
||||
/// matcher as reqwest. A NO_PROXY match returns None: direct requests must keep
|
||||
/// their public DNS validation and pins. Model and reviewed-plugin requests
|
||||
/// return before even reading proxy credentials.
|
||||
pub fn configured_mcp_proxy<F>(
|
||||
url: &reqwest::Url,
|
||||
disallow_ambient_proxy: bool,
|
||||
mut read_environment: F,
|
||||
) -> Result<Option<reqwest::Proxy>>
|
||||
where
|
||||
F: FnMut(&str) -> std::result::Result<String, std::env::VarError>,
|
||||
{
|
||||
if disallow_ambient_proxy {
|
||||
return Ok(None);
|
||||
}
|
||||
let proxy_url = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"]
|
||||
.into_iter()
|
||||
.find_map(|key| {
|
||||
read_environment(key)
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
});
|
||||
let Some(proxy_url) = proxy_url else {
|
||||
return Ok(None);
|
||||
};
|
||||
// Normalize userinfo and Unicode with the URL parser before passing the
|
||||
// URL to reqwest's own underlying matcher. Keep its missing-scheme support.
|
||||
let normalized = reqwest::Url::parse(&proxy_url)
|
||||
.ok()
|
||||
.filter(|url| url.has_host())
|
||||
.or_else(|| reqwest::Url::parse(&format!("http://{proxy_url}")).ok());
|
||||
let Some(normalized) = normalized else {
|
||||
tracing::warn!(target: "mcp", proxy = %redact_proxy_userinfo(&proxy_url), "ignoring malformed HTTP(S)_PROXY URL");
|
||||
return Ok(None);
|
||||
};
|
||||
let no_proxy = read_environment("NO_PROXY")
|
||||
.or_else(|_| read_environment("no_proxy"))
|
||||
.unwrap_or_default()
|
||||
.replace(char::is_whitespace, ",");
|
||||
if no_proxy.split(',').any(|entry| entry == "*") {
|
||||
return Ok(None);
|
||||
}
|
||||
let matcher = hyper_util::client::proxy::matcher::Matcher::builder()
|
||||
.all(normalized.as_str())
|
||||
.no(no_proxy)
|
||||
.build();
|
||||
let destination: http::Uri = url.as_str().parse()?;
|
||||
let Some(route) = matcher.intercept(&destination) else {
|
||||
return Ok(None);
|
||||
};
|
||||
// Build the actual proxy from the matched route itself so the decision
|
||||
// that grants delegated DNS authority cannot diverge from the transport.
|
||||
let route_uri = route.uri().to_string();
|
||||
let route_uri = match route.uri().scheme_str() {
|
||||
Some("socks5") => route_uri.replacen("socks5://", "socks5h://", 1),
|
||||
Some("socks4") => route_uri.replacen("socks4://", "socks4a://", 1),
|
||||
_ => route_uri,
|
||||
};
|
||||
let mut proxy = reqwest::Proxy::all(route_uri)?;
|
||||
if let Some(auth) = route.basic_auth() {
|
||||
proxy = proxy.custom_http_auth(auth.clone());
|
||||
}
|
||||
if let Some((user, password)) = route.raw_auth() {
|
||||
proxy = proxy.basic_auth(user, password);
|
||||
}
|
||||
Ok(Some(proxy))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reviewed_plugin_remote_proxy_policy_never_reads_ambient_environment() {
|
||||
let reads = std::cell::Cell::new(0_u32);
|
||||
let proxy = configured_mcp_proxy(
|
||||
&reqwest::Url::parse("https://example.com/mcp").unwrap(),
|
||||
true,
|
||||
|_| {
|
||||
reads.set(reads.get() + 1);
|
||||
Ok("http://127.0.0.1:9999".to_string())
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
reads.get(),
|
||||
0,
|
||||
"reviewed remotes must not read proxy values"
|
||||
);
|
||||
assert!(proxy.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_authored_mcp_proxy_policy_keeps_environment_support() {
|
||||
let requested = std::cell::RefCell::new(Vec::new());
|
||||
let proxy = configured_mcp_proxy(
|
||||
&reqwest::Url::parse("https://example.com/mcp").unwrap(),
|
||||
false,
|
||||
|name| {
|
||||
requested.borrow_mut().push(name.to_string());
|
||||
match name {
|
||||
"HTTPS_PROXY" => Ok("http://127.0.0.1:8080".to_string()),
|
||||
_ => Err(std::env::VarError::NotPresent),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
requested.into_inner(),
|
||||
vec![
|
||||
"HTTPS_PROXY".to_string(),
|
||||
"NO_PROXY".to_string(),
|
||||
"no_proxy".to_string(),
|
||||
]
|
||||
);
|
||||
assert!(proxy.unwrap().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redact_proxy_userinfo_strips_password() {
|
||||
// Corporate-style proxy URL with embedded creds — the
|
||||
// password must never reach the on-disk log file. URL strings
|
||||
// are assembled from placeholder constants via `format!` so the
|
||||
// literal source never contains a scheme-prefixed username +
|
||||
// password pair (colon-separated, `@`-terminated) that
|
||||
// GitGuardian's "Basic Auth String" detector would flag as a
|
||||
// committed credential.
|
||||
let (placeholder_user, placeholder_pass) = ("PLACEHOLDER_USER", "PLACEHOLDER_PASS");
|
||||
let with_creds = format!("http://{placeholder_user}:{placeholder_pass}@proxy.example/");
|
||||
let redacted = redact_proxy_userinfo(&with_creds);
|
||||
assert_eq!(redacted, "http://***@proxy.example/");
|
||||
assert!(!redacted.contains(placeholder_pass));
|
||||
assert!(!redacted.contains(placeholder_user));
|
||||
|
||||
// User only (no password) — still redacted.
|
||||
let with_user_only = format!("https://{placeholder_user}@proxy.example:8080");
|
||||
let redacted = redact_proxy_userinfo(&with_user_only);
|
||||
assert_eq!(redacted, "https://***@proxy.example:8080");
|
||||
|
||||
// No userinfo segment — pass through.
|
||||
let redacted = redact_proxy_userinfo("http://proxy.example:3128/");
|
||||
assert_eq!(redacted, "http://proxy.example:3128/");
|
||||
|
||||
// `@` appears only in the path, not as userinfo separator —
|
||||
// must not be mistaken for credentials.
|
||||
let redacted = redact_proxy_userinfo("http://proxy.example/path@thing");
|
||||
assert_eq!(redacted, "http://proxy.example/path@thing");
|
||||
|
||||
// Garbage input (no `://`) returned unchanged — the
|
||||
// surrounding warning log is the only caller and is already
|
||||
// handling the malformed-URL case.
|
||||
assert_eq!(redact_proxy_userinfo("not-a-url"), "not-a-url");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "codewhale-notify"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Shared Codewhale notification policy and local audio dispatch"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
test-thread-scoped-state = ["test-support"]
|
||||
|
||||
[dependencies]
|
||||
codewhale-config = { path = "../config", version = "0.10.2" }
|
||||
codewhale-secrets = { path = "../secrets", version = "0.10.2" }
|
||||
regex = "1.11"
|
||||
tempfile.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
codewhale-runtime = { path = "../runtime", version = "0.10.2", features = ["test-support"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
dirs.workspace = true
|
||||
windows = { version = "0.62", features = ["Win32_Media_Audio", "Win32_System_Diagnostics_Debug", "Win32_UI_WindowsAndMessaging"] }
|
||||
File renamed without changes.
@@ -27,7 +27,7 @@ pub fn emit_terminal(cue: &SoundCue, out: &mut dyn Write) -> AudioOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
pub const WHALE_WAV: &[u8] = include_bytes!("../../assets/audio/codewhale-whale-call.wav");
|
||||
pub const WHALE_WAV: &[u8] = include_bytes!("../assets/audio/codewhale-whale-call.wav");
|
||||
|
||||
/// Resolve an external system helper (the audio players `aplay` and the
|
||||
/// pet's `ffplay`, the Linux browser launcher `xdg-open`) from fixed install
|
||||
@@ -39,7 +39,7 @@ pub const WHALE_WAV: &[u8] = include_bytes!("../../assets/audio/codewhale-whale-
|
||||
/// custom `~/bin`, a version manager shim) is refused and the caller reports
|
||||
/// the feature as unavailable. That is deliberate — there is no `PATH`
|
||||
/// fallback.
|
||||
pub(crate) fn trusted_system_executable(name: &str) -> io::Result<PathBuf> {
|
||||
pub fn trusted_system_executable(name: &str) -> io::Result<PathBuf> {
|
||||
trusted_player_in(name, &trusted_player_dirs())
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ fn is_executable_file(path: &Path) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
|
||||
pub fn dispatch(cue: &SoundCue, out: &mut dyn Write) -> AudioOutcome {
|
||||
#[cfg(target_os = "windows")]
|
||||
if matches!(cue, SoundCue::Bell | SoundCue::Beep | SoundCue::DoubleBell) {
|
||||
@@ -130,16 +130,16 @@ pub fn dispatch(cue: &SoundCue, out: &mut dyn Write) -> AudioOutcome {
|
||||
dispatch_wav(cue)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, all(feature = "test-thread-scoped-state", debug_assertions)))]
|
||||
pub fn dispatch(_cue: &SoundCue, _out: &mut dyn Write) -> AudioOutcome {
|
||||
// Production entry points are also fail-closed in library tests.
|
||||
// Production entry points are also fail-closed in library and scoped debug tests.
|
||||
AudioOutcome::Unsupported
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
|
||||
static PLAYING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
|
||||
fn dispatch_wav(cue: &SoundCue) -> AudioOutcome {
|
||||
use std::sync::atomic::Ordering;
|
||||
if !cfg!(any(
|
||||
@@ -179,7 +179,7 @@ fn dispatch_wav(cue: &SoundCue) -> AudioOutcome {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[cfg(not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))))]
|
||||
fn play_wav(cue: &SoundCue) -> io::Result<()> {
|
||||
let mut bundled = None;
|
||||
let path = match cue {
|
||||
@@ -203,7 +203,10 @@ fn play_wav(cue: &SoundCue) -> io::Result<()> {
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(all(not(test), target_os = "windows"))]
|
||||
#[cfg(all(
|
||||
not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))),
|
||||
target_os = "windows"
|
||||
))]
|
||||
fn play_file(path: &std::path::Path) -> io::Result<()> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows::Win32::Media::Audio::{PlaySoundW, SND_FILENAME, SND_NODEFAULT};
|
||||
@@ -217,7 +220,10 @@ fn play_file(path: &std::path::Path) -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(test), any(target_os = "macos", target_os = "linux")))]
|
||||
#[cfg(all(
|
||||
not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))),
|
||||
any(target_os = "macos", target_os = "linux")
|
||||
))]
|
||||
fn play_file(path: &std::path::Path) -> io::Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let player = PathBuf::from("/usr/bin/afplay");
|
||||
@@ -237,7 +243,7 @@ fn play_file(path: &std::path::Path) -> io::Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
not(test),
|
||||
not(any(test, all(feature = "test-thread-scoped-state", debug_assertions))),
|
||||
not(any(target_os = "windows", target_os = "macos", target_os = "linux"))
|
||||
))]
|
||||
fn play_file(_path: &std::path::Path) -> io::Result<()> {
|
||||
@@ -16,7 +16,7 @@ use super::{DeliveryOutcome, Method, NotificationGate};
|
||||
/// - `$TERM` contains `kitty` → `Kitty`
|
||||
/// - Unknown terminal → `Off` (never invent an audible fallback)
|
||||
#[must_use]
|
||||
pub(crate) fn resolve_method() -> Method {
|
||||
pub fn resolve_method() -> Method {
|
||||
let term_program = std::env::var("TERM_PROGRAM").unwrap_or_default();
|
||||
match term_program.as_str() {
|
||||
"iTerm.app" | "WezTerm" | "Cmux" => return Method::Osc9,
|
||||
@@ -108,7 +108,7 @@ fn build_escape(method: Method, in_tmux: bool, msg: &str) -> Vec<u8> {
|
||||
|
||||
/// All side effects sit behind injected sinks. A disallowed event reaches none.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn notify_with_sinks(
|
||||
pub fn notify_with_sinks(
|
||||
method: Method,
|
||||
in_tmux: bool,
|
||||
payload: &NotificationPayload,
|
||||
@@ -117,18 +117,15 @@ pub(crate) fn notify_with_sinks(
|
||||
gate: NotificationGate,
|
||||
attention_allowed: bool,
|
||||
sink: &mut dyn Write,
|
||||
decide_sound: &mut dyn FnMut(
|
||||
NotificationKind,
|
||||
bool,
|
||||
) -> crate::notify::sound_policy::SoundDecision,
|
||||
decide_sound: &mut dyn FnMut(NotificationKind, bool) -> crate::sound_policy::SoundDecision,
|
||||
audio: &mut dyn FnMut(
|
||||
&crate::notify::sound_policy::SoundCue,
|
||||
&crate::sound_policy::SoundCue,
|
||||
&mut dyn Write,
|
||||
) -> crate::notify::audio::AudioOutcome,
|
||||
) -> crate::audio::AudioOutcome,
|
||||
native: &mut dyn FnMut(&NotificationPayload) -> DeliveryOutcome,
|
||||
) -> DeliveryOutcome {
|
||||
use crate::notify::audio::AudioOutcome;
|
||||
use crate::notify::sound_policy::SoundDecision;
|
||||
use crate::audio::AudioOutcome;
|
||||
use crate::sound_policy::SoundDecision;
|
||||
if !attention_allowed {
|
||||
return DeliveryOutcome::SuppressedByAttention;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
//! delivery-method and category gate, attention (focus) rules, the
|
||||
//! per-event sound policy and audio cues.
|
||||
//!
|
||||
//! Runtime code owns *what* may notify and *which* sound it selects. The
|
||||
//! This crate owns *what* may notify and *which* sound it selects. The
|
||||
//! terminal UI owns the *terminal*: `tui::notifications` is the only code that
|
||||
//! hands a real terminal to [`delivery::notify_with_sinks`], writes the taskbar
|
||||
//! progress sequence or the window title. Runtime callers that need a delivery
|
||||
@@ -21,7 +21,9 @@ pub mod sound_policy;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::{NotificationCondition, NotificationMethod, NotificationsConfig};
|
||||
use codewhale_config::notifications::{
|
||||
NotificationCondition, NotificationMethod, NotificationsConfig,
|
||||
};
|
||||
use payload::NotificationKind;
|
||||
|
||||
/// Notification delivery method.
|
||||
@@ -197,7 +199,7 @@ impl NotificationGate {
|
||||
const ELEVATION_NEEDED_BIT: u8 = 1 << 5;
|
||||
const MODEL_NOTIFY_BIT: u8 = 1 << 6;
|
||||
|
||||
pub(crate) const fn to_bits(self) -> u8 {
|
||||
pub const fn to_bits(self) -> u8 {
|
||||
(self.quiet as u8 * Self::QUIET_BIT)
|
||||
| (self.turn_complete as u8 * Self::TURN_COMPLETE_BIT)
|
||||
| (self.subagent_terminal as u8 * Self::SUBAGENT_TERMINAL_BIT)
|
||||
@@ -207,7 +209,7 @@ impl NotificationGate {
|
||||
| (self.model_notify as u8 * Self::MODEL_NOTIFY_BIT)
|
||||
}
|
||||
|
||||
pub(crate) const fn from_bits(bits: u8) -> Self {
|
||||
pub const fn from_bits(bits: u8) -> Self {
|
||||
Self {
|
||||
quiet: bits & Self::QUIET_BIT != 0,
|
||||
turn_complete: bits & Self::TURN_COMPLETE_BIT != 0,
|
||||
@@ -226,15 +228,15 @@ impl NotificationGate {
|
||||
/// until the terminal explicitly reports `FocusLost`, so the safe startup
|
||||
/// behavior is silence rather than an unexpected banner or bell.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum AttentionCondition {
|
||||
pub enum AttentionCondition {
|
||||
Always = 0,
|
||||
Unfocused = 1,
|
||||
Never = 2,
|
||||
}
|
||||
|
||||
pub(crate) const DEFAULT_UNFOCUSED_GRACE: Duration = Duration::from_secs(2);
|
||||
pub const DEFAULT_UNFOCUSED_GRACE: Duration = Duration::from_secs(2);
|
||||
#[must_use]
|
||||
pub(crate) fn attention_delivery_allowed_at(
|
||||
pub fn attention_delivery_allowed_at(
|
||||
condition: AttentionCondition,
|
||||
focused: bool,
|
||||
unfocused_since_ms: u64,
|
||||
@@ -254,7 +256,7 @@ pub(crate) fn attention_delivery_allowed_at(
|
||||
|
||||
/// Native hosts provide focus observations; the same grace/condition rule
|
||||
/// applies before either native sound or banner preparation.
|
||||
pub(crate) fn native_attention_allowed(
|
||||
pub fn native_attention_allowed(
|
||||
config: &NotificationsConfig,
|
||||
focused: bool,
|
||||
unfocused_for: Duration,
|
||||
@@ -214,14 +214,6 @@ impl NotificationPayload {
|
||||
}
|
||||
|
||||
/// Bounded, sanitized headline. Never empty.
|
||||
///
|
||||
/// Only the macOS path reads this today — `display notification` is the
|
||||
/// one backend that takes a separate subtitle, while the escape-sequence
|
||||
/// backends send a single string via [`Self::body`]. Tests exercise it on
|
||||
/// every platform, but `#[cfg(test)]` uses do not keep it alive in a
|
||||
/// non-macOS release build, so the allow is scoped to exactly that case
|
||||
/// rather than blanket-silencing dead_code on the accessor.
|
||||
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
|
||||
#[must_use]
|
||||
pub fn headline(&self) -> &str {
|
||||
&self.headline
|
||||
@@ -3,8 +3,8 @@
|
||||
//! one cue and preserves per-category repeat history across settings updates.
|
||||
|
||||
use super::payload::NotificationKind;
|
||||
use crate::config::{CompletionSound, NotificationsConfig};
|
||||
pub use codewhale_config::notifications::NotificationEvent as SoundEvent;
|
||||
use codewhale_config::notifications::{CompletionSound, NotificationsConfig};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
|
||||
@@ -199,7 +199,7 @@ pub fn decide_configured(
|
||||
.unwrap_or(SoundDecision::Suppress(SuppressReason::Disabled))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn configure(policy: EventSoundPolicy) {
|
||||
*policy_cell().write().unwrap() = policy;
|
||||
}
|
||||
@@ -207,7 +207,7 @@ pub fn configure(policy: EventSoundPolicy) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::EventSoundConfig;
|
||||
use codewhale_config::notifications::EventSoundConfig;
|
||||
|
||||
#[test]
|
||||
fn canonical_whale_controls_all_six_categories_over_legacy_controls() {
|
||||
@@ -314,7 +314,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn settings_refresh_preserves_repeat_history() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let _lock = codewhale_runtime::test_support::lock_test_env();
|
||||
let config = NotificationsConfig {
|
||||
sound: Some(CompletionSound::Whale),
|
||||
..Default::default()
|
||||
@@ -334,7 +334,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn configured_off_decision_ignores_intervening_installed_whale_policy() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let _lock = codewhale_runtime::test_support::lock_test_env();
|
||||
let off = NotificationsConfig {
|
||||
sound: Some(CompletionSound::Off),
|
||||
..Default::default()
|
||||
@@ -367,7 +367,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn configured_sound_keeps_installed_defaults_and_suppression_keeps_history() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let _lock = codewhale_runtime::test_support::lock_test_env();
|
||||
let off = NotificationsConfig {
|
||||
sound: Some(CompletionSound::Off),
|
||||
..Default::default()
|
||||
@@ -412,7 +412,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn concurrent_configured_decisions_share_history_with_settings_refresh() {
|
||||
let _lock = crate::test_support::lock_test_env();
|
||||
let _lock = codewhale_runtime::test_support::lock_test_env();
|
||||
configure(EventSoundPolicy::default());
|
||||
let whale = NotificationsConfig {
|
||||
sound: Some(CompletionSound::Whale),
|
||||
@@ -264,9 +264,20 @@ pub struct McpManagerSnapshot {
|
||||
/// Structured clarification request (`request_user_input`).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UserInputRequest {
|
||||
#[serde(default)]
|
||||
pub require_typed_answers: bool,
|
||||
pub questions: Vec<UserInputQuestionEvent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UserInputClosedOutcome {
|
||||
Answered,
|
||||
Cancelled,
|
||||
TimedOut,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// Which permission gate produced a `ToolGateDecision`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -743,6 +754,12 @@ pub enum EventMsg {
|
||||
id: String,
|
||||
request: UserInputRequest,
|
||||
},
|
||||
UserInputClosed {
|
||||
thread_id: ThreadId,
|
||||
session_id: SessionId,
|
||||
id: String,
|
||||
outcome: UserInputClosedOutcome,
|
||||
},
|
||||
/// Authoritative API conversation state (`Message`s / `SystemPrompt`
|
||||
/// serialized).
|
||||
SessionUpdated {
|
||||
@@ -752,6 +769,8 @@ pub enum EventMsg {
|
||||
messages: Vec<Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
system_prompt: Option<Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
instruction_assembly: Option<Value>,
|
||||
model: String,
|
||||
workspace: PathBuf,
|
||||
},
|
||||
@@ -871,6 +890,7 @@ pub const EVENT_KINDS: &[&str] = &[
|
||||
"approval_required",
|
||||
"approval_withdrawn",
|
||||
"user_input_required",
|
||||
"user_input_closed",
|
||||
"session_updated",
|
||||
"elevation_required",
|
||||
"lsp_repair_update",
|
||||
@@ -932,6 +952,7 @@ impl EventMsg {
|
||||
Self::ApprovalRequired { .. } => "approval_required",
|
||||
Self::ApprovalWithdrawn { .. } => "approval_withdrawn",
|
||||
Self::UserInputRequired { .. } => "user_input_required",
|
||||
Self::UserInputClosed { .. } => "user_input_closed",
|
||||
Self::SessionUpdated { .. } => "session_updated",
|
||||
Self::ElevationRequired { .. } => "elevation_required",
|
||||
Self::LspRepairUpdate { .. } => "lsp_repair_update",
|
||||
@@ -993,6 +1014,7 @@ impl EventMsg {
|
||||
| Self::ApprovalRequired { thread_id, .. }
|
||||
| Self::ApprovalWithdrawn { thread_id, .. }
|
||||
| Self::UserInputRequired { thread_id, .. }
|
||||
| Self::UserInputClosed { thread_id, .. }
|
||||
| Self::SessionUpdated { thread_id, .. }
|
||||
| Self::ElevationRequired { thread_id, .. }
|
||||
| Self::LspRepairUpdate { thread_id, .. }
|
||||
@@ -1054,6 +1076,7 @@ impl EventMsg {
|
||||
| Self::ApprovalRequired { session_id, .. }
|
||||
| Self::ApprovalWithdrawn { session_id, .. }
|
||||
| Self::UserInputRequired { session_id, .. }
|
||||
| Self::UserInputClosed { session_id, .. }
|
||||
| Self::SessionUpdated { session_id, .. }
|
||||
| Self::ElevationRequired { session_id, .. }
|
||||
| Self::LspRepairUpdate { session_id, .. }
|
||||
@@ -1480,22 +1503,35 @@ mod tests {
|
||||
session_id: s.clone(),
|
||||
id: "c2".into(),
|
||||
request: UserInputRequest {
|
||||
require_typed_answers: true,
|
||||
questions: vec![UserInputQuestionEvent {
|
||||
header: "h".into(),
|
||||
id: "q1".into(),
|
||||
question: "?".into(),
|
||||
detail: Some("Exact detail".into()),
|
||||
intent: Some(crate::UserInputIntentEvent::PlanReview {
|
||||
approve: "Proceed".into(),
|
||||
call_id: Some("plan-1".into()),
|
||||
}),
|
||||
options: vec![],
|
||||
allow_free_text: true,
|
||||
multi_select: false,
|
||||
}],
|
||||
},
|
||||
},
|
||||
EventMsg::UserInputClosed {
|
||||
thread_id: t.clone(),
|
||||
session_id: s.clone(),
|
||||
id: "c2".into(),
|
||||
outcome: UserInputClosedOutcome::TimedOut,
|
||||
},
|
||||
EventMsg::SessionUpdated {
|
||||
thread_id: t.clone(),
|
||||
session_id: s.clone(),
|
||||
engine_session_id: "sess".into(),
|
||||
messages: vec![json!({"role": "user", "content": []})],
|
||||
system_prompt: Some(json!("sys")),
|
||||
instruction_assembly: None,
|
||||
model: "m".into(),
|
||||
workspace: PathBuf::from("/ws"),
|
||||
},
|
||||
|
||||
@@ -17,6 +17,26 @@ use serde_json::Value;
|
||||
|
||||
use super::Status;
|
||||
|
||||
/// Explicit boundary migration for pre-Fleet serialized role tokens.
|
||||
///
|
||||
/// Call this only at load / parse edges. Runtime code must use Fleet role
|
||||
/// names via `FleetRole::as_str`. Returns `None` for tokens that are
|
||||
/// already canonical or unknown — callers should prefer `FleetRole::parse`
|
||||
/// for full acceptance (canonical + legacy).
|
||||
#[must_use]
|
||||
pub fn migrate_legacy_role_token(token: &str) -> Option<&'static str> {
|
||||
match token.trim().to_ascii_lowercase().as_str() {
|
||||
"worker" | "general-purpose" | "general_purpose" | "default" => Some("general"),
|
||||
"scout" | "exploration" | "explorer" => Some("explore"),
|
||||
"plan" | "planning" | "awaiter" => Some("planner"),
|
||||
"review" | "code-review" | "code_review" => Some("reviewer"),
|
||||
"builder" | "implementer" | "implementation" => Some("implement"),
|
||||
"verifier" | "verify" | "verification" | "validator" | "tester" => Some("test"),
|
||||
"consultant" | "oracle" => Some("advisor"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const FLEET_PROTOCOL_VERSION: &str = "0.1.0";
|
||||
|
||||
/// Globally unique identifier for a fleet run.
|
||||
@@ -1872,3 +1892,120 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime delegation has no default product depth limit. Explicit owner
|
||||
/// values still restrict child authority; zero disables delegation.
|
||||
pub const DEFAULT_SPAWN_DEPTH: u32 = u32::MAX;
|
||||
|
||||
/// Shell posture, ordered most → least restrictive so `min_with` is the safe
|
||||
/// side of a clamp.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ShellCeiling {
|
||||
None,
|
||||
ReadOnly,
|
||||
Full,
|
||||
}
|
||||
|
||||
impl ShellCeiling {
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::ReadOnly => "read_only",
|
||||
Self::Full => "full",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn min_with(self, other: Self) -> Self {
|
||||
if self <= other { self } else { other }
|
||||
}
|
||||
}
|
||||
|
||||
/// A Runtime child-authority envelope.
|
||||
///
|
||||
/// This type is intentionally not part of `ExactMember`. Exact Fleet files
|
||||
/// choose identity, route, and reasoning; Runtime intersects its role posture
|
||||
/// with the live parent after member selection. Keeping the envelope as a
|
||||
/// separate type lets receipts describe the authority that Runtime actually
|
||||
/// installed without turning a saved Fleet into a trust boundary.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PermissionCeiling {
|
||||
pub write: bool,
|
||||
/// Whether the Runtime child may be handed a **model-visible network tool**
|
||||
/// (fetch, browse, HTTP).
|
||||
///
|
||||
/// This is deliberately *not* a statement about transport. Host-owned
|
||||
/// provider inference — the ordinary API call CodeWhale makes on the
|
||||
/// member's behalf — always happens over the network and is not governed
|
||||
/// by this field. A child with `network_tool = false` still runs on a
|
||||
/// remote model; it simply has no tool with which to reach the network
|
||||
/// itself. Receipts disclose that distinction rather than implying an
|
||||
/// air-gap.
|
||||
#[serde(alias = "network")]
|
||||
pub network_tool: bool,
|
||||
pub shell: ShellCeiling,
|
||||
/// Nested-delegation budget the Runtime child may consume.
|
||||
pub delegation_depth: u32,
|
||||
/// Whether the Runtime child may be handed tools at all.
|
||||
pub tools: bool,
|
||||
}
|
||||
|
||||
impl PermissionCeiling {
|
||||
/// The Router's fixed posture: no tools (so no network tool), no shell, no
|
||||
/// writes, no delegation. Not configurable — see `RouterMember`.
|
||||
///
|
||||
/// The Router itself is still *inferred* by its configured provider over
|
||||
/// the network; that is host-owned transport, disclosed on the receipt.
|
||||
pub const ROUTER: Self = Self {
|
||||
write: false,
|
||||
network_tool: false,
|
||||
shell: ShellCeiling::None,
|
||||
delegation_depth: 0,
|
||||
tools: false,
|
||||
};
|
||||
|
||||
/// Legacy named presets retained for Runtime compatibility and tests.
|
||||
///
|
||||
/// The exact Fleet parser accepts a historic `permissions` key only as
|
||||
/// ignored input; no preset selected here is projected onto a member.
|
||||
pub fn preset(name: &str) -> Option<Self> {
|
||||
let base = |write, network_tool, shell, delegation_depth| Self {
|
||||
write,
|
||||
network_tool,
|
||||
shell,
|
||||
delegation_depth,
|
||||
tools: true,
|
||||
};
|
||||
match name.trim().to_ascii_lowercase().as_str() {
|
||||
"none" => Some(Self::ROUTER),
|
||||
"analyst" => Some(base(false, false, ShellCeiling::None, 0)),
|
||||
"read_only" | "readonly" => Some(base(false, false, ShellCeiling::ReadOnly, 0)),
|
||||
"tester" | "verifier" => Some(base(false, false, ShellCeiling::Full, 0)),
|
||||
"read_write" | "readwrite" => Some(base(true, false, ShellCeiling::Full, 0)),
|
||||
"full" => Some(base(true, true, ShellCeiling::Full, 1)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Narrow this ceiling against the active session posture. Every field
|
||||
/// takes the more restrictive side, so the result can never grant more
|
||||
/// than either input.
|
||||
#[must_use]
|
||||
pub fn clamp_to(self, session: Self) -> Self {
|
||||
Self {
|
||||
write: self.write && session.write,
|
||||
network_tool: self.network_tool && session.network_tool,
|
||||
shell: self.shell.min_with(session.shell),
|
||||
delegation_depth: self.delegation_depth.min(session.delegation_depth),
|
||||
tools: self.tools && session.tools,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PermissionCeiling {
|
||||
fn default() -> Self {
|
||||
Self::preset("read_only").expect("read_only is a known preset")
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user