mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 10:36:05 -10:00
fix(security): close a daemon-socket sandbox escape and a codewhale web proof leak before 0.10.2
Found by a pre-release audit of the local network surfaces (Claude
workflow wf_d93a93bb-48d: three auditors plus adversarial verification
of every finding; a Gemini 4 Argon read-only review through agy found
nothing exploitable by default). Founder, 2026-10-18: pause account work
and hold the paid product until security review is stronger; this keeps
the local release to the same bar. Two unrefuted High findings and one
Medium are fixed here; the rest are listed in 0.10.2's Known limits.
1. Sandbox escape through the Runtime daemon socket (High).
Sandboxed commands (test scripts, postinstall hooks) could read the
owner receipt and connect to the daemon socket that `codewhale serve`,
the desktop app, the VS Code extension or ACP holds. The socket checks
only the peer uid, and an attached Control peer reaches the full
dispatcher, including app/config/set (sandbox_mode, approval_policy,
providers.*.base_url) and prompt/run. On Linux, bubblewrap's
--unshare-all does not stop a path-based AF_UNIX connect. On macOS,
Seatbelt with network on allows it.
- read_guard.rs: the daemon's default run directories, as this process
resolves them (CODEWHALE_HOME/run, $XDG_RUNTIME_DIR/codewhale,
~/Library/Application Support/codewhale, ~/.codewhale/run; an invalid
CODEWHALE_HOME falls back to the other layouts), join the default
sandbox deny-list. bubblewrap
masks an existing denied directory with an empty tmpfs, so the
socket and receipt are not there inside the sandbox.
- seatbelt.rs: a file-read deny does not stop connect(2). Checked with
sandbox-exec: with (allow network-outbound), the read deny alone
still connected, and adding (deny network-outbound (remote
unix-socket (subpath …))) refused it. With network on, every denied
path now gets that rule.
Live check (macOS, debug TUI, mock provider, CODEWHALE_SANDBOX_NETWORK_ACCESS=1,
a listener at <CODEWHALE_HOME>/run/t.sock; release-0102-refreeze-20261018/):
before "SOCKET-CONNECTED b'hi'", after "PermissionError: [Errno 1]".
Linux bubblewrap was not run end to end here; it relies on the existing
mask path proven for #7007.
2. `codewhale web` proof leak (High).
GET / wrote the live request proof into a meta tag for any request
carrying the session cookie and Sec-Fetch-Site: same-origin|none. The
cookie is host-scoped, not port-scoped, so a listener on another
127.0.0.1 port can receive it, and a non-browser client can claim any
Sec-Fetch-Site. Cookie plus proof opens every /v1 route (threads with
auto_approve, approvals, terminal input, config).
- web.rs: the page is static; the proof is never put in HTML.
index.html drops the meta tag; app.mjs drops pageProof and keeps the
proof in sessionStorage, so a reload still works. The existing
source assertion that app.mjs never uses localStorage stays.
- Behaviour change: a new tab opened by typing the address is not
signed in. docs/WEB.md and docs/RUNTIME_API.md say so and say what
to do.
Live check (curl against `codewhale web`, harness HOME, bootstrap
redeemed, then GET / with the cookie and Sec-Fetch-Site: none):
before "PROOF RE-ISSUED", after "no proof in page". Headless Chrome
over CDP on the fixed build: bootstrap ready with the proof in
sessionStorage and nothing in localStorage, reload ready, and a typed
new tab shows "not authenticated".
3. Web approval card spoofing (Medium).
When the model had written anything, the card showed only
`intent_summary` (the model's own text) and never the input. It now
shows the deterministic `summary`; then `description`, the runtime's
reason for asking (sandbox escalation, auto-review hold, repo law,
Computer Use consent), when it differs; then every argument in a <pre>:
the command first, then all other fields as JSON (sandbox_permissions,
justification, stdin, cwd, task_id ...), not truncated; and the model's
text last, labelled "The model says:". Headless Chrome with the mock
provider:
- phase3: summary "Run `echo later-01`", request "echo later-01"
- phase10 (bash with sandbox_permissions=danger-full-access): reason
"Sandbox escalation to 'danger-full-access' for this exact call: needs
the whole disk", request shows the command and the escalation fields
Tests changed for the new behaviour:
- web.rs runtime_surface_review_web_page_recovers_proof_for_reload_and_second_tab
becomes ..._web_page_never_carries_the_request_proof.
- runtime_api/tests.rs: the reload loop asserts the proof is absent.
- runtime_web_client.test.mjs: the independent-tab test becomes "web
session uses only its fragment or this tab's stored proof".
Checks:
- cargo test -p codewhale-tui --lib -- runtime_api:: runtime_surface
sandbox:: read_guard seatbelt bwrap provider_lake: 603 passed; 0 failed
- cargo test -p codewhale-commands: 28 passed; 0 failed
- node --test crates/tui/tests/runtime_web_client.test.mjs: 43/43; vitest
runtime_web app.test.mjs: 29/29
- after the review fixes: cargo test -p codewhale-tui --lib -- sandbox::
read_guard seatbelt bwrap runtime_surface runtime_api::web: 114 passed;
0 failed; clippy (tui, CI flags) clean; web tests 43/43 and 29/29
- cargo clippy -p codewhale-tui -p codewhale-commands --all-targets
--all-features --locked with CI's flags: clean; cargo fmt --check: clean
- npm test: 101/101, 19/19, 413 passed + 7 skipped of 420, 7/7, vitest
772/772; web prebuild + check-facts + check:docs OK; sync-changelog
--check up to date
Pre-push review (Claude workflow wf_626dd2b1-3e0, three lenses; Gemini 4
Argon via agy on the diff) found no bypass of the default setup and
these, all addressed before push:
- The card hid the runtime's `description` (a regression for sandbox
escalations) and dropped every input field but `command`. Fixed as in 3.
- An invalid (relative) CODEWHALE_HOME made runtime_daemon_socket_dirs
return nothing. Fixed with the fallback above.
- docs/zh_hans/RUNTIME_API.md still described the meta-tag recovery, and
zh_hans/WEB.md lacked the new-tab note. Both updated.
- The changelog overstated coverage. It now says the default deny-list
must stay on, and the gaps are listed under Known limits.
Unverified and not acted on: a claimed window after a TUI workspace switch
before the respawned engine installs the deny-list (recorded in the
release packet).
Known limits, listed in 0.10.2: no Host/Origin check under --insecure and
--insecure-no-auth; the bootstrap nonce passes through the browser
launcher's argv; the opt-in control socket has no peer-uid check; OAuth
loopback callbacks can be ended by a malformed request; the daemon-socket
deny covers only default locations as this process sees them, not a
directory created after a long-running command started, and not a macOS
workspace at or above the daemon directory (the directory can be moved).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
1 parent
7146817746
commit
0d347f5566
15 files changed
+253
-122
No files matched your search
+52
-7
@@ -11,16 +11,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
Codewhale v0.10.2 adds an animated pet view (`/pet on`), a live Terminal work
|
||||
dock and per-chat composer colors (`/color`). It closes three session
|
||||
data-loss paths, holds a `curl` or `wget` that uploads a credential, refuses
|
||||
shell commands containing invisible Unicode, keeps the Linux sandbox working when a
|
||||
hidden credential directory is a symlink, and uses much less CPU while the
|
||||
TUI waits on you. `/undo` takes back a whole request, `/diff` works outside a
|
||||
Git repository, DeepSeek's Responses route no longer rejects replayed tool
|
||||
history, and contributor fixes improve token accounting, relay model lists,
|
||||
localized replies and Windows launcher guidance.
|
||||
data-loss paths, a sandbox escape through the Runtime daemon socket and a
|
||||
`codewhale web` session leak; holds a `curl` or `wget` that uploads a
|
||||
credential; refuses shell commands containing invisible Unicode; keeps the
|
||||
Linux sandbox working when a hidden credential directory is a symlink; and
|
||||
uses much less CPU while the TUI waits on you. `/undo` takes back a whole
|
||||
request, `/diff` works outside a Git repository, DeepSeek's Responses route no
|
||||
longer rejects replayed tool history, and contributor fixes improve token
|
||||
accounting, relay model lists, localized replies and Windows launcher
|
||||
guidance.
|
||||
|
||||
### Latest fixes
|
||||
|
||||
- Sandboxed commands can no longer reach Codewhale's Runtime daemon socket.
|
||||
On Linux with bubblewrap, and on macOS with sandbox network access on, a
|
||||
command the agent ran (a test script or a postinstall hook, say) could read
|
||||
the owner receipt and attach to the socket that `codewhale serve`, the
|
||||
desktop app, the VS Code extension or an ACP session holds. From there it
|
||||
could change the sandbox mode, the approval policy or a provider's base URL
|
||||
in your config, or start an unsandboxed turn. The daemon's default run
|
||||
directories are now on the sandbox's default deny-list, and on macOS no
|
||||
socket under a denied path can be connected to. Turning the default
|
||||
deny-list off (`sandbox_read_denylist_defaults = false`) also turns this
|
||||
off.
|
||||
- `codewhale web` no longer gives its request proof to whoever holds the
|
||||
session cookie. The cookie is scoped to the host, not the port, so a
|
||||
listener on another `127.0.0.1` port could receive it, and `GET /` then
|
||||
returned the proof that unlocks the whole Runtime API. The proof now stays
|
||||
in the tab that opened the session, which keeps working across reloads
|
||||
when the browser allows session storage; a new tab opened by typing the
|
||||
address needs a fresh `codewhale web`.
|
||||
- The `codewhale web` approval card shows the request summary, Codewhale's
|
||||
reason for asking (such as a sandbox escalation) and every argument of the
|
||||
call in full, with the model's own description below, labeled as such.
|
||||
Before, when the model had written anything, the card showed only that
|
||||
text and never the command, so a prompt-injected model could describe a
|
||||
harmful command as a harmless one.
|
||||
- `/settings` choice lists that don't fit, such as Language and Theme, show
|
||||
a scroll rail. Thanks @Lstarsky0 (#7024).
|
||||
- During an `/mcp` search that finds nothing, the status line says "No
|
||||
@@ -1057,6 +1083,25 @@ localized replies and Windows launcher guidance.
|
||||
|
||||
### Known limits
|
||||
|
||||
- From a pre-release security review of the local network surfaces, not yet
|
||||
fixed: `codewhale serve --insecure` and `codewhale app-server
|
||||
--insecure-no-auth` check neither the `Host` nor the `Origin` header, so
|
||||
with either flag a website can reach the Runtime API through DNS
|
||||
rebinding; use them only on a machine you trust. The one-time
|
||||
`codewhale web` and `--mobile` bootstrap capability passes through the
|
||||
browser launcher's arguments, where another local OS user can read it. The
|
||||
opt-in control socket (`[control_socket] enabled = true`) does not check
|
||||
the connecting user and can be reached from sandboxed commands. OAuth
|
||||
loopback callbacks end a sign-in on the first malformed request, so a page
|
||||
or local process can make one fail.
|
||||
- The sandbox deny-list for the Runtime daemon socket covers its default
|
||||
locations as this process sees them. It does not cover a daemon started
|
||||
with `--socket-path` or with a different `XDG_RUNTIME_DIR` or
|
||||
`CODEWHALE_HOME`; a run directory created after a long-running sandboxed
|
||||
command started is not hidden from that command; and on macOS, when the
|
||||
workspace is your home directory or another folder above the daemon's
|
||||
directory, a sandboxed command can still move that directory out of the
|
||||
denied path.
|
||||
- Ambiguous current-quota 429s without a body retry delay of at most 120 s
|
||||
still end the turn; a header alone does not change that classification.
|
||||
Explicit exhausted account, billing and subscription limits remain terminal
|
||||
|
||||
+52
-7
@@ -11,16 +11,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
Codewhale v0.10.2 adds an animated pet view (`/pet on`), a live Terminal work
|
||||
dock and per-chat composer colors (`/color`). It closes three session
|
||||
data-loss paths, holds a `curl` or `wget` that uploads a credential, refuses
|
||||
shell commands containing invisible Unicode, keeps the Linux sandbox working when a
|
||||
hidden credential directory is a symlink, and uses much less CPU while the
|
||||
TUI waits on you. `/undo` takes back a whole request, `/diff` works outside a
|
||||
Git repository, DeepSeek's Responses route no longer rejects replayed tool
|
||||
history, and contributor fixes improve token accounting, relay model lists,
|
||||
localized replies and Windows launcher guidance.
|
||||
data-loss paths, a sandbox escape through the Runtime daemon socket and a
|
||||
`codewhale web` session leak; holds a `curl` or `wget` that uploads a
|
||||
credential; refuses shell commands containing invisible Unicode; keeps the
|
||||
Linux sandbox working when a hidden credential directory is a symlink; and
|
||||
uses much less CPU while the TUI waits on you. `/undo` takes back a whole
|
||||
request, `/diff` works outside a Git repository, DeepSeek's Responses route no
|
||||
longer rejects replayed tool history, and contributor fixes improve token
|
||||
accounting, relay model lists, localized replies and Windows launcher
|
||||
guidance.
|
||||
|
||||
### Latest fixes
|
||||
|
||||
- Sandboxed commands can no longer reach Codewhale's Runtime daemon socket.
|
||||
On Linux with bubblewrap, and on macOS with sandbox network access on, a
|
||||
command the agent ran (a test script or a postinstall hook, say) could read
|
||||
the owner receipt and attach to the socket that `codewhale serve`, the
|
||||
desktop app, the VS Code extension or an ACP session holds. From there it
|
||||
could change the sandbox mode, the approval policy or a provider's base URL
|
||||
in your config, or start an unsandboxed turn. The daemon's default run
|
||||
directories are now on the sandbox's default deny-list, and on macOS no
|
||||
socket under a denied path can be connected to. Turning the default
|
||||
deny-list off (`sandbox_read_denylist_defaults = false`) also turns this
|
||||
off.
|
||||
- `codewhale web` no longer gives its request proof to whoever holds the
|
||||
session cookie. The cookie is scoped to the host, not the port, so a
|
||||
listener on another `127.0.0.1` port could receive it, and `GET /` then
|
||||
returned the proof that unlocks the whole Runtime API. The proof now stays
|
||||
in the tab that opened the session, which keeps working across reloads
|
||||
when the browser allows session storage; a new tab opened by typing the
|
||||
address needs a fresh `codewhale web`.
|
||||
- The `codewhale web` approval card shows the request summary, Codewhale's
|
||||
reason for asking (such as a sandbox escalation) and every argument of the
|
||||
call in full, with the model's own description below, labeled as such.
|
||||
Before, when the model had written anything, the card showed only that
|
||||
text and never the command, so a prompt-injected model could describe a
|
||||
harmful command as a harmless one.
|
||||
- `/settings` choice lists that don't fit, such as Language and Theme, show
|
||||
a scroll rail. Thanks @Lstarsky0 (#7024).
|
||||
- During an `/mcp` search that finds nothing, the status line says "No
|
||||
@@ -1057,6 +1083,25 @@ localized replies and Windows launcher guidance.
|
||||
|
||||
### Known limits
|
||||
|
||||
- From a pre-release security review of the local network surfaces, not yet
|
||||
fixed: `codewhale serve --insecure` and `codewhale app-server
|
||||
--insecure-no-auth` check neither the `Host` nor the `Origin` header, so
|
||||
with either flag a website can reach the Runtime API through DNS
|
||||
rebinding; use them only on a machine you trust. The one-time
|
||||
`codewhale web` and `--mobile` bootstrap capability passes through the
|
||||
browser launcher's arguments, where another local OS user can read it. The
|
||||
opt-in control socket (`[control_socket] enabled = true`) does not check
|
||||
the connecting user and can be reached from sandboxed commands. OAuth
|
||||
loopback callbacks end a sign-in on the first malformed request, so a page
|
||||
or local process can make one fail.
|
||||
- The sandbox deny-list for the Runtime daemon socket covers its default
|
||||
locations as this process sees them. It does not cover a daemon started
|
||||
with `--socket-path` or with a different `XDG_RUNTIME_DIR` or
|
||||
`CODEWHALE_HOME`; a run directory created after a long-running sandboxed
|
||||
command started is not hidden from that command; and on macOS, when the
|
||||
workspace is your home directory or another folder above the daemon's
|
||||
directory, a sandboxed command can still move that directory out of the
|
||||
denied path.
|
||||
- Ambiguous current-quota 429s without a body retry delay of at most 120 s
|
||||
still end the turn; a header alone does not change that classification.
|
||||
Explicit exhausted account, billing and subscription limits remain terminal
|
||||
|
||||
@@ -2124,9 +2124,7 @@ async fn web_bootstrap_sets_strict_cookie_once_and_preserves_v1_auth() -> Result
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(reloaded.headers()[header::CACHE_CONTROL], "no-store");
|
||||
assert!(reloaded.text().await?.contains(&format!(
|
||||
"name=\"codewhale-web-request\" content=\"{proof}\""
|
||||
)));
|
||||
assert!(!reloaded.text().await?.contains(&proof));
|
||||
}
|
||||
for method in [reqwest::Method::GET, reqwest::Method::POST] {
|
||||
for metadata in [false, true] {
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::time::{Duration, Instant};
|
||||
use axum::Json;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{ConnectInfo, Path, Request, State};
|
||||
use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
|
||||
use axum::http::{HeaderValue, StatusCode, header};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -249,35 +249,15 @@ pub(super) async fn refresh_stream_ticket(
|
||||
response
|
||||
}
|
||||
|
||||
pub(super) async fn web_page(State(state): State<RuntimeApiState>, headers: HeaderMap) -> Response {
|
||||
let Some(web) = state.web.as_ref() else {
|
||||
pub(super) async fn web_page(State(state): State<RuntimeApiState>) -> Response {
|
||||
if state.web.is_none() {
|
||||
return not_found();
|
||||
};
|
||||
web_page_response(web, &headers)
|
||||
}
|
||||
web_page_response()
|
||||
}
|
||||
|
||||
fn web_page_response(web: &RuntimeWebState, headers: &HeaderMap) -> Response {
|
||||
let mut html = WEB_HTML.to_owned();
|
||||
// Recover the origin-scoped proof for reloads and independent tabs. A
|
||||
// cookie alone is insufficient: require same-origin or direct navigation
|
||||
// Fetch Metadata. Older clients without it still use the bootstrap proof.
|
||||
// Proofs are generated hex strings, so no HTML escaping is needed here.
|
||||
if headers
|
||||
.get("sec-fetch-site")
|
||||
.is_some_and(|site| site == "same-origin" || site == "none")
|
||||
&& web.matches_session_cookie(headers.get(header::COOKIE).and_then(|v| v.to_str().ok()))
|
||||
&& let Some(proof) = web
|
||||
.request_proof
|
||||
.lock()
|
||||
.unwrap_or_else(|p| p.into_inner())
|
||||
.as_deref()
|
||||
{
|
||||
html = html.replace(
|
||||
"name=\"codewhale-web-request\" content=\"\"",
|
||||
&format!("name=\"codewhale-web-request\" content=\"{proof}\""),
|
||||
);
|
||||
}
|
||||
secured_asset("text/html; charset=utf-8", html)
|
||||
fn web_page_response() -> Response {
|
||||
secured_asset("text/html; charset=utf-8", WEB_HTML.to_owned())
|
||||
}
|
||||
|
||||
pub(super) async fn web_styles(State(state): State<RuntimeApiState>) -> Response {
|
||||
@@ -390,57 +370,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_surface_review_web_page_recovers_proof_for_reload_and_second_tab() {
|
||||
async fn runtime_surface_review_web_page_never_carries_the_request_proof() {
|
||||
let (web, nonce) = RuntimeWebState::new();
|
||||
let (token, proof) = web.consume(&nonce, "127.0.0.1".parse().unwrap()).unwrap();
|
||||
let cookie = web_session_cookie(&token);
|
||||
for site in [
|
||||
None,
|
||||
Some("same-origin"),
|
||||
Some("none"),
|
||||
Some("same-site"),
|
||||
Some("cross-site"),
|
||||
] {
|
||||
for valid_cookie in [false, true] {
|
||||
for expired in [false, true] {
|
||||
let mut session = web.clone();
|
||||
if expired {
|
||||
session.session_expires_at = Instant::now();
|
||||
}
|
||||
let mut headers = HeaderMap::new();
|
||||
if valid_cookie {
|
||||
headers.insert(header::COOKIE, cookie.parse().unwrap());
|
||||
}
|
||||
if let Some(site) = site {
|
||||
headers.insert("sec-fetch-site", site.parse().unwrap());
|
||||
}
|
||||
let response = web_page_response(&session, &headers);
|
||||
assert_eq!(response.headers()[header::CACHE_CONTROL], "no-store");
|
||||
assert!(
|
||||
response.headers()[header::CONTENT_SECURITY_POLICY]
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains("frame-ancestors 'none'")
|
||||
);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let html = std::str::from_utf8(&body).unwrap();
|
||||
let recovered = html.contains(&format!(
|
||||
"name=\"codewhale-web-request\" content=\"{proof}\""
|
||||
));
|
||||
assert_eq!(
|
||||
recovered,
|
||||
valid_cookie && !expired && matches!(site, Some("same-origin" | "none")),
|
||||
"site={site:?}, valid_cookie={valid_cookie}, expired={expired}"
|
||||
);
|
||||
assert!(!html.contains(&token));
|
||||
if recovered {
|
||||
assert!(session.matches_request(Some(&cookie), Some(&proof)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let response = web_page_response();
|
||||
assert_eq!(response.headers()[header::CACHE_CONTROL], "no-store");
|
||||
assert!(
|
||||
response.headers()[header::CONTENT_SECURITY_POLICY]
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains("frame-ancestors 'none'")
|
||||
);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let html = std::str::from_utf8(&body).unwrap();
|
||||
assert!(!html.contains(&proof));
|
||||
assert!(!html.contains(&token));
|
||||
assert!(!html.contains("codewhale-web-request"));
|
||||
assert_eq!(
|
||||
web.consume(&nonce, "127.0.0.1".parse().unwrap()),
|
||||
Err(BootstrapError::Invalid)
|
||||
|
||||
@@ -495,11 +495,25 @@ export function eventStreamUrl(threadId, latestSeq, ticket = "") {
|
||||
return ticket ? `${url}&web_stream_ticket=${encodeURIComponent(ticket)}` : url;
|
||||
}
|
||||
|
||||
export function createWebSessionFetch({ location, history, storage, fetch, pageProof = "" }) {
|
||||
export function approvalRequestText(input) {
|
||||
if (input == null) return "";
|
||||
if (typeof input === "string") return input;
|
||||
if (typeof input !== "object") return String(input);
|
||||
const { command, ...rest } = input;
|
||||
const parts = [];
|
||||
if (typeof command === "string") parts.push(command);
|
||||
const restKeys = Object.keys(rest);
|
||||
if (restKeys.length > 0 || typeof command !== "string") {
|
||||
parts.push(JSON.stringify(typeof command === "string" ? rest : input, null, 2));
|
||||
}
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
|
||||
export function createWebSessionFetch({ location, history, storage, fetch }) {
|
||||
const key = "codewhale_web_request_proof";
|
||||
const fragmentProof = new URLSearchParams(location.hash.slice(1)).get("p");
|
||||
if (fragmentProof) history.replaceState(null, "", location.pathname + location.search);
|
||||
let proof = pageProof || fragmentProof || "";
|
||||
let proof = fragmentProof || "";
|
||||
if (proof) {
|
||||
try { storage().setItem(key, proof); } catch (_) { /* Memory-only session. */ }
|
||||
} else {
|
||||
@@ -1081,7 +1095,6 @@ function startBrowserClient() {
|
||||
location: window.location,
|
||||
history: window.history,
|
||||
storage: () => window.sessionStorage,
|
||||
pageProof: document.querySelector('meta[name="codewhale-web-request"]')?.content,
|
||||
fetch: (...args) => fetch(...args),
|
||||
});
|
||||
|
||||
@@ -1743,7 +1756,15 @@ function startBrowserClient() {
|
||||
const title = element("h2", "", approval.tool_name || "Tool request");
|
||||
title.id = titleId;
|
||||
card.append(title);
|
||||
card.append(element("p", "", approval.intent_summary || approval.description || "Codewhale is waiting for permission."));
|
||||
card.append(element("p", "", approval.summary || approval.description || "Codewhale is waiting for permission."));
|
||||
if (approval.summary && approval.description && approval.description !== approval.summary) {
|
||||
card.append(element("p", "approval-reason", approval.description));
|
||||
}
|
||||
const request = approvalRequestText(approval.input);
|
||||
if (request) card.append(element("pre", "approval-request", request));
|
||||
if (approval.intent_summary) {
|
||||
card.append(element("p", "approval-claim", `The model says: ${approval.intent_summary}`));
|
||||
}
|
||||
const actions = element("div", "attention-actions");
|
||||
const rememberLabel = element("label", "remember-field");
|
||||
const remember = document.createElement("input");
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="codewhale-web-request" content="">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="theme-color" content="#020711">
|
||||
<title>Codewhale</title>
|
||||
|
||||
@@ -882,6 +882,28 @@ textarea::placeholder {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.attention-card .approval-request {
|
||||
margin: 0 0 12px;
|
||||
padding: 8px 10px;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--line, rgba(127, 127, 127, 0.3));
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.attention-card .approval-reason {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.attention-card .approval-claim {
|
||||
opacity: 0.75;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.attention-card p {
|
||||
margin: 0;
|
||||
color: var(--text-soft);
|
||||
|
||||
@@ -434,6 +434,7 @@ fn default_denied_subtrees() -> Vec<(PathBuf, &'static str)> {
|
||||
crate::oauth::auth_file_path(),
|
||||
"Codex CLI login (resolved auth file)",
|
||||
));
|
||||
out.extend(runtime_daemon_socket_dirs());
|
||||
let Some(home) = dirs::home_dir() else {
|
||||
return out;
|
||||
};
|
||||
@@ -573,6 +574,48 @@ fn default_denied_subtrees() -> Vec<(PathBuf, &'static str)> {
|
||||
out
|
||||
}
|
||||
|
||||
fn runtime_daemon_socket_dirs() -> Vec<(PathBuf, &'static str)> {
|
||||
use codewhale_app_server::daemon_socket::{SocketPathInputs, resolve_socket_path};
|
||||
let live = SocketPathInputs::from_environment(None).unwrap_or_else(|_| SocketPathInputs {
|
||||
xdg_runtime_dir: std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(PathBuf::from),
|
||||
user_home: codewhale_paths::user_home(),
|
||||
macos: cfg!(target_os = "macos"),
|
||||
..SocketPathInputs::default()
|
||||
});
|
||||
let layouts = [
|
||||
live.clone(),
|
||||
SocketPathInputs {
|
||||
codewhale_home_override: None,
|
||||
..live.clone()
|
||||
},
|
||||
SocketPathInputs {
|
||||
codewhale_home_override: None,
|
||||
xdg_runtime_dir: None,
|
||||
..live.clone()
|
||||
},
|
||||
SocketPathInputs {
|
||||
codewhale_home_override: None,
|
||||
xdg_runtime_dir: None,
|
||||
macos: false,
|
||||
..live
|
||||
},
|
||||
];
|
||||
let mut dirs: Vec<PathBuf> = Vec::new();
|
||||
for layout in &layouts {
|
||||
if let Ok(socket) = resolve_socket_path(layout)
|
||||
&& let Some(dir) = socket.parent()
|
||||
&& !dirs.iter().any(|seen| seen == dir)
|
||||
{
|
||||
dirs.push(dir.to_path_buf());
|
||||
}
|
||||
}
|
||||
dirs.into_iter()
|
||||
.map(|dir| (dir, "Codewhale runtime daemon socket and owner receipt"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn machine_wide_denied_subtrees() -> Vec<(PathBuf, &'static str)> {
|
||||
let mut out: Vec<(PathBuf, &'static str)> = vec![
|
||||
(
|
||||
|
||||
@@ -263,6 +263,8 @@ fn generate_policy(
|
||||
// last-match-wins, so these rules override every broad read allowance
|
||||
// above — including the full-disk `(allow file-read*)` — for the listed
|
||||
// subpaths. Metadata reads are denied too so the paths do not enumerate.
|
||||
// A read deny does not stop connect(2) to a socket under the path, so with
|
||||
// network on, unix-socket connects under each path are denied as well.
|
||||
if !denied_read_subpaths.is_empty() {
|
||||
full_policy.push_str("\n\n; Opt-in read deny-list (user-configured)\n");
|
||||
for path in denied_read_subpaths {
|
||||
@@ -271,6 +273,11 @@ fn generate_policy(
|
||||
.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"");
|
||||
full_policy.push_str(&format!("(deny file-read* (subpath \"{escaped}\"))\n"));
|
||||
if policy.has_network_access() {
|
||||
full_policy.push_str(&format!(
|
||||
"(deny network-outbound (remote unix-socket (subpath \"{escaped}\")))\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1094,8 +1094,8 @@ test("web session fetch keeps request proof outside cookies and strips bootstrap
|
||||
assert.equal(calls.at(-1).options.headers.get("x-codewhale-web-request"), "memory-proof");
|
||||
});
|
||||
|
||||
test("web session recovers from the page in an independent tab or without storage", async () => {
|
||||
for (const storedProof of [null, "old-process-proof", "unavailable"]) {
|
||||
test("web session uses only its fragment or this tab's stored proof", async () => {
|
||||
for (const storedProof of [null, "this-tab-proof", "unavailable"]) {
|
||||
const stored = new Map([["codewhale_web_request_proof", storedProof]]);
|
||||
const calls = [];
|
||||
const request = createWebSessionFetch({
|
||||
@@ -1105,17 +1105,14 @@ test("web session recovers from the page in an independent tab or without storag
|
||||
if (storedProof === "unavailable") throw new Error("storage unavailable");
|
||||
return { getItem: (key) => stored.get(key), setItem: (key, value) => stored.set(key, value) };
|
||||
},
|
||||
pageProof: "current-page-proof",
|
||||
fetch: async (_path, options) => { calls.push(options); return { ok: true }; },
|
||||
});
|
||||
await request("/v1/threads");
|
||||
await request("/__codewhale/web/stream-ticket", { method: "POST" });
|
||||
assert.equal(calls.length, 2);
|
||||
const expected = storedProof === "this-tab-proof" ? "this-tab-proof" : "";
|
||||
for (const options of calls) {
|
||||
assert.equal(options.headers.get("x-codewhale-web-request"), "current-page-proof");
|
||||
}
|
||||
if (storedProof !== "unavailable") {
|
||||
assert.equal(stored.get("codewhale_web_request_proof"), "current-page-proof");
|
||||
assert.equal(options.headers.get("x-codewhale-web-request"), expected);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+9
-7
@@ -908,13 +908,15 @@ attacker has strictly easier local avenues than this race.
|
||||
Web fetches require the session cookie plus an origin-scoped request proof;
|
||||
streams use a fresh single-use ticket. The initial redirect carries the proof
|
||||
in a fragment, which the client removes and saves in origin-scoped
|
||||
`sessionStorage`. On reload or in a second tab, an authenticated `GET /` also
|
||||
embeds the proof in a meta tag when `Sec-Fetch-Site` is `same-origin` or `none`
|
||||
(direct navigation). The page uses `no-store`, disallows framing, and grants no
|
||||
cross-origin read access. This lets a new tab recover without reusing the
|
||||
bootstrap URL, including when storage is unavailable. Clients without Fetch
|
||||
Metadata can only reuse the fragment or their existing stored proof; recovery
|
||||
does not extend the server session or replace an expired cookie.
|
||||
`sessionStorage`, so a reload of that tab keeps working when the browser
|
||||
allows session storage. The server never puts
|
||||
the proof in a page: the session cookie is host-scoped, not port-scoped, so a
|
||||
listener on another loopback port can receive it, and the cookie alone must
|
||||
not be enough to recover the proof. A new tab opened by typing the address
|
||||
therefore has no proof; use the original tab, duplicate it, or restart
|
||||
`codewhale web` for a fresh one-time session. The page uses `no-store`,
|
||||
disallows framing, and grants no cross-origin read access. Nothing extends the
|
||||
server session or replaces an expired cookie.
|
||||
Cross-origin Fetch Metadata or a mismatched Origin is rejected on web API
|
||||
requests. Explicit bearer and Runtime-token header clients keep their existing
|
||||
behavior. Transient stream-ticket failures retry with capped backoff; HTTP
|
||||
|
||||
@@ -80,6 +80,9 @@ before operating either one, especially before selecting a non-loopback bind.
|
||||
credentials.
|
||||
- If a session expired, stop and restart `codewhale web` to mint a new
|
||||
process-local session. Reusing an old bootstrap URL is expected to fail.
|
||||
- A new tab opened by typing the address is not signed in: the session's
|
||||
request proof stays in the tab that opened it. Use that tab, duplicate it,
|
||||
or restart `codewhale web`.
|
||||
|
||||
For integration endpoints, headers, events, and the complete web-session
|
||||
contract, see [RUNTIME_API.md](RUNTIME_API.md).
|
||||
@@ -774,12 +774,12 @@ codewhale serve --mobile [...] [--insecure]
|
||||
攻击者有严格比这个竞争更简单的本地途径。
|
||||
Web 请求需要会话 cookie 加上一个限定于源(origin)的请求证明;
|
||||
流使用一张新的一次性票据。初始重定向把证明放在 fragment 中,客户端将其移除并
|
||||
保存到限定于源的 `sessionStorage`。重新加载或在第二个标签页中打开时,
|
||||
当 `Sec-Fetch-Site` 为 `same-origin` 或 `none`(直接导航)时,
|
||||
经过认证的 `GET /` 还会把证明嵌入一个 meta 标签。该页面使用 `no-store`、
|
||||
禁止被嵌入框架,也不授予任何跨源读取权限。这让新标签页无需复用 bootstrap URL
|
||||
即可恢复,包括存储不可用的情况。不支持 Fetch Metadata 的客户端只能复用 fragment
|
||||
或它已存储的证明;恢复不会延长服务器会话,也不会替换已过期的 cookie。
|
||||
保存到限定于源的 `sessionStorage`;只要浏览器允许会话存储,重新加载该标签页
|
||||
仍可继续使用。服务器从不把证明写进页面:会话 cookie 只限定主机而不限定端口,
|
||||
另一个回环端口上的监听者可能收到它,因此仅凭 cookie 不能取回证明。通过手动输入
|
||||
地址打开的新标签页没有证明;请使用原来的标签页、复制该标签页,或重新启动
|
||||
`codewhale web` 获取新的一次性会话。该页面使用 `no-store`、禁止被嵌入框架,
|
||||
也不授予任何跨源读取权限。任何操作都不会延长服务器会话,也不会替换已过期的 cookie。
|
||||
跨源的 Fetch Metadata 或不匹配的 Origin 会在 web API 请求上被拒绝。显式的
|
||||
bearer 与 Runtime 令牌 header 客户端保持其既有行为。暂时性的流票据失败会以
|
||||
有上限的退避重试;HTTP 401/403 会停止票据重试,直到打开新的会话。
|
||||
|
||||
@@ -72,5 +72,7 @@ Runtime token 不会出现在渲染后的 HTML、浏览器存储、URL 查询参
|
||||
web 命令不会配置或迁移提供商的凭据。
|
||||
- 如果会话已过期,请停止并重新启动 `codewhale web`,以创建一个仅对新进程有效的会话。
|
||||
重复使用旧的引导 URL 应当失败。
|
||||
- 通过手动输入地址打开的新标签页不会处于登录状态:会话的请求证明只保存在打开它的
|
||||
标签页中。请使用那个标签页、复制它,或重新启动 `codewhale web`。
|
||||
|
||||
有关集成端点、请求头、事件和完整的网页会话约定,请参阅 [RUNTIME_API.md](../RUNTIME_API.md)。
|
||||
@@ -288,8 +288,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/RUNTIME_API.md",
|
||||
"sha256": "4fef71a41b64ef8bf38479d8f7bda40bec369608a8033f990fcf73856889f40b",
|
||||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. New integrations should target `app-server`.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | JSON-RPC 2.0 over stdio | Local SDK / control probe (no listener) |\n| `codewhale app-server --socket [--socket-path P]` | JSON-RPC 2.0 over a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | HTTP on `127.0.0.1:8787` | Legacy in-process app-server (`/healthz`, `/thread`, `/app`, `/prompt`); background jobs are `GET /v1/jobs` on the Runtime API; `/prompt` and `/thread` messages execute real turns via the runtime bridge. There is no direct `/tool` route: tools run only inside Engine turns, under the Engine's tool catalog and approval posture. This legacy server does not surface approvals: its bridge forwards only text deltas and the turn's completion, and it has no decision route, so an approval-gated call waits unanswered. Drive approval-gated work through the Runtime API (`/v1/threads/*` events and `POST /v1/approvals/{approval_id}`) |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nThe legacy in-process `codewhale app-server` also requires an explicit\n`--auth-token` or `CODEWHALE_APP_SERVER_TOKEN` before binding a non-loopback\nhost; its generated one-time `cwapp_*` token is loopback-only.\n\nDevice tokens minted by the master through `POST /v1/auth/client-tokens`\nhave an immutable `intent`: `Line truncated
|
||||
"sha256": "60965eebaa7200290f8cb306be99f4cd0b8edd7a6382c6a72becc3ed23975b5f",
|
||||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. New integrations should target `app-server`.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | JSON-RPC 2.0 over stdio | Local SDK / control probe (no listener) |\n| `codewhale app-server --socket [--socket-path P]` | JSON-RPC 2.0 over a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | HTTP on `127.0.0.1:8787` | Legacy in-process app-server (`/healthz`, `/thread`, `/app`, `/prompt`); background jobs are `GET /v1/jobs` on the Runtime API; `/prompt` and `/thread` messages execute real turns via the runtime bridge. There is no direct `/tool` route: tools run only inside Engine turns, under the Engine's tool catalog and approval posture. This legacy server does not surface approvals: its bridge forwards only text deltas and the turn's completion, and it has no decision route, so an approval-gated call waits unanswered. Drive approval-gated work through the Runtime API (`/v1/threads/*` events and `POST /v1/approvals/{approval_id}`) |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nThe legacy in-process `codewhale app-server` also requires an explicit\n`--auth-token` or `CODEWHALE_APP_SERVER_TOKEN` before binding a non-loopback\nhost; its generated one-time `cwapp_*` token is loopback-only.\n\nDevice tokens minted by the master through `POST /v1/auth/client-tokens`\nhave an immutable `intent`: `Line truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -299,8 +299,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/WEB.md",
|
||||
"sha256": "6235bee74db77a00625539fc5e3a243b6cb2ed4fe7788b3337c03c3c3eb8037b",
|
||||
"content": "# Local browser client\n\n> 阅读简体中文版:[zh_hans/WEB.md](zh_hans/WEB.md)。\n\n`codewhale web` opens Codewhale's embedded browser client over the canonical\nRuntime API. It is a local surface: the server always binds to\n`127.0.0.1`, cannot be rebound to a LAN address, and cannot run with Runtime\nauthentication disabled.\n\n## Start it\n\nFrom the workspace Codewhale should operate in, run:\n\n```bash\ncodewhale web\n```\n\nThe default address is `http://127.0.0.1:7878`. To avoid a local port\ncollision, choose another loopback port:\n\n```bash\ncodewhale web --port 8788\n```\n\nCodewhale starts the Runtime API, serves the dependency-free client embedded\nin the installed binary, prints a single-use launch URL, and asks the operating\nsystem to open that URL in the default browser. If the browser does not open,\nuse the printed URL within ten minutes. Stop the process with `Ctrl+C`; the\nbrowser session ends with it.\n\n## What the browser can do\n\nThe embedded client provides a responsive thread and search rail, Runtime-owned\nsession facts, transcript and tool receipts, and a composer. It can create,\nselect, rename, and archive threads; start or steer turns; interrupt work;\nresolve approvals; and answer Runtime user-input requests.\n\nThe browser is another view of the same local Runtime. It does not create a\nsecond cloud account, copy provider credentials into browser storage, or\nweaken the configured approval and sandbox policies.\n\n## Authentication boundary\n\nThe browser-launch URL contains a random, short-lived, one-time bootstrap\ncapability. It never contains the Runtime bearer token. A loopback request\nexchanges the capability for an `HttpOnly`, `SameSite=Strict`, process-local\nsession cookie and immediately invalidates the capability.\n\nReused, expired, malformed, and non-loopback bootstrap attempts fail closed.\nThe Runtime token is not placed in rendered HTML, browser storage, URL\nqueries or fragments, or browser-launch arguments. The one-time bootstrap\nvalue is printed in the local terminal and briefly passes through the operating\nsystem's browser launcher. It is single-use and expires after ten minutes, but\na hostile process already running as the same OS user remains inside the local\ntrust boundary.\n\nCookie-authenticated state-changing requests must also present the exact\nlocal web origin. Cross-origin browser requests are rejected. Existing\nexplicit bearer and Runtime-token-header clients retain their normal Runtime\nAPI behavior.\n\n## Local means local\n\n`codewhale web` accepts only `--port`; there is no `--host` or insecure-auth\noption on this command. Do not treat it as a public website or expose its port\ndirectly through router forwarding, a public reverse proxy, or a tunnel.\n\nThe separate `codewhale app-server --mobile` and `--http` modes have different\ndeployment and authentication contracts. Read [RUNTIME_API.md](RUNTIME_API.md)\nbefore operating either one, especially before selecting a non-loopback bind.\n\n## Troubleshooting\n\n- If port `7878` is occupied, pass an unused `--port` value.\n- If the browser does not open, copy the printed single-use bootstrap URL into\n a browser on the same machine within ten minutes. Start `codewhale web` again\n if that URL has already been used or expired.\n- If the page loads but a provider is unavailable, inspect `codewhale doctor`\n and `/provider`; the web command does not configure or move provider\n credentials.\n- If a session expired, stop and restart `codewhale web` to mint a new\n process-local session. Reusing an old bootstrap URL is expected to fail.\n\nFor integration endpoints, headers, events, and the complete web-session\ncontract, see [RUNTIME_API.md](RUNTIME_API.md).\n"
|
||||
"sha256": "e38dbc33c3f768fa9ea208a2c6231445406fe8f6421c56c961c21afa7382901e",
|
||||
"content": "# Local browser client\n\n> 阅读简体中文版:[zh_hans/WEB.md](zh_hans/WEB.md)。\n\n`codewhale web` opens Codewhale's embedded browser client over the canonical\nRuntime API. It is a local surface: the server always binds to\n`127.0.0.1`, cannot be rebound to a LAN address, and cannot run with Runtime\nauthentication disabled.\n\n## Start it\n\nFrom the workspace Codewhale should operate in, run:\n\n```bash\ncodewhale web\n```\n\nThe default address is `http://127.0.0.1:7878`. To avoid a local port\ncollision, choose another loopback port:\n\n```bash\ncodewhale web --port 8788\n```\n\nCodewhale starts the Runtime API, serves the dependency-free client embedded\nin the installed binary, prints a single-use launch URL, and asks the operating\nsystem to open that URL in the default browser. If the browser does not open,\nuse the printed URL within ten minutes. Stop the process with `Ctrl+C`; the\nbrowser session ends with it.\n\n## What the browser can do\n\nThe embedded client provides a responsive thread and search rail, Runtime-owned\nsession facts, transcript and tool receipts, and a composer. It can create,\nselect, rename, and archive threads; start or steer turns; interrupt work;\nresolve approvals; and answer Runtime user-input requests.\n\nThe browser is another view of the same local Runtime. It does not create a\nsecond cloud account, copy provider credentials into browser storage, or\nweaken the configured approval and sandbox policies.\n\n## Authentication boundary\n\nThe browser-launch URL contains a random, short-lived, one-time bootstrap\ncapability. It never contains the Runtime bearer token. A loopback request\nexchanges the capability for an `HttpOnly`, `SameSite=Strict`, process-local\nsession cookie and immediately invalidates the capability.\n\nReused, expired, malformed, and non-loopback bootstrap attempts fail closed.\nThe Runtime token is not placed in rendered HTML, browser storage, URL\nqueries or fragments, or browser-launch arguments. The one-time bootstrap\nvalue is printed in the local terminal and briefly passes through the operating\nsystem's browser launcher. It is single-use and expires after ten minutes, but\na hostile process already running as the same OS user remains inside the local\ntrust boundary.\n\nCookie-authenticated state-changing requests must also present the exact\nlocal web origin. Cross-origin browser requests are rejected. Existing\nexplicit bearer and Runtime-token-header clients retain their normal Runtime\nAPI behavior.\n\n## Local means local\n\n`codewhale web` accepts only `--port`; there is no `--host` or insecure-auth\noption on this command. Do not treat it as a public website or expose its port\ndirectly through router forwarding, a public reverse proxy, or a tunnel.\n\nThe separate `codewhale app-server --mobile` and `--http` modes have different\ndeployment and authentication contracts. Read [RUNTIME_API.md](RUNTIME_API.md)\nbefore operating either one, especially before selecting a non-loopback bind.\n\n## Troubleshooting\n\n- If port `7878` is occupied, pass an unused `--port` value.\n- If the browser does not open, copy the printed single-use bootstrap URL into\n a browser on the same machine within ten minutes. Start `codewhale web` again\n if that URL has already been used or expired.\n- If the page loads but a provider is unavailable, inspect `codewhale doctor`\n and `/provider`; the web command does not configure or move provider\n credentials.\n- If a session expired, stop and restart `codewhale web` to mint a new\n process-local session. Reusing an old bootstrap URL is expected to fail.\n- A new tab opened by typing the address is not signed in: the session's\n request proof stays in the tab that opened it. Use that tab, duplicate it,\n or restart `codewhale web`.\n\nFor integration endpoints, headers, events, and the complete web-session\ncontract, see [RUNTIME_API.md](RUNTIME_API.md).\n"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user