mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
Fix API compatibility admission and sandboxed PTY sessions for 0.10.2 [skip ci]
Make app-server --http the explicit full Engine entrypoint. Compatibility turns require and forward an empty tool allowlist, and refuse interactive work and active goal starts before mutation. Reject unsupported ACP MCP configuration before new/load, including cached sessions. State SDK scope and document authenticated core HTTP controls in both API guides. Build both PTY consumers through ExecEnv. Linux bubblewrap launches assign the controlling terminal inside the PID namespace with util-linux setsid; retain sandbox mounts, sanitized environment and process-tree cleanup. Local verification on macOS ARM64: app-server 192, protocol 96, ACP 33, terminal/tool-free checks 5, CLI app-server 7, feature registry 2, portable navigation 9; all passed. Runtime SDK 19 plus TypeScript/syntax passed. CLI built; 4139 tracked inputs stayed unchanged during the final batch. Only API Markdown and derived docs changed after that build. Facts, changelog mirrors, help output and whitespace checks passed. Actual implementation-binary observations: API refusals/capabilities 38; Herdr 0.9.5 idle/working/blocked/idle, normal release and same-conversation crash restoration; Herdr 0.8 usage-error retry and sticky state-only reporting. Local synthetic loopback provider only, no provider spend. Linux platform reproduction confirmed the foreground/controlling-terminal repair and containment, but exact final Engine Linux qualification is still pending alongside final CI, package/OS checks and publication. Refs: CW-6572, CW-7430, CW-6606, CW-7462
This commit is contained in:
1 parent
a815930d14
commit
109d0a3f22
21 files changed
+597
-143
No files matched your search
@@ -28,6 +28,19 @@ and independent workflow steps can continue after another child fails.
|
||||
|
||||
### Latest fixes
|
||||
|
||||
- Linux sandboxed terminals deliver Ctrl-C to the foreground program without
|
||||
killing the bubblewrap wrapper. A program that handles SIGINT can keep running
|
||||
and accept more input. PTY jobs and persistent terminals retain their sandbox
|
||||
and process-tree cleanup; this path requires util-linux `setsid`.
|
||||
|
||||
- Full Engine integrations now point explicitly to `codewhale app-server --http`.
|
||||
Compatibility HTTP, stdio and socket turns require `allowed_tools: []` and
|
||||
reject interactive execution and goal starts before work begins, closing the approval and
|
||||
clarification dead end. ACP new/load rejects unsupported supplied MCP servers
|
||||
instead of silently ignoring them; ACP v1 MCP support remains partial. The
|
||||
Runtime SDK guide states its Fleet/event coverage and provides authenticated
|
||||
HTTP examples for the remaining client controls.
|
||||
|
||||
- `tokio` moves to 1.53.3, which retires `RUSTSEC-2026-0386`. The advisory
|
||||
covers `tokio::io::unix::AsyncFd`'s safe constructors accepting a file
|
||||
descriptor they do not own; the lock now sits in a range the advisory lists as
|
||||
|
||||
+189
-39
@@ -476,6 +476,8 @@ struct ThreadIdParams {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ThreadMessageParams {
|
||||
#[serde(default)]
|
||||
allowed_tools: Option<Vec<String>>,
|
||||
#[serde(default, rename = "maxOutputTokens", alias = "max_output_tokens")]
|
||||
max_output_tokens: Option<std::num::NonZeroU32>,
|
||||
thread_id: String,
|
||||
@@ -534,10 +536,9 @@ async fn shutdown_signal() {
|
||||
/// There is no `/tool`: a direct tool call outside a turn would need its own
|
||||
/// tool catalog and approval decision, and the Engine behind the runtime
|
||||
/// bridge is the only tool and approval authority. Tools run inside turns
|
||||
/// (`/prompt`, `/thread` messages). This server does not surface approvals:
|
||||
/// `RuntimeBridge::stream_turn_events` forwards only `item.delta` and the
|
||||
/// turn's completion, and there is no decision route, so approval-gated work
|
||||
/// belongs on the Runtime API (`/v1/threads/*`, `POST /v1/approvals/{id}`).
|
||||
/// (`/prompt`, `/thread` messages). Compatibility turns require an explicit
|
||||
/// empty `allowed_tools` list; interactive execution is refused before turn
|
||||
/// admission. Tool-enabled work belongs on `app-server --http` and `/v1/*`.
|
||||
const ADVERTISED_ROUTES: &[&str] = &["/thread", "/app", "/prompt"];
|
||||
|
||||
/// Existing compatibility routes adopted by the canonical host listener.
|
||||
@@ -1155,6 +1156,7 @@ fn http_error_from_jsonrpc(err: JsonRpcError) -> (StatusCode, Json<Value>) {
|
||||
"code": code,
|
||||
"jsonrpc_code": err.code,
|
||||
"message": err.message,
|
||||
"data": err.data,
|
||||
}
|
||||
})),
|
||||
)
|
||||
@@ -1167,12 +1169,20 @@ async fn thread_handler(State(state): State<AppState>, Json(req): Json<ThreadReq
|
||||
if let ThreadRequest::Message {
|
||||
thread_id,
|
||||
input,
|
||||
allowed_tools,
|
||||
images,
|
||||
max_output_tokens,
|
||||
} = req
|
||||
{
|
||||
return match run_http_thread_message(&state, thread_id, input, images, max_output_tokens)
|
||||
.await
|
||||
return match run_http_thread_message(
|
||||
&state,
|
||||
thread_id,
|
||||
input,
|
||||
allowed_tools,
|
||||
images,
|
||||
max_output_tokens,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => (StatusCode::OK, Json(res)).into_response(),
|
||||
Err(err) => http_error_from_jsonrpc(err).into_response(),
|
||||
@@ -1468,6 +1478,19 @@ impl JsonRpcError {
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_interactive_execution() -> Self {
|
||||
Self {
|
||||
code: -32602,
|
||||
message: "Compatibility transports cannot answer approvals or clarification requests. Use codewhale app-server --http and /v1/* for interactive turns and goal execution; tool-free turn requests require explicit allowed_tools: [].".to_string(),
|
||||
data: Some(json!({
|
||||
"error": "interactive_execution_unsupported",
|
||||
"entrypoint": "codewhale app-server --http",
|
||||
"allowed_tools_required": [],
|
||||
"goal_execution": false,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Server error (-32000..-32099): the turn engine could not be reached,
|
||||
/// or refused to start the turn — either way nothing ran. Distinct from
|
||||
/// `internal` because the caller can retry this one once a runtime is up.
|
||||
@@ -1572,6 +1595,9 @@ async fn handle_thread_request(
|
||||
state: &AppState,
|
||||
req: ThreadRequest,
|
||||
) -> std::result::Result<ThreadResponse, JsonRpcError> {
|
||||
if matches!(&req, ThreadRequest::GoalSet(_)) {
|
||||
return Err(JsonRpcError::unsupported_interactive_execution());
|
||||
}
|
||||
thread_control::handle(state, req).await
|
||||
}
|
||||
|
||||
@@ -1585,6 +1611,7 @@ struct RuntimeTurnInput<'a> {
|
||||
}
|
||||
|
||||
struct BridgedTurn<'a> {
|
||||
allowed_tools: Option<&'a [String]>,
|
||||
max_output_tokens: Option<std::num::NonZeroU32>,
|
||||
/// Client-facing thread id; the bridge maps it to a runtime thread.
|
||||
thread_key: &'a str,
|
||||
@@ -1618,6 +1645,9 @@ async fn run_bridged_turn<W: AsyncWrite + Unpin>(
|
||||
turn: BridgedTurn<'_>,
|
||||
transcript: Option<&mut TurnTranscript>,
|
||||
) -> std::result::Result<Value, JsonRpcError> {
|
||||
if !matches!(turn.allowed_tools, Some([])) {
|
||||
return Err(JsonRpcError::unsupported_interactive_execution());
|
||||
}
|
||||
let mut hint = {
|
||||
let hints = state.stdio_thread_hints.lock().await;
|
||||
hints.get(turn.thread_key).cloned()
|
||||
@@ -1742,6 +1772,7 @@ async fn run_prompt_turn<W: AsyncWrite + Unpin>(
|
||||
state,
|
||||
writer,
|
||||
BridgedTurn {
|
||||
allowed_tools: req.allowed_tools.as_deref(),
|
||||
max_output_tokens: req.max_output_tokens,
|
||||
thread_key: &thread_key,
|
||||
input: &req.prompt,
|
||||
@@ -1798,6 +1829,7 @@ async fn run_http_thread_message(
|
||||
state: &AppState,
|
||||
thread_id: String,
|
||||
input: String,
|
||||
allowed_tools: Option<Vec<String>>,
|
||||
images: Vec<RuntimeImageInput>,
|
||||
max_output_tokens: Option<std::num::NonZeroU32>,
|
||||
) -> std::result::Result<ThreadResponse, JsonRpcError> {
|
||||
@@ -1807,6 +1839,7 @@ async fn run_http_thread_message(
|
||||
state,
|
||||
&mut sink,
|
||||
BridgedTurn {
|
||||
allowed_tools: allowed_tools.as_deref(),
|
||||
max_output_tokens,
|
||||
thread_key: &thread_id,
|
||||
input: &input,
|
||||
@@ -1847,6 +1880,7 @@ async fn handle_stdio_thread_message<W: AsyncWrite + Unpin>(
|
||||
state,
|
||||
writer,
|
||||
BridgedTurn {
|
||||
allowed_tools: parsed.allowed_tools.as_deref(),
|
||||
max_output_tokens: parsed.max_output_tokens,
|
||||
thread_key: &parsed.thread_id,
|
||||
input: &parsed.input,
|
||||
@@ -2432,7 +2466,7 @@ impl RuntimeBridge {
|
||||
max_output_tokens,
|
||||
expected_workspace,
|
||||
} = input;
|
||||
let mut request = json!({ "prompt": input });
|
||||
let mut request = json!({ "prompt": input, "allowed_tools": [] });
|
||||
if let Some(workspace) = expected_workspace {
|
||||
request["expected_workspace"] = json!(workspace);
|
||||
}
|
||||
@@ -2638,6 +2672,11 @@ impl RuntimeBridge {
|
||||
}
|
||||
let payload = envelope.get("payload").cloned().unwrap_or(Value::Null);
|
||||
match event_name.as_str() {
|
||||
"approval.required" | "user_input.required" => {
|
||||
bail!(
|
||||
"interactive_execution_unsupported: Runtime requested interaction during a tool-free compatibility turn; use codewhale app-server --http and /v1/*"
|
||||
);
|
||||
}
|
||||
"item.delta" => {
|
||||
let kind = payload
|
||||
.get("kind")
|
||||
@@ -2920,6 +2959,10 @@ async fn dispatch_stdio_request_with_writer<W: AsyncWrite + Unpin>(
|
||||
"transport": transport.label(),
|
||||
"families": ["thread/*", "app/*", "prompt/*"],
|
||||
"turn_image_inputs": true,
|
||||
"interactive_execution": false,
|
||||
"goal_execution": false,
|
||||
"tool_free_turns": true,
|
||||
"full_api_entrypoint": "codewhale app-server --http",
|
||||
"methods": methods,
|
||||
}),
|
||||
should_exit: false,
|
||||
@@ -2928,6 +2971,10 @@ async fn dispatch_stdio_request_with_writer<W: AsyncWrite + Unpin>(
|
||||
"thread/capabilities" => StdioDispatchResult {
|
||||
result: json!({
|
||||
"turn_image_inputs": true,
|
||||
"interactive_execution": false,
|
||||
"goal_execution": false,
|
||||
"tool_free_turns": true,
|
||||
"full_api_entrypoint": "codewhale app-server --http",
|
||||
"methods": [
|
||||
"thread/request",
|
||||
"thread/create",
|
||||
@@ -2953,6 +3000,7 @@ async fn dispatch_stdio_request_with_writer<W: AsyncWrite + Unpin>(
|
||||
if let ThreadRequest::Message {
|
||||
thread_id,
|
||||
input,
|
||||
allowed_tools,
|
||||
images,
|
||||
max_output_tokens,
|
||||
} = request
|
||||
@@ -2963,6 +3011,7 @@ async fn dispatch_stdio_request_with_writer<W: AsyncWrite + Unpin>(
|
||||
ThreadMessageParams {
|
||||
thread_id,
|
||||
input,
|
||||
allowed_tools,
|
||||
images,
|
||||
max_output_tokens,
|
||||
},
|
||||
@@ -3188,6 +3237,10 @@ async fn dispatch_stdio_request_with_writer<W: AsyncWrite + Unpin>(
|
||||
}
|
||||
"prompt/capabilities" => StdioDispatchResult {
|
||||
result: json!({
|
||||
"interactive_execution": false,
|
||||
"goal_execution": false,
|
||||
"tool_free_turns": true,
|
||||
"full_api_entrypoint": "codewhale app-server --http",
|
||||
"methods": ["prompt/request", "prompt/run"]
|
||||
}),
|
||||
should_exit: false,
|
||||
@@ -3241,8 +3294,12 @@ async fn process_app_request(
|
||||
ok: true,
|
||||
data: json!({
|
||||
"routes": ADVERTISED_ROUTES,
|
||||
"interactive_execution": false,
|
||||
"goal_execution": false,
|
||||
"tool_free_turns": true,
|
||||
"full_api_entrypoint": "codewhale app-server --http",
|
||||
"config": ["get", "set", "unset", "list", "reload"],
|
||||
"events": ["response_start", "response_delta", "response_end", "tool_call_start", "tool_call_result"],
|
||||
"events": ["response_start", "response_delta", "response_end"],
|
||||
"transport": "stdio+http",
|
||||
"config_path": state.config_path.as_ref().map(|p| p.display().to_string()),
|
||||
}),
|
||||
@@ -3671,7 +3728,7 @@ mod tests {
|
||||
let refused = dispatch_stdio_request(
|
||||
&state,
|
||||
"prompt/request",
|
||||
json!({"prompt":"must not dispatch"}),
|
||||
json!({"allowed_tools": [], "prompt":"must not dispatch"}),
|
||||
)
|
||||
.await
|
||||
.expect_err("no captured owner can execute this prompt");
|
||||
@@ -4392,7 +4449,7 @@ mod tests {
|
||||
let result = dispatch_stdio_request(
|
||||
&state,
|
||||
"thread/message",
|
||||
json!({"thread_id":"canonical-1","input":"next"}),
|
||||
json!({"thread_id":"canonical-1","allowed_tools": [], "input":"next"}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4406,7 +4463,7 @@ mod tests {
|
||||
let error = dispatch_stdio_request(
|
||||
&state,
|
||||
"thread/message",
|
||||
json!({"thread_id":"never-created","input":"hello"}),
|
||||
json!({"thread_id":"never-created","allowed_tools": [], "input":"hello"}),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
@@ -4415,6 +4472,7 @@ mod tests {
|
||||
&state,
|
||||
"never-created".into(),
|
||||
"hello".into(),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
None,
|
||||
)
|
||||
@@ -4441,7 +4499,7 @@ mod tests {
|
||||
dispatch_stdio_request(
|
||||
&state,
|
||||
"thread/message",
|
||||
json!({"thread_id":"client-1","input":"first"}),
|
||||
json!({"thread_id":"client-1","allowed_tools": [], "input":"first"}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4459,7 +4517,7 @@ mod tests {
|
||||
dispatch_stdio_request(
|
||||
&restarted,
|
||||
"thread/message",
|
||||
json!({"thread_id":"client-1","input":"second"}),
|
||||
json!({"thread_id":"client-1","allowed_tools": [], "input":"second"}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4501,7 +4559,7 @@ mod tests {
|
||||
dispatch_stdio_request(
|
||||
&state,
|
||||
"thread/message",
|
||||
json!({"thread_id":"client-1","input":"hello"}),
|
||||
json!({"thread_id":"client-1","allowed_tools": [], "input":"hello"}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4545,7 +4603,7 @@ mod tests {
|
||||
dispatch_stdio_request(
|
||||
&state,
|
||||
"thread/message",
|
||||
json!({"thread_id":"client-1","input":"hello"}),
|
||||
json!({"thread_id":"client-1","allowed_tools": [], "input":"hello"}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -4583,7 +4641,7 @@ mod tests {
|
||||
let error = dispatch_stdio_request(
|
||||
&state,
|
||||
"thread/message",
|
||||
json!({"thread_id":"client-1","input":"hello"}),
|
||||
json!({"thread_id":"client-1","allowed_tools": [], "input":"hello"}),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
@@ -4636,7 +4694,7 @@ mod tests {
|
||||
dispatch_stdio_request(
|
||||
&state,
|
||||
"thread/message",
|
||||
json!({"thread_id":"client-1","input":"retry"}),
|
||||
json!({"thread_id":"client-1","allowed_tools": [], "input":"retry"}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5290,11 +5348,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stdio_thread_goal_methods_round_trip_persisted_goal() {
|
||||
async fn stdio_thread_goal_methods_refuse_execution_and_read_existing_goal() {
|
||||
let (state, _tmp, server) = thread_control::compatibility_fixture().await;
|
||||
let capabilities = dispatch_stdio_request(&state, "thread/capabilities", json!({}))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(capabilities.result["goal_execution"], false);
|
||||
assert!(
|
||||
capabilities.result["methods"]
|
||||
.as_array()
|
||||
@@ -5316,9 +5375,17 @@ mod tests {
|
||||
json!({"thread_id":id,"objective":"Release 0.10.1","token_budget":59000}),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
set.data.unwrap()["error"],
|
||||
"interactive_execution_unsupported"
|
||||
);
|
||||
thread_control::handle(
|
||||
&state,
|
||||
serde_json::from_value(json!({"kind":"goal_set","thread_id":id,"objective":"Release 0.10.1","token_budget":59000})).unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(set.result["goal"]["objective"], "Release 0.10.1");
|
||||
assert_eq!(set.result["goal"]["status"], "active");
|
||||
let got = dispatch_stdio_request(&state, "thread/goal/get", json!({"thread_id":id}))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5489,7 +5556,7 @@ mod tests {
|
||||
client_writer
|
||||
.write_all(
|
||||
b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"thread/message\",\
|
||||
\"params\":{\"thread_id\":\"thr_a\",\"input\":\"go\"}}\n",
|
||||
\"params\":{\"thread_id\":\"thr_a\",\"input\":\"go\",\"allowed_tools\":[]}}\n",
|
||||
)
|
||||
.await
|
||||
.expect("send thread/message");
|
||||
@@ -5648,7 +5715,7 @@ mod tests {
|
||||
let result = dispatch_stdio_request(
|
||||
&state,
|
||||
"prompt/run",
|
||||
json!({"prompt":"review","maxOutputTokens":1500}),
|
||||
json!({"allowed_tools": [], "prompt":"review","maxOutputTokens":1500}),
|
||||
)
|
||||
.await;
|
||||
assert!(result.unwrap_err().message.contains("does not support"));
|
||||
@@ -5660,7 +5727,7 @@ mod tests {
|
||||
dispatch_stdio_request(
|
||||
&state,
|
||||
"prompt/run",
|
||||
json!({"prompt":"review","model":model,"maxOutputTokens":1500})
|
||||
json!({"allowed_tools": [], "prompt":"review","model":model,"maxOutputTokens":1500})
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
@@ -5673,15 +5740,15 @@ mod tests {
|
||||
for (method, params) in [
|
||||
(
|
||||
"prompt/run",
|
||||
json!({"prompt":"review","maxOutputTokens":1500}),
|
||||
json!({"allowed_tools": [], "prompt":"review","maxOutputTokens":1500}),
|
||||
),
|
||||
(
|
||||
"thread/message",
|
||||
json!({"thread_id":"stdio-cap","input":"review","maxOutputTokens":1500}),
|
||||
json!({"thread_id":"stdio-cap","allowed_tools": [], "input":"review","maxOutputTokens":1500}),
|
||||
),
|
||||
(
|
||||
"thread/request",
|
||||
json!({"kind":"message","thread_id":"request-cap","input":"review","maxOutputTokens":1500}),
|
||||
json!({"kind":"message","thread_id":"request-cap","allowed_tools": [], "input":"review","maxOutputTokens":1500}),
|
||||
),
|
||||
] {
|
||||
dispatch_stdio_request(&state, method, params)
|
||||
@@ -5692,6 +5759,7 @@ mod tests {
|
||||
&state,
|
||||
"http-cap".into(),
|
||||
"review".into(),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
std::num::NonZeroU32::new(1500),
|
||||
)
|
||||
@@ -5716,7 +5784,7 @@ mod tests {
|
||||
dispatch_stdio_request(
|
||||
&state,
|
||||
"prompt/run",
|
||||
json!({"prompt":"review","maxOutputTokens":invalid})
|
||||
json!({"allowed_tools": [], "prompt":"review","maxOutputTokens":invalid})
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
@@ -6099,7 +6167,7 @@ mod tests {
|
||||
&state,
|
||||
&mut writer,
|
||||
"prompt/request",
|
||||
json!({ "prompt": "what is 2+2" }),
|
||||
json!({ "allowed_tools": [], "prompt": "what is 2+2" }),
|
||||
AppTransport::Stdio,
|
||||
)
|
||||
.await
|
||||
@@ -6167,9 +6235,13 @@ mod tests {
|
||||
// Port 9 (discard) refuses immediately: no runtime is listening.
|
||||
seed_bridge_at(&state, "http://127.0.0.1:9".to_string()).await;
|
||||
|
||||
let err = dispatch_stdio_request(&state, "prompt/run", json!({ "prompt": "hello" }))
|
||||
.await
|
||||
.expect_err("a prompt with no reachable runtime must fail, not echo");
|
||||
let err = dispatch_stdio_request(
|
||||
&state,
|
||||
"prompt/run",
|
||||
json!({ "allowed_tools": [], "prompt": "hello" }),
|
||||
)
|
||||
.await
|
||||
.expect_err("a prompt with no reachable runtime must fail, not echo");
|
||||
assert_eq!(err.code, RUNTIME_UNAVAILABLE_CODE);
|
||||
|
||||
let (status, Json(body)) = http_error_from_jsonrpc(err);
|
||||
@@ -6184,9 +6256,13 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn empty_prompt_is_rejected_before_any_runtime_work() {
|
||||
let (state, _tmp) = capability_test_state();
|
||||
let err = dispatch_stdio_request(&state, "prompt/request", json!({ "prompt": " " }))
|
||||
.await
|
||||
.expect_err("an empty prompt must be rejected");
|
||||
let err = dispatch_stdio_request(
|
||||
&state,
|
||||
"prompt/request",
|
||||
json!({ "allowed_tools": [], "prompt": " " }),
|
||||
)
|
||||
.await
|
||||
.expect_err("an empty prompt must be rejected");
|
||||
assert_eq!(err.code, -32602);
|
||||
assert!(
|
||||
state.runtime_bridge.lock().await.is_none(),
|
||||
@@ -6205,6 +6281,7 @@ mod tests {
|
||||
&state,
|
||||
"thr_http".to_string(),
|
||||
"go".to_string(),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
None,
|
||||
)
|
||||
@@ -6237,6 +6314,7 @@ mod tests {
|
||||
&state,
|
||||
"thr_http".to_string(),
|
||||
"go".to_string(),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
None,
|
||||
)
|
||||
@@ -6248,6 +6326,73 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn submit_user_input_refuses_instead_of_claiming_resolution() {
|
||||
let (state, _tmp) = capability_test_state();
|
||||
for (method, params) in [
|
||||
(
|
||||
"thread/goal/set",
|
||||
json!({"thread_id":"unknown","objective":"must not start"}),
|
||||
),
|
||||
(
|
||||
"thread/request",
|
||||
json!({"kind":"goal_set","thread_id":"unknown","objective":"must not start"}),
|
||||
),
|
||||
] {
|
||||
let error = dispatch_stdio_request(&state, method, params)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let (status, Json(body)) = http_error_from_jsonrpc(error);
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
body["error"]["data"]["error"],
|
||||
"interactive_execution_unsupported"
|
||||
);
|
||||
assert!(state.runtime_thread_map.lock().await.is_empty());
|
||||
assert!(state.runtime_bridge.lock().await.is_none());
|
||||
}
|
||||
for tools in [Value::Null, json!(["shell"])] {
|
||||
for (method, params) in [
|
||||
(
|
||||
"thread/message",
|
||||
json!({"thread_id":"unknown","input":"do work","allowed_tools":tools}),
|
||||
),
|
||||
(
|
||||
"thread/request",
|
||||
json!({"kind":"message","thread_id":"unknown","input":"do work","allowed_tools":tools}),
|
||||
),
|
||||
(
|
||||
"prompt/run",
|
||||
json!({"prompt":"do work","allowed_tools":tools}),
|
||||
),
|
||||
(
|
||||
"prompt/request",
|
||||
json!({"prompt":"do work","allowed_tools":tools}),
|
||||
),
|
||||
] {
|
||||
let error = dispatch_stdio_request(&state, method, params)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, -32602);
|
||||
assert_eq!(
|
||||
error.data.unwrap()["error"],
|
||||
"interactive_execution_unsupported"
|
||||
);
|
||||
}
|
||||
let error = run_http_thread_message(
|
||||
&state,
|
||||
"unknown".into(),
|
||||
"do work".into(),
|
||||
serde_json::from_value(tools).unwrap(),
|
||||
Vec::new(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
error.data.unwrap()["error"],
|
||||
"interactive_execution_unsupported"
|
||||
);
|
||||
assert!(state.runtime_thread_map.lock().await.is_empty());
|
||||
assert!(state.runtime_bridge.lock().await.is_none());
|
||||
}
|
||||
let response = process_app_request(
|
||||
&state,
|
||||
AppRequest::SubmitUserInput {
|
||||
@@ -6763,7 +6908,10 @@ mod tests {
|
||||
let requests = seen.lock().await;
|
||||
assert_eq!(requests.len(), usize::from(supported));
|
||||
if supported {
|
||||
assert_eq!(requests[0], json!({"prompt":"look","images":images}));
|
||||
assert_eq!(
|
||||
requests[0],
|
||||
json!({"allowed_tools": [], "prompt":"look","images":images})
|
||||
);
|
||||
}
|
||||
server.abort();
|
||||
}
|
||||
@@ -6773,13 +6921,15 @@ mod tests {
|
||||
fn runtime_image_daemon_all_input_families_preserve_images() {
|
||||
let image = json!({"mime":"image/png","dataBase64":"AQ=="});
|
||||
let thread: ThreadMessageParams = serde_json::from_value(
|
||||
json!({"thread_id":"thr_fixture","input":"look","images":[image.clone()]}),
|
||||
json!({"thread_id":"thr_fixture","allowed_tools": [], "input":"look","images":[image.clone()]}),
|
||||
)
|
||||
.unwrap();
|
||||
let prompt: PromptRequest = serde_json::from_value(
|
||||
json!({"allowed_tools": [], "prompt":"look","images":[image.clone()]}),
|
||||
)
|
||||
.unwrap();
|
||||
let prompt: PromptRequest =
|
||||
serde_json::from_value(json!({"prompt":"look","images":[image.clone()]})).unwrap();
|
||||
let generic: ThreadRequest = serde_json::from_value(
|
||||
json!({"kind":"message","thread_id":"thr_fixture","input":"look","images":[image]}),
|
||||
json!({"kind":"message","thread_id":"thr_fixture","allowed_tools": [], "input":"look","images":[image]}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(thread.images, prompt.images);
|
||||
|
||||
@@ -1234,6 +1234,7 @@ async fn bridged_turn_sends_the_observed_owner_workspace_as_final_narrowing() {
|
||||
&state,
|
||||
"canonical-1".into(),
|
||||
"hello".into(),
|
||||
Some(Vec::new()),
|
||||
Vec::new(),
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -458,7 +458,11 @@ async fn owner_attaches_round_trips_and_shuts_down_cleanly() {
|
||||
for (id, method, params) in [
|
||||
(1, "capabilities", json!({})),
|
||||
(10, "thread/list", json!({})),
|
||||
(11, "prompt/run", json!({ "prompt": "hi" })),
|
||||
(
|
||||
11,
|
||||
"prompt/run",
|
||||
json!({ "prompt": "hi", "allowed_tools": [] }),
|
||||
),
|
||||
] {
|
||||
let early = client.call(id, method, params).await;
|
||||
assert_eq!(early["error"]["code"], json!(-32010), "{method}: {early}");
|
||||
|
||||
+11
-5
@@ -367,7 +367,7 @@ Forwarded serve options:
|
||||
|
||||
`codewhale serve --http` and `codewhale serve --mobile` remain compatibility
|
||||
aliases for `codewhale app-server --http` and `codewhale app-server --mobile`.
|
||||
New integrations should prefer `codewhale app-server`.")]
|
||||
New integrations should prefer `codewhale app-server --http` for the full Engine API.")]
|
||||
Serve(TuiPassthroughArgs),
|
||||
/// Open the first-class local browser client over the canonical Runtime API.
|
||||
#[command(
|
||||
@@ -399,18 +399,21 @@ New integrations should prefer `codewhale app-server`.")]
|
||||
Thread(ThreadArgs),
|
||||
/// Evaluate sandbox/approval policy decisions.
|
||||
Sandbox(SandboxArgs),
|
||||
/// Run the canonical runtime API / control plane (HTTP/SSE, mobile, stdio).
|
||||
/// Expose the full Engine API with --http, or smaller compatibility controls.
|
||||
#[command(after_help = "\
|
||||
Transports:
|
||||
codewhale app-server --http Full HTTP/SSE runtime API (/v1/*) on 127.0.0.1:7878
|
||||
codewhale app-server --mobile Runtime API + phone control page (127.0.0.1 only)
|
||||
codewhale app-server --stdio JSON-RPC control transport over stdio
|
||||
codewhale app-server --stdio Compatibility JSON-RPC controls and tool-free turns
|
||||
codewhale app-server Compatibility HTTP routes on the canonical owner at 127.0.0.1:8787
|
||||
|
||||
`--http` and `--mobile` serve the same mature runtime API as `codewhale serve
|
||||
--http`/`--mobile`, which remain as compatibility aliases. The runtime API token
|
||||
is read from --auth-token, CODEWHALE_RUNTIME_TOKEN, or DEEPSEEK_RUNTIME_TOKEN.
|
||||
|
||||
Compatibility turns require explicit allowed_tools: []; use --http for tools,
|
||||
approvals, clarification and the full event journal.
|
||||
|
||||
See docs/RUNTIME_API.md.")]
|
||||
AppServer(AppServerArgs),
|
||||
/// Generate shell completions.
|
||||
@@ -2001,8 +2004,8 @@ struct AppServerArgs {
|
||||
/// Equivalent to the legacy `codewhale serve --mobile`.
|
||||
#[arg(long, conflicts_with = "stdio")]
|
||||
mobile: bool,
|
||||
/// Run the app-server JSON-RPC control transport over stdio.
|
||||
/// Used by local SDKs and JSON-RPC integrations.
|
||||
/// Run the smaller compatibility JSON-RPC control transport over stdio.
|
||||
/// Tool-free turns require allowed_tools: []; use --http for interactive work.
|
||||
#[arg(long, default_value_t = false)]
|
||||
stdio: bool,
|
||||
/// Run as the desktop daemon: the same JSON-RPC control transport as
|
||||
@@ -6366,6 +6369,9 @@ fn run_app_server_command(
|
||||
path: args.socket_path.clone(),
|
||||
})
|
||||
} else if !args.http && !args.mobile {
|
||||
eprintln!(
|
||||
"note: bare app-server selects the compatibility API on port 8787 by default; use codewhale app-server --http for the full /v1/* API, approvals and SSE events"
|
||||
);
|
||||
Some(RuntimeControlFrontend::LegacyHttp)
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -598,6 +598,8 @@ pub enum ThreadRequest {
|
||||
Message {
|
||||
thread_id: String,
|
||||
input: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
allowed_tools: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
images: Vec<runtime::RuntimeImageInput>,
|
||||
#[serde(
|
||||
@@ -705,6 +707,8 @@ pub struct AppResponse {
|
||||
/// A simple prompt request that sends text to the model and returns output.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptRequest {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub allowed_tools: Option<Vec<String>>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "maxOutputTokens",
|
||||
|
||||
@@ -28,6 +28,19 @@ and independent workflow steps can continue after another child fails.
|
||||
|
||||
### Latest fixes
|
||||
|
||||
- Linux sandboxed terminals deliver Ctrl-C to the foreground program without
|
||||
killing the bubblewrap wrapper. A program that handles SIGINT can keep running
|
||||
and accept more input. PTY jobs and persistent terminals retain their sandbox
|
||||
and process-tree cleanup; this path requires util-linux `setsid`.
|
||||
|
||||
- Full Engine integrations now point explicitly to `codewhale app-server --http`.
|
||||
Compatibility HTTP, stdio and socket turns require `allowed_tools: []` and
|
||||
reject interactive execution and goal starts before work begins, closing the approval and
|
||||
clarification dead end. ACP new/load rejects unsupported supplied MCP servers
|
||||
instead of silently ignoring them; ACP v1 MCP support remains partial. The
|
||||
Runtime SDK guide states its Fleet/event coverage and provides authenticated
|
||||
HTTP examples for the remaining client controls.
|
||||
|
||||
- `tokio` moves to 1.53.3, which retires `RUSTSEC-2026-0386`. The advisory
|
||||
covers `tokio::io::unix::AsyncFd`'s safe constructors accepting a file
|
||||
descriptor they do not own; the lock now sits in a range the advisory lists as
|
||||
|
||||
@@ -362,6 +362,7 @@ impl AcpServer {
|
||||
self.sessions.insert(id, AcpSession { thread_id, cursor });
|
||||
}
|
||||
async fn new_session(&mut self, params: Value) -> std::result::Result<Value, AcpError> {
|
||||
validate_mcp_servers(¶ms)?;
|
||||
let cwd = params
|
||||
.get("cwd")
|
||||
.and_then(Value::as_str)
|
||||
@@ -459,6 +460,7 @@ impl AcpServer {
|
||||
}
|
||||
|
||||
async fn load_session(&mut self, params: Value) -> std::result::Result<Value, AcpError> {
|
||||
validate_mcp_servers(¶ms)?;
|
||||
let requested = params
|
||||
.get("sessionId")
|
||||
.and_then(Value::as_str)
|
||||
@@ -1027,6 +1029,17 @@ impl AcpError {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_mcp_servers(params: &Value) -> std::result::Result<(), AcpError> {
|
||||
match params.get("mcpServers") {
|
||||
None => Ok(()),
|
||||
Some(Value::Array(servers)) if servers.is_empty() => Ok(()),
|
||||
Some(Value::Array(_)) => Err(AcpError::invalid_params(
|
||||
"Client-supplied MCP servers are unsupported by Codewhale's ACP adapter. Use an empty mcpServers list, or configure MCP through codewhale app-server --http and the Runtime API. ACP v1 stdio MCP support is incomplete.",
|
||||
)),
|
||||
Some(_) => Err(AcpError::invalid_params("mcpServers must be an array")),
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_result(client_protocol_version: Option<u64>, config: &Config) -> Value {
|
||||
json!({
|
||||
"protocolVersion": client_protocol_version
|
||||
@@ -1828,6 +1841,20 @@ mod tests {
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn new_session_returns_durable_bare_uuid_without_provider_or_history() -> Result<()> {
|
||||
let mut rig = Rig::new(fixture_config(), vec![])?;
|
||||
for servers in [
|
||||
json!([{"name":"fixture","command":"must-not-execute","args":[],"env":[]}]),
|
||||
json!({}),
|
||||
Value::Null,
|
||||
] {
|
||||
let error = rig
|
||||
.server
|
||||
.new_session(json!({"mcpServers":servers}))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, -32602);
|
||||
assert!(rig.server.sessions.is_empty());
|
||||
assert!(rig.server.insertion_order.is_empty());
|
||||
}
|
||||
let id = rig.new_session().await;
|
||||
uuid::Uuid::parse_str(&id)?;
|
||||
let store = crate::session_manager::SessionManager::new(rig.server.sessions_dir.clone())?;
|
||||
@@ -1848,6 +1875,12 @@ mod tests {
|
||||
Some(id.as_str())
|
||||
);
|
||||
assert_eq!(rig.mock.call_count(), 0);
|
||||
for requested in [id.as_str(), &id[..8]] {
|
||||
let error = rig.server.load_session(json!({"sessionId":requested,"mcpServers":[{"name":"fixture","command":"must-not-execute","args":[],"env":[]}]})).await.unwrap_err();
|
||||
assert_eq!(error.code, -32602);
|
||||
assert_eq!(rig.server.sessions.len(), 1);
|
||||
assert_eq!(rig.mock.call_count(), 0);
|
||||
}
|
||||
let loaded = rig
|
||||
.server
|
||||
.load_session(json!({"sessionId":&id[..8]}))
|
||||
|
||||
@@ -127,7 +127,7 @@ pub fn is_available() -> bool {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn is_executable(path: &std::path::Path) -> bool {
|
||||
pub(crate) fn is_executable(path: &std::path::Path) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
std::fs::metadata(path)
|
||||
|
||||
@@ -337,6 +337,47 @@ impl ExecEnv {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_env = "ohos"))]
|
||||
pub fn pty_command(
|
||||
&self,
|
||||
cwd: &std::path::Path,
|
||||
) -> anyhow::Result<portable_pty::CommandBuilder> {
|
||||
let mut command = portable_pty::CommandBuilder::new(self.program());
|
||||
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
|
||||
if self.sandbox_type == SandboxType::LinuxBubblewrap {
|
||||
anyhow::ensure!(
|
||||
self.program() == bwrap::BWRAP_PATH,
|
||||
"sandboxed PTY requires the configured bubblewrap wrapper"
|
||||
);
|
||||
let args = self.args();
|
||||
let separator = args
|
||||
.iter()
|
||||
.position(|arg| arg == "--")
|
||||
.filter(|index| index + 1 < args.len())
|
||||
.ok_or_else(|| anyhow::anyhow!("sandboxed PTY command is missing its target"))?;
|
||||
let setsid = ["/usr/bin/setsid", "/bin/setsid"]
|
||||
.into_iter()
|
||||
.find(|path| bwrap::is_executable(std::path::Path::new(path)))
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("sandboxed PTY requires util-linux setsid; install util-linux")
|
||||
})?;
|
||||
command.args(&args[..=separator]);
|
||||
command.args([setsid, "--ctty", "--wait", "--"]);
|
||||
command.args(&args[separator + 1..]);
|
||||
command.set_controlling_tty(false);
|
||||
} else {
|
||||
command.args(self.args());
|
||||
}
|
||||
#[cfg(not(all(target_os = "linux", not(target_env = "ohos"))))]
|
||||
command.args(self.args());
|
||||
command.cwd(cwd);
|
||||
crate::child_env::apply_to_pty_command(
|
||||
&mut command,
|
||||
crate::child_env::string_map_env(&self.env),
|
||||
);
|
||||
Ok(command)
|
||||
}
|
||||
|
||||
/// Check if this execution is sandboxed.
|
||||
pub fn is_sandboxed(&self) -> bool {
|
||||
!matches!(self.sandbox_type, SandboxType::None)
|
||||
|
||||
@@ -43,7 +43,7 @@ use windows::Win32::System::JobObjects::{
|
||||
use windows::core::PCWSTR;
|
||||
|
||||
#[cfg(not(target_env = "ohos"))]
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
use portable_pty::{PtySize, native_pty_system};
|
||||
|
||||
mod guidance;
|
||||
mod output;
|
||||
@@ -2752,12 +2752,7 @@ impl ShellManager {
|
||||
})
|
||||
.context("Failed to open PTY")?;
|
||||
|
||||
let mut cmd = CommandBuilder::new(program);
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
cmd.cwd(working_dir);
|
||||
child_env::apply_to_pty_command(&mut cmd, child_env::string_map_env(&exec_env.env));
|
||||
let cmd = exec_env.pty_command(working_dir)?;
|
||||
|
||||
let mut child = pair
|
||||
.slave
|
||||
|
||||
@@ -355,17 +355,9 @@ fn create_session(
|
||||
|
||||
let (prepared, sandbox_confined) =
|
||||
prepare_pty_shell(&shell, &workspace, policy.clone(), shell_manager)?;
|
||||
let mut command = portable_pty::CommandBuilder::new(&prepared.command[0]);
|
||||
for arg in &prepared.command[1..] {
|
||||
command.arg(arg);
|
||||
}
|
||||
command.cwd(&prepared.cwd);
|
||||
// Same sanitized environment as the `exec_shell` PTY path; sandbox
|
||||
// markers from `prepared.env` are applied as explicit overrides.
|
||||
crate::child_env::apply_to_pty_command(
|
||||
&mut command,
|
||||
crate::child_env::string_map_env(&prepared.env),
|
||||
);
|
||||
let command = prepared
|
||||
.pty_command(&prepared.cwd)
|
||||
.map_err(|e| format!("failed to prepare PTY shell: {e}"))?;
|
||||
let child = pair
|
||||
.slave
|
||||
.spawn_command(command)
|
||||
|
||||
@@ -65,6 +65,17 @@ diverging from monotonic elapsed by more than the suspend threshold — reports
|
||||
`System sleep detected; connection lost — retrying request`, and re-issues the
|
||||
request instead of failing the turn (#2990).
|
||||
|
||||
## Linux sandboxed terminals
|
||||
|
||||
Sandboxed PTYs require bubblewrap and util-linux `setsid` at `/usr/bin/setsid`
|
||||
or `/bin/setsid`. Codewhale assigns the controlling terminal inside bubblewrap's
|
||||
PID namespace so Ctrl-C reaches the foreground program without killing its
|
||||
wrapper. A program that handles SIGINT can continue accepting input. Both
|
||||
`exec_shell` PTY jobs and persistent terminal sessions use this path. Missing
|
||||
`setsid` fails the PTY launch; it does not remove the sandbox. Non-PTY commands
|
||||
do not require this helper. Namespace isolation, read-only mounts and
|
||||
`--die-with-parent` cleanup remain in force.
|
||||
|
||||
## Windows support and release qualification
|
||||
|
||||
Windows compatibility depends on the runtime architecture and terminal. The
|
||||
|
||||
+66
-22
@@ -2,7 +2,7 @@
|
||||
|
||||
> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。
|
||||
|
||||
`codewhale app-server` exposes the [Codewhale Engine](ARCHITECTURE.md) through
|
||||
`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through
|
||||
its canonical local Runtime API and control plane.
|
||||
Local SDKs, mobile/remote-control clients, and editor integrations talk to it
|
||||
instead of screen-scraping terminal output. It serves the full HTTP/SSE runtime
|
||||
@@ -13,7 +13,9 @@ such as Zed.
|
||||
|
||||
`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**
|
||||
for `codewhale app-server --http` / `--mobile`; both launch the identical
|
||||
server. New integrations should target `app-server`.
|
||||
server. Desktop, web, SDK and custom integrations should target
|
||||
`app-server --http`. Bare `app-server`, `--stdio` and `--socket` select smaller
|
||||
compatibility interfaces, not the full Engine API.
|
||||
|
||||
`codewhale exec` is the separate one-shot headless worker path (stream-json,
|
||||
fleet worker subprocess, CI primitive). It is not part of this API, but it
|
||||
@@ -53,9 +55,9 @@ For the read-only record of what a thread or turn did, see
|
||||
| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |
|
||||
| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |
|
||||
| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |
|
||||
| `codewhale app-server --stdio` | JSON-RPC 2.0 over stdio | Local SDK / control probe (no listener) |
|
||||
| `codewhale app-server --stdio` | JSON-RPC 2.0 over stdio | Compatibility controls and explicit tool-free turns; no full Engine event or approval/input channel. |
|
||||
| `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. |
|
||||
| `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}`) |
|
||||
| `codewhale app-server` | HTTP on `127.0.0.1:8787` | Compatibility `/healthz`, `/thread`, `/app`, `/prompt`. Turn requests require explicit `allowed_tools: []`; interactive execution fails before a turn starts. Use `app-server --http` for tools, approvals, clarification, goal execution, steering and the full event journal. |
|
||||
| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |
|
||||
|
||||
`app-server --http` and `--mobile` launch the same mature runtime API server
|
||||
@@ -123,7 +125,7 @@ prompt/tool prefix.
|
||||
|
||||
### Workspace files and session artifacts
|
||||
|
||||
Native clients (the GPUI desktop's Files and Preview modules) browse and edit
|
||||
Native clients browse and edit
|
||||
the server's configured workspace through three authenticated routes. They
|
||||
read and write the workspace directly; there is no second file store, cache
|
||||
or index, and no path override: the workspace root is the only root.
|
||||
@@ -437,17 +439,11 @@ local integration clients can rely on it not changing silently.
|
||||
|
||||
### Daemon socket: `codewhale app-server --socket`
|
||||
|
||||
The desktop shell (DESKTOP-APP-BRIEF §2) attaches to a long-lived daemon over
|
||||
a unix domain socket. The wire is the `--stdio` transport verbatim — the same
|
||||
The socket transport exposes compatibility controls on a long-lived daemon.
|
||||
The wire is the `--stdio` transport verbatim — the same
|
||||
newline-delimited JSON-RPC 2.0 methods, dispatched by the same code — with one
|
||||
handshake in front of it.
|
||||
|
||||
> Note (2026-09-14): the Tauri desktop shell named above is retiring under the
|
||||
> 2026-09-14 product-client transition, and the DESKTOP-APP-BRIEF reference is
|
||||
> a dangling pointer (that brief does not exist in this repo). The GPUI client
|
||||
> in the private `codehwhale-gpui` repo is the successor daemon consumer over
|
||||
> this HTTP runtime API; the socket protocol described here is unchanged.
|
||||
|
||||
**Endpoint.** `--socket-path` if given; else `$CODEWHALE_HOME/run/daemon.sock`
|
||||
when `CODEWHALE_HOME` is set (an explicit home is an isolation boundary); else
|
||||
`$XDG_RUNTIME_DIR/codewhale/daemon.sock`; else
|
||||
@@ -495,6 +491,36 @@ left running. The owner's `shutdown` stops the listener, closes every
|
||||
connection, and removes the socket file. Journal replay from a client's
|
||||
last-seen `seq` is not part of this transport yet.
|
||||
|
||||
### Compatibility execution limits
|
||||
|
||||
The compatibility HTTP, stdio and socket interfaces advertise
|
||||
`interactive_execution: false`, `goal_execution: false`, `tool_free_turns: true` and
|
||||
`full_api_entrypoint: "codewhale app-server --http"`. Clients must require
|
||||
`tool_free_turns: true` before relying on an empty allowlist: older compatibility
|
||||
servers may ignore the field. Every `thread/message`,
|
||||
`thread/request` message, `prompt/run`, `prompt/request` and HTTP `/prompt` or
|
||||
`/thread` message requires an explicit `allowed_tools: []`. Omitted, null or
|
||||
nonempty lists fail with `interactive_execution_unsupported` before creating a
|
||||
Runtime thread or starting a turn. Malformed lists fail parameter decoding.
|
||||
JSON-RPC reports `-32602` with `data.error: "interactive_execution_unsupported"`;
|
||||
HTTP reports 400 with `error.code: "invalid_request"`, `error.jsonrpc_code: -32602`
|
||||
and the same detail in `error.data`. Compatibility `thread/goal/set` and
|
||||
`ThreadRequest::GoalSet` also refuse before changing the goal: setting an active
|
||||
goal starts background Engine work. Reading and clearing existing goals remain
|
||||
available; starting goals uses the full Runtime HTTP API. The known method
|
||||
remains dispatchable so older clients get this actionable refusal. For example:
|
||||
|
||||
```json
|
||||
{"jsonrpc":"2.0","id":3,"method":"thread/message","params":{"thread_id":"thr_...","input":"Summarize this conversation","allowed_tools":[]}}
|
||||
```
|
||||
|
||||
The bridge sends the same empty allowlist to the canonical Runtime for each
|
||||
admitted turn, so a tool-enabled thread cannot widen it. It forwards assistant
|
||||
text and terminal status. Unexpected approval or clarification requests fail
|
||||
and interrupt the exact owned turn; they cannot wait silently. This deliberately
|
||||
narrows the old compatibility behavior. Tool-enabled clients must migrate to
|
||||
the authenticated `/v1/*` API and SSE rather than enabling Full Access.
|
||||
|
||||
### Interrupting a turn
|
||||
|
||||
`thread/message` streams until the turn reaches a terminal state, which can
|
||||
@@ -525,6 +551,7 @@ same bridge `thread/message` uses. There is no local fallback: nothing else
|
||||
in the app-server can produce model output, so a prompt either runs or fails.
|
||||
|
||||
- `params.prompt` is required and must be non-empty (`-32602` otherwise).
|
||||
- `params.allowed_tools` must be `[]`; interactive turns use `app-server --http`.
|
||||
- `params.thread_id` is optional. With one, the prompt runs on that thread and
|
||||
its history. Without one, the runtime gets a fresh thread for that single
|
||||
turn; the mapping is dropped when the turn ends, so a one-shot prompt is not
|
||||
@@ -651,16 +678,23 @@ When a headless turn calls `request_user_input`, the runtime emits a
|
||||
POST /v1/user-input/{thread_id}/{request_id}
|
||||
```
|
||||
|
||||
The app-server control transport cannot accept that reply.
|
||||
The compatibility control transport cannot accept that reply.
|
||||
`app/request` with `SubmitUserInput` returns `ok: false` and
|
||||
`error: "user_input_reply_unsupported"`. This is a property of the transport,
|
||||
not an omission: while a turn is streaming, the stdio loop executes only
|
||||
`thread/interrupt` and queues everything else, so an answer sent there would
|
||||
wait on the very turn that is waiting for it.
|
||||
`error: "user_input_reply_unsupported"`. Compatibility turns require an empty
|
||||
tool catalog and refuse interactive execution at admission, so this limitation
|
||||
cannot leave a supported turn waiting for an answer. The fixed catalog and
|
||||
response limitation are Codewhale implementation choices.
|
||||
|
||||
## SDK contract
|
||||
|
||||
The app-server exists so an external SDK can answer — without scraping TUI
|
||||
The `@codewhale/runtime-sdk` package currently supplies Fleet helpers and
|
||||
read-only thread event streaming. It does not wrap thread creation, turn
|
||||
start/steer/interrupt, approval decisions, clarification answers or model
|
||||
discovery. Use authenticated raw HTTP for these operations; the package guide
|
||||
provides the [request recipe](../npm/runtime-sdk/README.md#full-engine-api).
|
||||
The hosted Responses SDK is a separate contract.
|
||||
|
||||
The full `app-server --http` API exists so an external SDK can answer — without scraping TUI
|
||||
output — *what route ran, which provider/model/reasoning/permission profile was
|
||||
effective, what events happened, how many tokens were used, and how the run
|
||||
finished.* The durable Thread/Turn/Item data model already carries most of
|
||||
@@ -678,7 +712,7 @@ this; the table maps each integration need to where a local client reads it.
|
||||
|
||||
For one-shot/headless automation, prefer `codewhale exec` with explicit
|
||||
`--provider <id> --model <id>` so a failure identifies the exact provider/model
|
||||
pair. Use `app-server` when a local integration needs to start, resume, steer,
|
||||
pair. Use `app-server --http` when a local integration needs to start, resume, steer,
|
||||
or interrupt turns, list models/capabilities, follow the event stream, or read
|
||||
usage. Both paths share the same runtime, so route-effective model resolution
|
||||
and the event vocabulary match.
|
||||
@@ -729,6 +763,16 @@ Full histories, tool pairs, signatures, media and partial-effect receipts are
|
||||
saved under the same checkpoint guard and session write lease used by HTTP.
|
||||
ACP display text may be shortened; the saved history is the full Core snapshot.
|
||||
|
||||
Codewhale 0.10.2 implements a partial ACP v1 adapter. Both `session/new`
|
||||
and `session/load` reject a nonempty client-supplied `mcpServers` list with
|
||||
`-32602` before creating, resuming or returning a cached session. The field may
|
||||
be omitted or `[]`; null and other non-array values are invalid. No supplied
|
||||
command is executed during this validation. [ACP v1 requires stdio MCP support](https://agentclientprotocol.com/protocol/v1/session-setup#mcp-servers);
|
||||
`mcpCapabilities.http: false` and `sse: false` only describe optional transports
|
||||
and do not establish full conformance. Use the full Runtime API and its existing
|
||||
MCP management for those integrations. The restricted tool catalog below is a
|
||||
Codewhale host profile, not an ACP protocol restriction.
|
||||
|
||||
The trusted local ACP profile narrows Core to file/search/git/patch and admitted
|
||||
foreground shell tools. Shell requires both editor terminal support and operator
|
||||
`allow_shell`; a requested external sandbox that is unavailable removes shell.
|
||||
@@ -1947,9 +1991,9 @@ tokens but `0.0` cost. Added in v0.8.10 (#564).
|
||||
}
|
||||
```
|
||||
|
||||
### Native client routes (GPUI desktop)
|
||||
### Native client routes
|
||||
|
||||
These families serve the GPUI desktop client over the same bearer-token
|
||||
These families serve native clients over the same bearer-token
|
||||
transport. They reuse the runtime's existing authorities — the engine's shell
|
||||
manager, the durable thread store, the workspace confinement layer, the
|
||||
config's credential plumbing — and add no second runtime, session store,
|
||||
|
||||
+11
-1
@@ -277,7 +277,7 @@ owner = "crates/execpolicy/src/approval_mode.rs"
|
||||
[[feature]]
|
||||
id = "sandbox"
|
||||
name = "Command sandbox"
|
||||
summary = "Wraps commands in Seatbelt on macOS; bubblewrap on Linux is on by default once installed and verified."
|
||||
summary = "Seatbelt on macOS; verified bubblewrap on Linux by default. Sandboxed PTYs require util-linux setsid."
|
||||
status = "stable"
|
||||
surfaces = { tui = "stable" }
|
||||
docs = "docs/SANDBOX.md"
|
||||
@@ -483,6 +483,16 @@ surfaces = { api = "stable" }
|
||||
docs = "docs/RUNTIME_API.md"
|
||||
owner = "crates/tui/src/runtime_api.rs"
|
||||
|
||||
[[feature]]
|
||||
id = "compatibility-tool-free-turns"
|
||||
name = "Compatibility turn admission"
|
||||
summary = "Compatibility transports require explicit tool-free turns; interactive clients use the full HTTP/SSE API."
|
||||
status = "stable"
|
||||
surfaces = { api = "stable" }
|
||||
since = "0.10.2"
|
||||
docs = "docs/RUNTIME_API.md#compatibility-execution-limits"
|
||||
owner = "crates/app-server/src/lib.rs"
|
||||
|
||||
[[feature]]
|
||||
id = "runtime-provider-keys"
|
||||
name = "Runtime provider keys"
|
||||
|
||||
@@ -39,6 +39,15 @@ CODEWHALE_PROVIDER=vllm VLLM_BASE_URL=http://127.0.0.1:8000/v1 VLLM_MODEL=<id> \
|
||||
`--auto` 默认启用 Full Access(完全访问):没有 Codewhale shell 沙箱,并开启信任模式。
|
||||
加上 `--sandbox workspace-write` 可保留 shell 沙箱和文件工具的工作区限制,并关闭信任模式。
|
||||
|
||||
## Linux 沙箱终端
|
||||
|
||||
沙箱 PTY 需要 bubblewrap,以及位于 `/usr/bin/setsid` 或 `/bin/setsid` 的
|
||||
util-linux `setsid`。Codewhale 在 bubblewrap 的 PID 命名空间内分配控制终端,
|
||||
让 Ctrl-C 发送到前台程序而不终止包装进程。处理 SIGINT 的程序仍可继续接收输入。
|
||||
`exec_shell` PTY 任务和持久终端会话共用此路径。缺少 `setsid` 会拒绝 PTY 启动,
|
||||
不会移除沙箱;非 PTY 命令不需要该辅助程序。命名空间隔离、只读挂载和
|
||||
`--die-with-parent` 清理仍然生效。
|
||||
|
||||
## 在回合期间让主机保持唤醒
|
||||
|
||||
交互式 TUI 回合(turn)进行中时,Codewhale 会持有平台的空闲休眠断言,
|
||||
|
||||
+39
-18
@@ -4,8 +4,9 @@
|
||||
> 最后与英文同步日期(last synced with English revision):2026-09-29。
|
||||
> 2026-10-06 补齐设备令牌意图、逐调用变更、技能详情、持久配置与账号宪章契约。
|
||||
> 2026-10-11 补齐工作区信任契约:`workspace_trusted` 与 `POST /v1/workspace/trust`。
|
||||
> 2026-10-23 补齐完整 API 入口、兼容执行限制与 ACP MCP 拒绝边界。
|
||||
|
||||
`codewhale app-server` 是本地运行时的规范 API 与控制面。本地 SDK、移动端/远程控制客户端
|
||||
`codewhale app-server --http` 是本地运行时的规范 API 与控制面。本地 SDK、移动端/远程控制客户端
|
||||
以及编辑器集成都与它对话,而不是去抓取终端输出。它提供完整的 HTTP/SSE 运行时
|
||||
API(`/v1/*`)、基于 stdio 的 JSON-RPC 控制传输,以及面向手机的移动端页面。
|
||||
`codewhale doctor --json` 提供机器可读的健康状态,`codewhale serve --acp` 则通过 stdio
|
||||
@@ -13,7 +14,7 @@ API(`/v1/*`)、基于 stdio 的 JSON-RPC 控制传输,以及面向手机
|
||||
|
||||
`codewhale serve --http` / `serve --mobile` 仍作为 `codewhale app-server --http` /
|
||||
`--mobile` 的**兼容别名**存在;两者启动的是同一个服务器。新的集成应当以
|
||||
`app-server` 为目标。
|
||||
`app-server --http` 为目标。裸 `app-server`、`--stdio` 和 `--socket` 是能力较小的兼容接口。
|
||||
|
||||
`codewhale exec` 是另一条一次性的无头 worker 路径(stream-json、fleet worker 子进程、
|
||||
CI 原语)。它不属于本 API,但共享同一个运行时、提供商/模型解析、权限配置
|
||||
@@ -53,7 +54,7 @@ local supervisor / SDK / automation harness
|
||||
| `codewhale app-server --mobile` | 回环上的 HTTP/SSE + `/mobile` | 运行时 API + 本地移动端控制页 |
|
||||
| `codewhale app-server --stdio` | 基于 stdio 的 JSON-RPC 2.0 | 本地 SDK / 控制探针(不监听端口) |
|
||||
| `codewhale app-server --socket [--socket-path P]` | 基于权限为 `0600` 的 Unix 域套接字(macOS/Linux)或仅限所有者的命名管道(Windows)的 JSON-RPC 2.0 | 桌面守护进程:多客户端、内核对端身份校验、`daemon/attach` 认领握手。Windows 原生应用验收仍待完成。 |
|
||||
| `codewhale app-server` | 在 `127.0.0.1:8787` 上的 HTTP | 旧式进程内 app-server(`/healthz`、`/thread`、`/app`、`/prompt`);后台作业请用运行时 API 的 `GET /v1/jobs`;`/prompt` 与 `/thread` 消息会通过运行时桥执行真实回合。它没有直接的 `/tool` 路由:工具只在 Engine 回合内部、在 Engine 的工具目录与审批姿态下运行。这个旧式服务器不呈现审批:它的桥只转发文本增量和回合的完成,并且没有决策路由,所以一个受审批门控的调用会一直等不到答复。受审批门控的工作请通过运行时 API 驱动(`/v1/threads/*` 事件与 `POST /v1/approvals/{approval_id}`) |
|
||||
| `codewhale app-server` | 在 `127.0.0.1:8787` 上的 HTTP | 兼容 `/healthz`、`/thread`、`/app`、`/prompt`。回合请求必须显式带 `allowed_tools: []`;交互执行在回合开始前拒绝。工具、审批、澄清、引导和完整事件日志请用 `app-server --http`。 |
|
||||
| `codewhale serve --http` / `--mobile` | 与 `app-server --http`/`--mobile` 相同的服务器 | 兼容别名 |
|
||||
|
||||
`app-server --http` 与 `--mobile` 启动的是历史上经由 `serve --http` 访问的同一个
|
||||
@@ -111,7 +112,7 @@ Drive 保留现有 Runtime/控制权限,但不能铸造、列举或撤销设
|
||||
|
||||
### 工作区文件与会话工件
|
||||
|
||||
原生客户端(GPUI 桌面的 Files 与 Preview 模块)通过三条需认证的路由浏览并编辑
|
||||
原生客户端通过三条需认证的路由浏览并编辑
|
||||
服务器配置的工作区。它们直接读写工作区;没有第二套文件存储、缓存或索引,
|
||||
也没有路径覆盖:工作区根目录是唯一的根目录。
|
||||
|
||||
@@ -343,17 +344,32 @@ printf '%s\n' \
|
||||
`crates/app-server/src/lib.rs` 中的一个漂移测试钉住,因此 SDK 与本地集成客户端
|
||||
可以依赖它不会悄悄变化。
|
||||
|
||||
### 兼容执行限制
|
||||
|
||||
HTTP 兼容路由、stdio 和 socket 均声明 `interactive_execution: false`、`goal_execution: false`、
|
||||
`tool_free_turns: true` 与 `full_api_entrypoint: "codewhale app-server --http"`。
|
||||
客户端必须先确认 `tool_free_turns: true`,才能依赖空工具列表;旧兼容服务器可能忽略该字段。
|
||||
`thread/message`、`thread/request` 的 message、`prompt/run`、`prompt/request`
|
||||
以及 HTTP `/prompt`、`/thread` 的 message 必须显式提供 `allowed_tools: []`。
|
||||
省略、null 或非空列表返回 `interactive_execution_unsupported`,不会创建 Runtime
|
||||
线程或启动回合;错误类型的值也会被拒绝。桥在实际回合请求中继续传递空工具列表,
|
||||
只转发助手文本和最终状态;意外的审批或澄清请求会报错并中断该回合。
|
||||
工具客户端须迁移到带 bearer 认证的完整 HTTP/SSE API。
|
||||
JSON-RPC 返回 `-32602` 和 `data.error: "interactive_execution_unsupported"`;
|
||||
HTTP 返回 400,`error.code` 为 `invalid_request`,同一详情位于 `error.data`。
|
||||
兼容 `thread/goal/set` 以及 `ThreadRequest::GoalSet` 也在修改目标前拒绝,
|
||||
因为设为活动目标会启动后台工作。读取、清除现有目标仍可用;开始目标请用完整 HTTP API。
|
||||
|
||||
`@codewhale/runtime-sdk` 当前只封装 Fleet 和线程事件读取,未封装线程创建、
|
||||
回合开始/引导/中断、审批、澄清或模型发现。完整集成请使用
|
||||
[SDK 指南中的认证 HTTP 示例](../../npm/runtime-sdk/README.md#full-engine-api)。
|
||||
|
||||
### 守护进程套接字:`codewhale app-server --socket`
|
||||
|
||||
桌面外壳(DESKTOP-APP-BRIEF §2)通过 unix 域套接字连接到一个长期存活的守护进程。
|
||||
此套接字传输在长期存活的守护进程上暴露兼容控制。
|
||||
其线上协议就是 `--stdio` 传输本身——同样的换行分隔 JSON-RPC 2.0 方法,由同一份代码
|
||||
分发——只是在前面加了一次握手。
|
||||
|
||||
> 注(2026-09-14):上面提到的 Tauri 桌面外壳在 2026-09-14 的产品客户端切换中正在退役,
|
||||
> 而 DESKTOP-APP-BRIEF 这个引用是一个悬空指针(本仓库中不存在该 brief)。私有
|
||||
> `codehwhale-gpui` 仓库中的 GPUI 客户端是这套 HTTP 运行时 API 的继任守护进程消费方;
|
||||
> 这里描述的套接字协议没有变化。
|
||||
|
||||
**端点。** 若给了 `--socket-path` 就用它;否则若设置了 `CODEWHALE_HOME` 则用
|
||||
`$CODEWHALE_HOME/run/daemon.sock`(显式指定 home 即是一条隔离边界);否则用
|
||||
`$XDG_RUNTIME_DIR/codewhale/daemon.sock`;再否则在 macOS 上用
|
||||
@@ -525,12 +541,10 @@ owner 存储中的原始操作;响应丢失或来源历史后来增长不会
|
||||
POST /v1/user-input/{thread_id}/{request_id}
|
||||
```
|
||||
|
||||
app-server 控制传输无法接受该回复。
|
||||
带 `SubmitUserInput` 的 `app/request` 会返回 `ok: false` 与
|
||||
`error: "user_input_reply_unsupported"`。这是该传输的固有性质,
|
||||
不是遗漏:当一个回合正在流式传输时,stdio 循环只执行
|
||||
`thread/interrupt`,其他请求一律排队,所以从那里发出的答复会
|
||||
等待那个正在等它的回合本身。
|
||||
兼容控制传输无法接受该回复。`app/request` 的 `SubmitUserInput` 返回
|
||||
`ok: false` 与 `error: "user_input_reply_unsupported"`。兼容回合要求空工具列表,
|
||||
交互执行在准入时拒绝,因此受支持的回合不会静默等待无法提交的答案。
|
||||
这是 Codewhale 当前实现的限制。
|
||||
|
||||
## SDK 契约
|
||||
|
||||
@@ -595,6 +609,13 @@ Core 回合、事件时间线、审批等待器和完整 Engine 会话快照。
|
||||
完整历史、工具调用/结果配对、签名、媒体和部分执行回执通过 HTTP 同用的检查点
|
||||
守卫与会话写入租约保存。ACP 展示文本可以缩短,持久历史仍是完整 Core 快照。
|
||||
|
||||
Codewhale 0.10.2 的 ACP v1 支持不完整。`session/new` 和 `session/load`
|
||||
会在创建、恢复或返回缓存会话之前拒绝非空 `mcpServers`,返回 `-32602`。
|
||||
该字段可省略或为 `[]`;null 与其他非数组值无效。验证不会执行所提供的命令。
|
||||
[ACP v1 要求 stdio MCP 支持](https://agentclientprotocol.com/protocol/v1/session-setup#mcp-servers);HTTP/SSE 能力为 false 并不代表完整合规。
|
||||
需要 MCP 的集成请用完整 Runtime API。下列工具限制来自 Codewhale host profile,
|
||||
不是 ACP 协议本身的限制。
|
||||
|
||||
受信任的本地 ACP profile 将 Core 收窄到文件/搜索/git/patch,以及获准的前台
|
||||
shell 工具。Shell 同时要求编辑器声明 terminal 支持、操作者允许 `allow_shell`;
|
||||
指定的外部沙箱不可用时不提供 shell。此接口不提供 MCP、动态工具、任务、PTY、
|
||||
@@ -1630,9 +1651,9 @@ token 但成本为 `0.0`。于 v0.8.10 加入(#564)。
|
||||
}
|
||||
```
|
||||
|
||||
### 原生客户端路由(GPUI 桌面)
|
||||
### 原生客户端路由
|
||||
|
||||
这些族在同一套 bearer 令牌传输上为 GPUI 桌面客户端服务。它们复用运行时既有的权威——
|
||||
这些族在同一套 bearer 令牌传输上为原生客户端服务。它们复用运行时既有的权威——
|
||||
引擎的 shell 管理器、持久线程存储、工作区限制层、
|
||||
配置的凭据管道——并不增加第二套运行时、会话存储、
|
||||
调度器或凭据存储。
|
||||
|
||||
@@ -5,6 +5,21 @@ Runtime API. The package is intentionally transport-only: it never bypasses the
|
||||
Rust runtime, sandbox, approvals, provider configuration, or Runtime execution
|
||||
ledger.
|
||||
|
||||
Version 0.10.2 wraps Fleet operations and read-only thread event streaming.
|
||||
It does **not** provide thread creation/resume, turn start/steer/interrupt,
|
||||
approval, clarification or model discovery helpers. Those operations already
|
||||
exist on the full Engine HTTP API; use the recipe below. This package is
|
||||
distinct from the hosted model/Responses API SDK, `codewhale-sdk`.
|
||||
|
||||
Start the full API with `codewhale app-server --http`. Bare `app-server`,
|
||||
`--stdio` and `--socket` are smaller compatibility interfaces. Their turns
|
||||
require explicit `allowed_tools: []` and cannot run interactive tools or start
|
||||
background goals.
|
||||
Require `tool_free_turns: true` in capabilities before relying on that restriction;
|
||||
older compatibility servers may ignore the allowlist field.
|
||||
The supervisor supplies `CODEWHALE_RUNTIME_TOKEN`; a generated Runtime token
|
||||
is not printed for discovery. Send the supplied token as a bearer credential.
|
||||
|
||||
```js
|
||||
import { createRuntimeClient } from "@codewhale/runtime-sdk";
|
||||
|
||||
@@ -53,7 +68,7 @@ for await (const event of client.fleetEvents(created.run.id, { after: cursor }))
|
||||
- `fleetEvents(runId, { after, limit })`
|
||||
- `createFleetRun(spec)`
|
||||
|
||||
The v0.9.11 Runtime implements the complete local managed-Fleet path. Fleet
|
||||
The Runtime implements the local managed-Fleet path. Fleet
|
||||
names the roster and selected member; the Runtime owns launch authority,
|
||||
durable run/worker/event state, replay, and execution. A creation request must
|
||||
name its roles, define a `parallel` Workflow, and select the explicit
|
||||
@@ -68,6 +83,85 @@ Older runtimes that do not expose one of these endpoints produce a
|
||||
`RuntimeCapabilityError` with a stable capability string instead of a generic
|
||||
fetch failure.
|
||||
|
||||
## Full Engine API
|
||||
|
||||
The existing HTTP API supplies the controls missing from this package. This
|
||||
example uses Node's built-in `fetch` and the same token as the client above;
|
||||
it does not add a second session store, turn loop or approval authority.
|
||||
|
||||
```js
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
const baseUrl = "http://127.0.0.1:7878";
|
||||
const token = process.env.CODEWHALE_RUNTIME_TOKEN;
|
||||
if (!token) throw new Error("The supervisor must supply CODEWHALE_RUNTIME_TOKEN");
|
||||
|
||||
async function runtime(path, { method = "GET", body, signal } = {}) {
|
||||
const url = new URL(path, baseUrl);
|
||||
if (url.origin !== new URL(baseUrl).origin || url.username || url.password) {
|
||||
throw new Error("Runtime requests must stay on the configured origin");
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method, signal, redirect: "error",
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
accept: "application/json",
|
||||
...(body === undefined ? {} : { "content-type": "application/json" }),
|
||||
},
|
||||
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Runtime HTTP ${response.status}: ${await response.text()}`);
|
||||
return response.status === 204 ? null : response.json();
|
||||
}
|
||||
|
||||
const info = await runtime("/v1/runtime/info");
|
||||
if (!info.capabilities.thread_create_idempotency ||
|
||||
!info.capabilities.turn_operation_idempotency ||
|
||||
!info.capabilities.turn_operation_lookup) {
|
||||
throw new Error("This client requires Runtime operation-key recovery support");
|
||||
}
|
||||
const providers = await runtime("/v1/providers");
|
||||
const createIntent = { operation_key: randomUUID(), workspace: process.cwd(), mode: "agent" };
|
||||
const thread = await runtime("/v1/threads", { method: "POST", body: createIntent });
|
||||
const threadPath = `/v1/threads/${encodeURIComponent(thread.id)}`;
|
||||
const snapshot = await runtime(threadPath);
|
||||
```
|
||||
|
||||
Persist `createIntent` before sending it. Recover an uncertain creation with
|
||||
the same operation key and original body; never mint a new key just because a
|
||||
response was lost. A turn likewise uses a persisted `operation_key`; inspect
|
||||
`GET /v1/threads/{id}/turn-operations/{operation_key}` after an uncertain turn
|
||||
response. HTTP acceptance is not terminal completion. Read the canonical
|
||||
snapshot, turn status, effective route metadata and events before reporting
|
||||
the outcome. An aborted fetch closes the request; it does not cancel Engine
|
||||
work. Use the explicit interrupt endpoint for that.
|
||||
|
||||
| Control | Authenticated HTTP request |
|
||||
| --- | --- |
|
||||
| Discover capabilities | `GET /v1/runtime/info` |
|
||||
| Discover models | `GET /v1/providers`, then `GET /v1/providers/{provider}/models?limit=250`; retain `model_provider_id` for configured routes and follow `nextCursor` |
|
||||
| Create/read a thread | `POST /v1/threads` / `GET /v1/threads/{id}` |
|
||||
| Resume | `POST /v1/threads/{id}/resume` |
|
||||
| Start a turn | `POST /v1/threads/{id}/turns` with `{ "prompt": "…", "operation_key": "…" }` |
|
||||
| Steer active work | `POST /v1/threads/{id}/turns/{turn_id}/steer` with `{ "prompt": "…" }` |
|
||||
| Interrupt | `POST /v1/threads/{id}/turns/{turn_id}/interrupt` |
|
||||
| Decide an approval | `POST /v1/approvals/{approval_id}` with `{ "decision": "allow" }` or `{ "decision": "deny" }`; `remember` defaults to false |
|
||||
| Answer clarification | `POST /v1/user-input/{id}/{request_id}` with `{ "answers": [{ "id": "question-id", "label": "selected option", "value": "answer" }] }` |
|
||||
|
||||
Encode each path identifier separately with `encodeURIComponent`. Decisions
|
||||
must come from the user and target the exact pending request from that thread's
|
||||
canonical `pending_approvals` or `pending_user_inputs`, including its turn/tool
|
||||
identity. Submit them while work is active; do not queue them behind completion.
|
||||
Expired or already answered IDs fail rather than settling another waiter.
|
||||
Use the existing `threadEvents()` helper for SSE and its cursor/end rules below.
|
||||
The snapshot's `latest_seq` is the initial subscription cursor; recover a
|
||||
replay gap by reloading the snapshot, without replaying a mutation.
|
||||
|
||||
This table is the core client journey, not an exhaustive schema or a claim
|
||||
that every planned capability is enabled. Honor `info.capabilities` and the
|
||||
[Runtime API contract](../../docs/RUNTIME_API.md), including permission,
|
||||
operation-key recovery and authenticated stream handling.
|
||||
|
||||
## Read a thread journal
|
||||
|
||||
`threadEvents(threadId, { sinceSeq, replayLimit, signal })` reads the existing
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@codewhale/runtime-sdk",
|
||||
"version": "0.10.2",
|
||||
"description": "Typed JavaScript helpers for Codewhale Runtime API Fleet endpoints.",
|
||||
"description": "JavaScript helpers for Codewhale Runtime Fleet operations and read-only thread events.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -288,8 +288,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/RUNTIME_API.md",
|
||||
"sha256": "1c412633f2dc8b3df98c7b33e7967845c75f7e3b70b3d467bfa5511c15db8686",
|
||||
"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": "82b8b74821dfefa8a6a3f16bec96a937fc146311cbd13e6bbb4df7333e486f4a",
|
||||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` 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. Desktop, web, SDK and custom integrations should target\n`app-server --http`. Bare `app-server`, `--stdio` and `--socket` select smaller\ncompatibility interfaces, not the full Engine API.\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 | Compatibility controls and explicit tool-free turns; no full Engine event or approval/input channel. |\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` | Compatibility `/healthz`, `/thread`, `/app`, `/prompt`. Turn requests require explicit `allowed_tools: []`; interactive execution fails before a turn starts. Use `app-server --http` for tools, approvals, clarification, goal execution, steering and the full event journal. |\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`: `watch` (the default when omitted) or explicit\n`drive`. Labels do not grant authority. Watch permits ordinary GET/HEAD reads,\nComputer displayLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"format": "codewhale.features.v1",
|
||||
"source": {
|
||||
"path": "docs/features.toml",
|
||||
"sha256": "4de7042a5390e82fa5e5ac55a7b33b932688f92ecafffe6071d41a371938638d"
|
||||
"sha256": "943a247aa65ed14d724ca81373832c6c741ed2bb04fea6bebab3df7167685ee8"
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
@@ -309,7 +309,7 @@
|
||||
{
|
||||
"id": "sandbox",
|
||||
"name": "Command sandbox",
|
||||
"summary": "Wraps commands in Seatbelt on macOS; bubblewrap on Linux is on by default once installed and verified.",
|
||||
"summary": "Seatbelt on macOS; verified bubblewrap on Linux by default. Sandboxed PTYs require util-linux setsid.",
|
||||
"status": "stable",
|
||||
"surfaces": {
|
||||
"tui": "stable"
|
||||
@@ -618,6 +618,19 @@
|
||||
"docs": "docs/RUNTIME_API.md",
|
||||
"registryLine": 478
|
||||
},
|
||||
{
|
||||
"id": "compatibility-tool-free-turns",
|
||||
"name": "Compatibility turn admission",
|
||||
"summary": "Compatibility transports require explicit tool-free turns; interactive clients use the full HTTP/SSE API.",
|
||||
"status": "stable",
|
||||
"surfaces": {
|
||||
"api": "stable"
|
||||
},
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md#compatibility-execution-limits",
|
||||
"registryLine": 487
|
||||
},
|
||||
{
|
||||
"id": "runtime-provider-keys",
|
||||
"name": "Runtime provider keys",
|
||||
@@ -629,7 +642,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md#put-v1providersidkey--write-only-credential",
|
||||
"registryLine": 487
|
||||
"registryLine": 497
|
||||
},
|
||||
{
|
||||
"id": "headless-exec",
|
||||
@@ -642,7 +655,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md",
|
||||
"registryLine": 497
|
||||
"registryLine": 507
|
||||
},
|
||||
{
|
||||
"id": "github-review",
|
||||
@@ -655,7 +668,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/GITHUB_APP.md",
|
||||
"registryLine": 506
|
||||
"registryLine": 516
|
||||
},
|
||||
{
|
||||
"id": "web-client",
|
||||
@@ -668,7 +681,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/WEB.md",
|
||||
"registryLine": 517
|
||||
"registryLine": 527
|
||||
},
|
||||
{
|
||||
"id": "keybindings",
|
||||
@@ -681,7 +694,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/KEYBINDINGS.md",
|
||||
"registryLine": 526
|
||||
"registryLine": 536
|
||||
},
|
||||
{
|
||||
"id": "settings",
|
||||
@@ -695,7 +708,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/CONFIGURATION.md",
|
||||
"registryLine": 535
|
||||
"registryLine": 545
|
||||
},
|
||||
{
|
||||
"id": "whats-new-notice",
|
||||
@@ -708,7 +721,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/CONFIGURATION.md#whats-new-after-an-update",
|
||||
"registryLine": 544
|
||||
"registryLine": 554
|
||||
},
|
||||
{
|
||||
"id": "themes",
|
||||
@@ -721,7 +734,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/schemas/user-theme.schema.json",
|
||||
"registryLine": 554
|
||||
"registryLine": 564
|
||||
},
|
||||
{
|
||||
"id": "chat-color",
|
||||
@@ -734,7 +747,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/GUIDE.md",
|
||||
"registryLine": 563
|
||||
"registryLine": 573
|
||||
},
|
||||
{
|
||||
"id": "localization",
|
||||
@@ -747,7 +760,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/LOCALIZATION.md",
|
||||
"registryLine": 573
|
||||
"registryLine": 583
|
||||
},
|
||||
{
|
||||
"id": "voice",
|
||||
@@ -761,7 +774,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 582
|
||||
"registryLine": 592
|
||||
},
|
||||
{
|
||||
"id": "pet",
|
||||
@@ -775,7 +788,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/PET_MODE.md",
|
||||
"registryLine": 590
|
||||
"registryLine": 600
|
||||
},
|
||||
{
|
||||
"id": "overview",
|
||||
@@ -788,7 +801,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/PROVIDERS.md",
|
||||
"registryLine": 599
|
||||
"registryLine": 609
|
||||
},
|
||||
{
|
||||
"id": "account-login",
|
||||
@@ -801,7 +814,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 608
|
||||
"registryLine": 618
|
||||
},
|
||||
{
|
||||
"id": "account-computer-files",
|
||||
@@ -814,7 +827,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 616
|
||||
"registryLine": 626
|
||||
},
|
||||
{
|
||||
"id": "context-anchors",
|
||||
@@ -827,7 +840,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 625
|
||||
"registryLine": 635
|
||||
},
|
||||
{
|
||||
"id": "window-pin",
|
||||
@@ -840,7 +853,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 633
|
||||
"registryLine": 643
|
||||
},
|
||||
{
|
||||
"id": "structured-copy",
|
||||
@@ -853,7 +866,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 641
|
||||
"registryLine": 651
|
||||
},
|
||||
{
|
||||
"id": "live-thinking",
|
||||
@@ -866,7 +879,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 649
|
||||
"registryLine": 659
|
||||
},
|
||||
{
|
||||
"id": "workbar",
|
||||
@@ -879,7 +892,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 657
|
||||
"registryLine": 667
|
||||
},
|
||||
{
|
||||
"id": "terminal-dock",
|
||||
@@ -892,7 +905,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 665
|
||||
"registryLine": 675
|
||||
},
|
||||
{
|
||||
"id": "telemetry",
|
||||
@@ -905,7 +918,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/TELEMETRY.md",
|
||||
"registryLine": 676
|
||||
"registryLine": 686
|
||||
},
|
||||
{
|
||||
"id": "docker",
|
||||
@@ -918,7 +931,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/DOCKER.md",
|
||||
"registryLine": 685
|
||||
"registryLine": 695
|
||||
},
|
||||
{
|
||||
"id": "termux",
|
||||
@@ -931,7 +944,7 @@
|
||||
"since": null,
|
||||
"flag": null,
|
||||
"docs": "docs/TERMUX.md",
|
||||
"registryLine": 694
|
||||
"registryLine": 704
|
||||
},
|
||||
{
|
||||
"id": "chatgpt-plan-sign-in",
|
||||
@@ -946,7 +959,7 @@
|
||||
"since": "0.10.1",
|
||||
"flag": null,
|
||||
"docs": "docs/PROVIDERS.md#sign-in-with-chatgpt",
|
||||
"registryLine": 703
|
||||
"registryLine": 713
|
||||
},
|
||||
{
|
||||
"id": "claude-plan-sign-in",
|
||||
@@ -961,7 +974,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/PROVIDERS.md#sign-in-with-claude",
|
||||
"registryLine": 713
|
||||
"registryLine": 723
|
||||
},
|
||||
{
|
||||
"id": "plugin-oauth-providers",
|
||||
@@ -976,7 +989,7 @@
|
||||
"since": "0.10.1",
|
||||
"flag": null,
|
||||
"docs": "docs/PLUGIN_PROVIDERS.md",
|
||||
"registryLine": 723
|
||||
"registryLine": 733
|
||||
},
|
||||
{
|
||||
"id": "orcarouter-sign-in",
|
||||
@@ -991,7 +1004,7 @@
|
||||
"since": "0.10.1",
|
||||
"flag": null,
|
||||
"docs": "docs/PROVIDERS.md",
|
||||
"registryLine": 733
|
||||
"registryLine": 743
|
||||
},
|
||||
{
|
||||
"id": "merch-interest",
|
||||
@@ -1006,7 +1019,7 @@
|
||||
"since": "unreleased",
|
||||
"flag": null,
|
||||
"docs": "web/lib/merch/README.md",
|
||||
"registryLine": 744
|
||||
"registryLine": 754
|
||||
},
|
||||
{
|
||||
"id": "profile-constitution",
|
||||
@@ -1020,7 +1033,7 @@
|
||||
"since": "0.10.1",
|
||||
"flag": null,
|
||||
"docs": "docs/RUNTIME_API.md#profile-constitution",
|
||||
"registryLine": 754
|
||||
"registryLine": 764
|
||||
},
|
||||
{
|
||||
"id": "plugin-shell-install",
|
||||
@@ -1033,7 +1046,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": "docs/PLUGINS.md",
|
||||
"registryLine": 764
|
||||
"registryLine": 774
|
||||
},
|
||||
{
|
||||
"id": "herdr-agent-state",
|
||||
@@ -1046,7 +1059,7 @@
|
||||
"since": "0.10.2",
|
||||
"flag": null,
|
||||
"docs": null,
|
||||
"registryLine": 774
|
||||
"registryLine": 784
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user