From ac116601a7544de878d65924f409f0d0c7a12f95 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 5 Nov 2026 12:17:36 -0800 Subject: [PATCH 1/8] contract follow-ups: object/destructured bridge senders, closed empty schemas, raw method strings, _meta.acpmux keys, open-remote --focus Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --- .../crates/cmux-tui/src/cli/action_hint.rs | 26 +++ cmux-tui/crates/cmux-tui/src/cli/app.rs | 18 +- .../src/cli/tests/action_argument_contract.rs | 115 +++++++++- scripts/ci/check-swift-acpmux-protocol.py | 196 ++++++++++++++---- tests/test_check_swift_acpmux_protocol.py | 63 +++++- .../acpmux/agentBridgeContract.test.ts | 103 ++++++++- 6 files changed, 474 insertions(+), 47 deletions(-) diff --git a/cmux-tui/crates/cmux-tui/src/cli/action_hint.rs b/cmux-tui/crates/cmux-tui/src/cli/action_hint.rs index 28ad80f46db3..1b9000f58e21 100644 --- a/cmux-tui/crates/cmux-tui/src/cli/action_hint.rs +++ b/cmux-tui/crates/cmux-tui/src/cli/action_hint.rs @@ -32,6 +32,32 @@ fn non_verbs() -> &'static HashMap { }) } +/// Whether the app action named `action` (its id or its CLI name) takes an argument called `argument` +/// (plans/cmux-next/action-surfaces.json `args`). +pub(super) fn action_takes_argument(action: &str, argument: &str) -> bool { + static ARGS: OnceLock>> = OnceLock::new(); + ARGS.get_or_init(|| { + let doc: Value = serde_json::from_str(ACTIONS_JSON).unwrap_or(Value::Null); + let mut map = HashMap::new(); + for action in doc["actions"].as_array().into_iter().flatten() { + let names: Vec = action["args"] + .as_array() + .into_iter() + .flatten() + .filter_map(|arg| arg["name"].as_str().map(str::to_owned)) + .collect(); + for key in [&action["id"], &action["cli_name"]] { + if let Some(key) = key.as_str() { + map.insert(key.to_owned(), names.clone()); + } + } + } + map + }) + .get(action) + .is_some_and(|names| names.iter().any(|name| name == argument)) +} + /// Whether the catalog has an app action that the CLI offers as `cmux `. pub(super) fn offered_app_action(name: &str) -> bool { static OFFERED: OnceLock> = OnceLock::new(); diff --git a/cmux-tui/crates/cmux-tui/src/cli/app.rs b/cmux-tui/crates/cmux-tui/src/cli/app.rs index a205b6c0ab44..7e3905ecf33c 100644 --- a/cmux-tui/crates/cmux-tui/src/cli/app.rs +++ b/cmux-tui/crates/cmux-tui/src/cli/app.rs @@ -551,7 +551,17 @@ pub(super) fn run_action( continue; } "focus" => { - params.insert("focus".into(), json!(true)); + // An action with its own `focus` argument (`open-remote`) takes the flag as that argument too, + // with an optional `true`/`false` after it; the run's own focus follows it. + let mut value = true; + if super::action_hint::action_takes_argument(action, "focus") { + if let Some(word @ ("true" | "false")) = args.get(index + 1).map(String::as_str) { + value = word == "true"; + index += 1; + } + arguments.insert("focus".into(), json!(value)); + } + params.insert("focus".into(), json!(value)); index += 1; continue; } @@ -584,6 +594,12 @@ pub(super) fn run_action( .ok_or_else(|| UsageError::new(messages.arg_shape.replace("{value}", text)))?; arguments.insert(key.into(), json!(value)); } + // `--focus=false` for an action with a `focus` argument: the argument, and the run's own focus. + "focus" if super::action_hint::action_takes_argument(action, "focus") => { + let on = value.as_str().is_none_or(|text| !text.eq_ignore_ascii_case("false")); + params.insert("focus".into(), json!(on)); + arguments.insert("focus".into(), json!(on)); + } _ => { let key = argument_name(&name); // A file or folder of the caller's working directory, for an app that runs elsewhere. diff --git a/cmux-tui/crates/cmux-tui/src/cli/tests/action_argument_contract.rs b/cmux-tui/crates/cmux-tui/src/cli/tests/action_argument_contract.rs index 2ead38ed8db1..748934961935 100644 --- a/cmux-tui/crates/cmux-tui/src/cli/tests/action_argument_contract.rs +++ b/cmux-tui/crates/cmux-tui/src/cli/tests/action_argument_contract.rs @@ -59,6 +59,27 @@ const CLI_FLAGS: &[&str] = &[ "timeout", ]; +/// CLI-owned flags `run_action` also hands to an action that declares an argument of that name +/// (`open-remote --focus false`: the run's focus and the action's `focus`). +const SHARED_FLAGS: &[&str] = &["focus"]; + +/// Action arguments whose flag the CLI's global parser takes first (`--session` names a daemon), with how +/// a caller reaches them instead. A new collision fails `every_argument_named_like_a_cli_flag_reaches_its_action`. +const GLOBAL_FLAG_ARGUMENTS: &[(&str, &str, &str)] = &[ + ( + "agent.openSessionWorkspace", + "session", + "--arg session=ID (or `cmux action run ... --session`)", + ), + ("agent.openSessionTab", "session", "--arg session=ID (or `cmux action run ... --session`)"), + ("remote.connect", "session", "--arg session=ID (or `cmux action run ... --session`)"), + ( + "history.resumeAgentSession", + "session", + "--arg session=ID (or `cmux action run ... --session`)", + ), +]; + /// Flags a CLI scope takes for itself before the words reach the app: `chat /// send`'s `--from`/`--as` (the sender label) and `--class` are read by /// chief/chat_scope.rs, which rewrites the words without them. @@ -696,7 +717,7 @@ fn cli_requests() -> Value { }; let flag = kebab(name); // A name `run_action` takes for itself (`--target`, `--focus`) goes as `--arg name=value`. - if CLI_FLAGS.contains(&flag.as_str()) { + if CLI_FLAGS.contains(&flag.as_str()) && !SHARED_FLAGS.contains(&flag.as_str()) { words.extend([ "--arg".to_owned(), format!("{name}={}", value.as_deref().unwrap_or("true")), @@ -732,3 +753,95 @@ fn the_cli_request_fixture_is_what_the_cli_encodes() { path.display() ); } + +/// Every argument of an offered action whose `--flag` the CLI also owns either reaches the action through +/// that flag (`SHARED_FLAGS`: `open-remote --focus false`) or is listed in `GLOBAL_FLAG_ARGUMENTS` with the +/// spelling that reaches it. `open-remote`'s `focus` was only reachable as `--arg focus=true`. +#[test] +fn every_argument_named_like_a_cli_flag_reaches_its_action() { + let path = repo_root().join("plans/cmux-next/action-surfaces.json"); + let root: Value = + serde_json::from_str(&std::fs::read_to_string(&path).expect("action-surfaces.json")) + .unwrap(); + let mut unreached = Vec::new(); + let mut listed = Vec::new(); + for action in root["actions"].as_array().expect("actions") { + let Some(cli_name) = action["cli_name"].as_str().filter(|_| action["cli"] == "offered") + else { + continue; + }; + let id = action["id"].as_str().unwrap_or_default(); + for arg in action["args"].as_array().into_iter().flatten() { + let name = arg["name"].as_str().unwrap_or_default(); + let flag = kebab(name); + if !CLI_FLAGS.contains(&flag.as_str()) { + continue; + } + if GLOBAL_FLAG_ARGUMENTS + .iter() + .any(|(action, argument, _)| *action == id && *argument == name) + { + listed.push(format!("{id} {name}")); + continue; + } + let value = if arg["kind"] == "bool" { "false" } else { "contract-1" }; + let mut words = cli_name.split_whitespace().map(str::to_owned).collect::>(); + words.extend([format!("--{flag}"), value.to_owned()]); + let params = encoded(&words); + let sent = ¶ms["args"][name]; + let want = if arg["kind"] == "bool" { + serde_json::json!(false) + } else { + serde_json::json!(value) + }; + if *sent != want { + unreached + .push(format!("{id}: `cmux {}` sends args.{name} = {sent}", words.join(" "))); + } + } + } + assert!( + unreached.is_empty(), + "these action arguments are out of their own flag's reach:\n{}", + unreached.join("\n") + ); + let stale = GLOBAL_FLAG_ARGUMENTS + .iter() + .map(|(action, argument, _)| format!("{action} {argument}")) + .filter(|entry| !listed.contains(entry)) + .collect::>(); + assert!( + stale.is_empty(), + "GLOBAL_FLAG_ARGUMENTS lists arguments the catalog no longer has: {stale:?}" + ); +} + +/// `open-remote --focus false` is the action's `focus` argument and the run's own focus; a bare `--focus` is +/// both true; `--focus=false` is the same as `--focus false`. +#[test] +fn open_remote_focus_reaches_the_action() { + let words = |tail: &[&str]| { + let mut words = vec!["open-remote".to_owned(), "--host".to_owned(), "h".to_owned()]; + words.extend(tail.iter().map(|word| (*word).to_owned())); + encoded(&words) + }; + for (tail, want) in [ + (&["--focus", "false"][..], false), + (&["--focus"][..], true), + (&["--focus=false"][..], false), + ] { + let params = words(tail); + assert_eq!(params["args"]["focus"], serde_json::json!(want), "{tail:?}: {params}"); + assert_eq!(params["focus"], serde_json::json!(want), "{tail:?}: {params}"); + } + // An action without a `focus` argument keeps `--focus` as the run's focus only. + let params = encoded(&[ + "workspace".to_owned(), + "rename".to_owned(), + "--focus".to_owned(), + "--name".to_owned(), + "x".to_owned(), + ]); + assert_eq!(params["focus"], serde_json::json!(true)); + assert!(params["args"].get("focus").is_none(), "{params}"); +} diff --git a/scripts/ci/check-swift-acpmux-protocol.py b/scripts/ci/check-swift-acpmux-protocol.py index 32ae09d02806..3a87fee88f01 100755 --- a/scripts/ci/check-swift-acpmux-protocol.py +++ b/scripts/ci/check-swift-acpmux-protocol.py @@ -9,7 +9,10 @@ (`_acpmux/...`, `session/...`, `acp.*`, `trigger/...`) must be a protocol method; * every method and param key in the agent pane's frame policy (CmuxNextAgentPane/AcpmuxPaneMethods*.swift: `requests`, `notifications`, `knownParams`) - must be a protocol method and, where the method's params are typed, a property of them. + must be a protocol method and, where the method's params are typed, a property of them; + * every `_meta.acpmux` key the app writes into a request (knownParams' second set, and the + `acpmux["key"] = ...` / `setAcpmuxMeta(...)` / `setDecision(..., field:)` writes) must be a + property of the protocol's `AcpmuxRequestMeta`. A name that is not one (a host-side op that shares the shape, a method acpmux has not registered yet) is listed in scripts/ci/swift-acpmux-protocol-allowlist.txt with its reason. @@ -89,67 +92,164 @@ def _resolve(self, schema): return schema def properties(self, method: str) -> set[str] | None: - """The method's param names, or None when its params are untyped or open.""" + """The method's param names, or None when its params are untyped or open. A closed object + with no properties (`{"additionalProperties": false}`) takes no key: an empty set.""" params = self.methods[method].get("params") if not isinstance(params, dict): return None + return self.schema_properties(params) + + def type_properties(self, name: str) -> set[str] | None: + """A named type's property names (as ``schema_properties``), or None when it is missing.""" + if name not in self.types: + return None + return self.schema_properties(self.types[name]) + + def schema_properties(self, root) -> set[str] | None: names: set[str] = set() - stack = [params] + closed = False + stack = [root] while stack: schema = self._resolve(stack.pop()) if not isinstance(schema, dict): return None + branches = [b for key in ("allOf", "anyOf", "oneOf") for b in schema.get(key, [])] + # `null` beside an object (an optional param) adds no key. + if schema.get("type") == "null": + continue + is_object = "properties" in schema or schema.get("type") == "object" or "additionalProperties" in schema # A JSON Schema object takes keys beyond its properties unless additionalProperties is false. - if "properties" in schema and schema.get("additionalProperties") is not False: + if is_object and schema.get("additionalProperties") is not False: return None + if is_object: + closed = True names.update(schema.get("properties", {})) - for key in ("allOf", "anyOf", "oneOf"): - stack.extend(schema.get(key, [])) - return names or None + stack.extend(branches) + return names if closed else (names or None) -def set_literal(text: str, name: str) -> list[str]: - """The strings of `static let NAME: Set = [ ... ]` (or `[String]`).""" +def swift_string_at(text: str, i: int) -> tuple[int, str] | None: + """The Swift string literal starting at `i` (`"..."`, `#"..."#`, `\"\"\"...\"\"\"`, `#\"\"\"...\"\"\"#`, any + number of `#`): (index just past it, its value), or None when no literal starts there. A raw string + keeps its backslashes; a multiline one is its lines without their common indentation, joined.""" + j = i + while j < len(text) and text[j] == "#": + j += 1 + hashes = j - i + if not text.startswith('"', j): + return None + if text.startswith('"""', j): + close = '"""' + "#" * hashes + end = text.find(close, j + 3) + end = len(text) if end < 0 else end + lines = text[j + 3 : end].split("\n")[1:] + if lines and not lines[-1].strip(): + lines = lines[:-1] + value = "\n".join(line.strip() for line in lines) + return end + len(close), value + close = '"' + "#" * hashes + k = j + 1 + value = [] + while k < len(text) and text[k] != "\n": + if text.startswith(close, k): + return k + len(close), "".join(value) + if text[k] == "\\" and hashes == 0: + value.append(text[k + 1 : k + 2]) + k += 2 + continue + value.append(text[k]) + k += 1 + return k, "".join(value) + + +def swift_tokens(text: str) -> list[tuple[str, str]]: + """Swift source as ("str", value) for each string literal and ("p", char) for each bracket, + paren, comma or colon; everything else dropped.""" + out: list[tuple[str, str]] = [] + i = 0 + while i < len(text): + literal = swift_string_at(text, i) if text[i] in '"#' else None + if literal: + i, value = literal + out.append(("str", value)) + elif text[i] in "[](),:": + out.append(("p", text[i])) + i += 1 + else: + i += 1 + return out + + +def declaration_tokens(text: str, name: str) -> list[tuple[str, str]]: + """The tokens of `static let NAME: T = [ ... ]`'s array, outer brackets excluded.""" m = re.search(rf"static let {name}\s*:[^=]*=\s*\[", text) if not m: return [] - depth, i = 1, m.end() - while i < len(text) and depth: - if text[i] == "[": + tokens = swift_tokens(text[m.end() :]) + depth = 1 + for index, (kind, value) in enumerate(tokens): + if kind == "p" and value in "[(": depth += 1 - elif text[i] == "]": + elif kind == "p" and value in "])": depth -= 1 - elif text[i] == '"': - j = i + 1 - while text[j] != '"': - j += 2 if text[j] == "\\" else 1 + if depth == 0: + return tokens[:index] + return tokens + + +def set_literal(text: str, name: str) -> list[str]: + """The strings of `static let NAME: Set = [ ... ]` (or `[String]`), raw and multiline included.""" + return [value for kind, value in declaration_tokens(text, name) if kind == "str"] + + +def known_params_with_meta(text: str) -> dict[str, tuple[list[str], list[str]]]: + """`knownParams`: method -> (its top-level param names, its `_meta.acpmux` keys).""" + tokens = declaration_tokens(text, "knownParams") + out: dict[str, tuple[list[str], list[str]]] = {} + i = 0 + while i < len(tokens): + # "method" : ( [ ... ] , [ ... ] ) + if tokens[i][0] == "str" and tokens[i + 1 : i + 3] == [("p", ":"), ("p", "(")]: + method = tokens[i][1] + lists: list[list[str]] = [] + j = i + 3 + depth = 1 + current: list[str] | None = None + while j < len(tokens) and depth: + kind, value = tokens[j] + if kind == "p" and value == "[": + current = [] + elif kind == "p" and value == "]": + lists.append(current or []) + current = None + elif kind == "p" and value == "(": + depth += 1 + elif kind == "p" and value == ")": + depth -= 1 + elif kind == "str" and current is not None: + current.append(value) + j += 1 + params, meta = out.setdefault(method, ([], [])) + params.extend(lists[0] if lists else []) + meta.extend(lists[1] if len(lists) > 1 else []) i = j - i += 1 - return STRING.findall(text[m.end() : i - 1]) + else: + i += 1 + return out def known_params(text: str) -> dict[str, list[str]]: """`knownParams`: method -> its top-level param names (the tuple's first set).""" - m = re.search(r"static let knownParams\s*:[^=]*=\s*\[", text) - if not m: - return {} - depth, i = 1, m.end() - while i < len(text) and depth: - if text[i] in "[(": - depth += 1 - elif text[i] in "])": - depth -= 1 - elif text[i] == '"': - j = i + 1 - while text[j] != '"': - j += 2 if text[j] == "\\" else 1 - i = j - i += 1 - body = text[m.end() : i - 1] - out: dict[str, list[str]] = {} - for entry in re.finditer(r'"([^"]+)"\s*:\s*\(\s*\[([^\]]*)\]', body): - out.setdefault(entry.group(1), []).extend(STRING.findall(entry.group(2))) - return out + return {method: params for method, (params, _) in known_params_with_meta(text).items()} + + +# The `_meta.acpmux` keys the app writes into a request frame outside knownParams. +META_WRITES = ( + re.compile(r'\bacpmux\["([A-Za-z0-9_]+)"\]\s*=(?!=)'), + re.compile(r'\bsetAcpmuxMeta\(\s*"([A-Za-z0-9_]+)"'), + re.compile(r'\bsetAcpmuxMeta\(\s*\[\s*"([A-Za-z0-9_]+)"\s*:'), + re.compile(r'\bsetDecision\([^)]*\bfield:\s*(?:String\s*=\s*)?"([A-Za-z0-9_]+)"'), +) def load_allowlist(path: Path) -> dict[str, str]: @@ -208,6 +308,24 @@ def unknown(name: str, where: str) -> None: if key not in names: unknown(f"{method}#{key}", f"AcpmuxPaneMethods.knownParams[{method}]") + # 3. The `_meta.acpmux` keys the app writes into requests. + meta_keys = protocol.type_properties("AcpmuxRequestMeta") + if meta_keys is None: + problems.append(f"{PROTOCOL}: no closed AcpmuxRequestMeta type, so the app's _meta.acpmux keys go unchecked") + else: + for method, (_, keys) in sorted(known_params_with_meta(pane).items()): + for key in keys: + if key not in meta_keys: + unknown(f"_meta.acpmux#{key}", f"AcpmuxPaneMethods.knownParams[{method}]") + for path in sorted((root / PANE).rglob("*.swift")): + text = strip_comments(path.read_text(errors="ignore")) + rel = path.relative_to(root) + for lineno, line in enumerate(text.splitlines(), 1): + for pattern in META_WRITES: + for key in pattern.findall(line): + if key not in meta_keys: + unknown(f"_meta.acpmux#{key}", f"{rel}:{lineno}") + for name in sorted(set(allowed) - used): if "#" not in name and name in protocol.methods: problems.append(f"{ALLOWLIST}: '{name}' is in the protocol now; remove it from the list") diff --git a/tests/test_check_swift_acpmux_protocol.py b/tests/test_check_swift_acpmux_protocol.py index e86f1383f527..3c15d0ad524f 100644 --- a/tests/test_check_swift_acpmux_protocol.py +++ b/tests/test_check_swift_acpmux_protocol.py @@ -21,6 +21,9 @@ {"kind": "request", "name": "session/prompt", "params": True, "result": True}, {"kind": "request", "name": "acp.session.fork", "params": {"$ref": "#/types/ForkParams"}, "result": True}, {"kind": "request", "name": "_acpmux/attach", "params": {"$ref": "#/types/AttachParams"}, "result": True}, + # A closed object with no properties: the method takes no param. + {"kind": "request", "name": "_acpmux/peers", "params": {"type": "object", "additionalProperties": False}, + "result": True}, ], "types": { # No additionalProperties: an open object, so a key beyond its properties is allowed. @@ -28,7 +31,12 @@ "AttachParams": { "additionalProperties": False, "properties": {"sessionId": {"type": "string"}, "afterSeq": {"type": ["integer", "null"]}}, - } + }, + "AcpmuxRequestMeta": { + "type": "object", + "additionalProperties": False, + "properties": {"promptId": {}, "localAppToken": {}, "decisionNonce": {}, "personDecision": {}}, + }, }, } @@ -36,7 +44,7 @@ nonisolated enum AcpmuxPaneMethods { public static let initialize = "{initialize}" public static let requests: Set = [ - "session/prompt", "_acpmux/attach", {extra_request} + "session/prompt", "_acpmux/attach", "_acpmux/peers", {extra_request} ] public static let notifications: Set = ["session/prompt"] } @@ -47,6 +55,7 @@ static let knownParams: [String: (params: Set, acpmux: Set)] = [ "_acpmux/attach": (["sessionId", "afterSeq"{extra_param}], ["promptId"]), "acp.session.fork": (["sessionId", "cwd"], []), + "_acpmux/peers": ([{peers_param}], [{extra_meta}]), ] static let other: [String: (params: Set, acpmux: Set)] = [ "_acpmux/attach": (["notAParam"], []), @@ -57,7 +66,7 @@ class CheckTests(unittest.TestCase): def tree(self, *, client: str = "", extra_request: str = "", extra_param: str = "", allow: str = "", - initialize: str = "initialize") -> Path: + initialize: str = "initialize", peers_param: str = "", extra_meta: str = "", pane_extra: str = "") -> Path: root = Path(tempfile.mkdtemp()) (root / check.PROTOCOL).parent.mkdir(parents=True) (root / check.PROTOCOL).write_text(json.dumps(PROTOCOL)) @@ -65,7 +74,10 @@ def tree(self, *, client: str = "", extra_request: str = "", extra_param: str = pane.mkdir(parents=True) (pane / "AcpmuxPaneMethods.swift").write_text( PANE.replace("{extra_request}", extra_request).replace("{initialize}", initialize)) - (pane / "AcpmuxPaneMethods+Params.swift").write_text(PARAMS.replace("{extra_param}", extra_param)) + (pane / "AcpmuxPaneMethods+Params.swift").write_text( + PARAMS.replace("{extra_param}", extra_param).replace("{peers_param}", peers_param) + .replace("{extra_meta}", extra_meta)) + (pane / "AgentPaneTransport.swift").write_text(pane_extra) (root / check.SOURCES / "Client.swift").write_text(client) (root / check.ALLOWLIST).parent.mkdir(parents=True) (root / check.ALLOWLIST).write_text(allow) @@ -104,6 +116,49 @@ def test_multiline_and_raw_strings(self): problems = check.check(self.tree(client=client)) self.assertEqual(sorted(p.split("'")[1] for p in problems), ["_acpmux/attachh", "_acpmux/rawdrift"], problems) + def test_a_closed_empty_schema_takes_no_param(self): + problems = check.check(self.tree(peers_param='"sessionId"')) + self.assertEqual(problems, [ + "AcpmuxPaneMethods.knownParams[_acpmux/peers]: '_acpmux/peers#sessionId' is not in " + f"{check.PROTOCOL}" + ]) + + def test_raw_and_multiline_strings_in_the_method_list(self): + # A raw or multiline literal in `requests` is read as its value, and a `]` inside a string + # does not end the list early. + problems = check.check(self.tree( + extra_request='#"_acpmux/rawdrift"#, """\n _acpmux/multidrift\n """, "a]b"')) + names = sorted(p.split("'")[1] for p in problems) + self.assertEqual(names, ["_acpmux/multidrift", "_acpmux/rawdrift", "a]b"], problems) + + def test_raw_strings_in_known_params(self): + problems = check.check(self.tree(extra_param=', #"promptID"#')) + self.assertEqual(len(problems), 1, problems) + self.assertIn("'_acpmux/attach#promptID'", problems[0]) + + def test_meta_acpmux_keys_are_checked(self): + problems = check.check(self.tree( + extra_meta='"promptId", "promptID"', + pane_extra=''' + acpmux["localAppToken"] = token + acpmux["steerOnly"] = true + if acpmux["model"] == nil {} + setAcpmuxMeta("approverDecisions", signed) + setAcpmuxMeta(["decisionNonce": nonce, field: proof]) + func setDecision(nonce: String, proof: some Sendable, field: String = "personDecision") {} + box.setDecision(nonce: n, proof: d, field: "approverDecision") + ''')) + names = sorted(p.split("'")[1] for p in problems) + self.assertEqual(names, ["_meta.acpmux#approverDecision", "_meta.acpmux#approverDecisions", + "_meta.acpmux#promptID", "_meta.acpmux#steerOnly"], problems) + + def test_a_missing_meta_type_fails(self): + root = self.tree() + spec = json.loads((root / check.PROTOCOL).read_text()) + del spec["types"]["AcpmuxRequestMeta"] + (root / check.PROTOCOL).write_text(json.dumps(spec)) + self.assertEqual(len(check.check(root)), 1) + def test_a_listed_name_is_allowed_and_a_stale_one_fails(self): allow = "_acpmux/attachh not registered yet (owner: acpmux)\n_acpmux/gone stale\nsession/prompt now defined\n" problems = check.check(self.tree(client='"_acpmux/attachh"\n', allow=allow)) diff --git a/webviews/src/agent-session/acpmux/agentBridgeContract.test.ts b/webviews/src/agent-session/acpmux/agentBridgeContract.test.ts index 4729f7d93136..eba5082ef686 100644 --- a/webviews/src/agent-session/acpmux/agentBridgeContract.test.ts +++ b/webviews/src/agent-session/acpmux/agentBridgeContract.test.ts @@ -160,6 +160,36 @@ function sentMethods(files: { path: string; text: string }[]): Sent[] { }); }); } + // A sender handed over as an object's property (`run({ send: postNative })`, `run({ callNative })`): the + // function's parameter at that index, destructured (`{ send }`, `{ send: post }`) or read (`opts.send(...)`), + // is a sender too. Keyed by function name, argument index and property name. + const passedProps = new Map>>(); + for (const { source } of sources) { + walk(source, (node) => { + if (!ts.isCallExpression(node)) return; + const callee = calleeName(node); + if (!callee) return; + node.arguments.forEach((arg, index) => { + if (!ts.isObjectLiteralExpression(arg)) return; + for (const property of arg.properties) { + const value = ts.isPropertyAssignment(property) + ? property.initializer + : ts.isShorthandPropertyAssignment(property) + ? property.name + : undefined; + const key = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) + ? property.name.text + : undefined; + if (!key || !value || !ts.isIdentifier(value) || !senders.has(value.text)) continue; + const byIndex = passedProps.get(callee) ?? new Map>(); + const keys = byIndex.get(index) ?? new Set(); + keys.add(key); + byIndex.set(index, keys); + passedProps.set(callee, byIndex); + } + }); + }); + } for (const { path, source } of sources) { walk(source, (node) => { let name: string | undefined; @@ -178,17 +208,61 @@ function sentMethods(files: { path: string; text: string }[]): Sent[] { } if (!name || !fn) return; const callbacks = new Map(); + // `opts` -> the property names that hold a sender (`opts.send(...)`). + const objectParams = new Map>(); fn.parameters.forEach((parameter, index) => { + const props = passedProps.get(name!)?.get(index); + if (ts.isObjectBindingPattern(parameter.name)) { + for (const element of parameter.name.elements) { + if (!ts.isIdentifier(element.name)) continue; + const key = element.propertyName && ts.isIdentifier(element.propertyName) + ? element.propertyName.text + : element.name.text; + const initial = element.initializer; + if (initial && ts.isIdentifier(initial) && senders.has(initial.text)) + callbacks.set(element.name.text, senders.get(initial.text)!); + else if (props?.has(key)) callbacks.set(element.name.text, true); + } + return; + } if (!ts.isIdentifier(parameter.name)) return; const initial = parameter.initializer; if (initial && ts.isIdentifier(initial) && senders.has(initial.text)) callbacks.set(parameter.name.text, senders.get(initial.text)!); else if (passed.get(name!)?.has(index)) callbacks.set(parameter.name.text, true); + else if (props) objectParams.set(parameter.name.text, props); }); - if (callbacks.size === 0) return; + if (callbacks.size === 0 && objectParams.size === 0) return; walk(fn, (inner) => { + // `const { send } = opts` destructures a sender out of an object parameter. + if ( + ts.isVariableDeclaration(inner) && + ts.isObjectBindingPattern(inner.name) && + inner.initializer && + ts.isIdentifier(inner.initializer) && + objectParams.has(inner.initializer.text) + ) { + const props = objectParams.get(inner.initializer.text)!; + for (const element of inner.name.elements) { + if (!ts.isIdentifier(element.name)) continue; + const key = element.propertyName && ts.isIdentifier(element.propertyName) + ? element.propertyName.text + : element.name.text; + if (props.has(key)) callbacks.set(element.name.text, true); + } + } if (!ts.isCallExpression(inner)) return; - const callee = calleeName(inner); + let callee = calleeName(inner); + const target = inner.expression; + if ( + !callee && + ts.isPropertyAccessExpression(target) && + ts.isIdentifier(target.expression) && + objectParams.get(target.expression.text)?.has(target.name.text) + ) { + callee = `${target.expression.text}.${target.name.text}`; + callbacks.set(callee, true); + } const arg = inner.arguments[0]; if (!callee || !callbacks.has(callee) || !arg) return; if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) { @@ -344,6 +418,27 @@ describe("agent bridge contract (schemas/agent-bridge/contract.json)", () => { function daemonRead(daemon: (method: string, params: object) => Promise) { return daemon("_acpmux/info", {}); } + function viaObject(opts: { send: (method: string, params: object) => Promise }) { + return opts.send("run.objct", {}); + } + viaObject({ send: postNative }); + function viaDestructure({ post }: { post: (method: string, params: object) => Promise }) { + return post("run.destructd", {}); + } + viaDestructure({ post: callNative }); + function viaRename({ send: s }: { send: (method: string, params: object) => Promise }) { + return s("run.renamd", {}); + } + viaRename({ send: postNative }); + function viaBody(deps: { postNative: (method: string, params: object) => Promise }) { + const { postNative: post } = deps; + return post("run.bdy", {}); + } + viaBody({ postNative }); + function daemonObject({ send }: { send: (method: string, params: object) => Promise }) { + return send("_acpmux/info", {}); + } + daemonObject({ send: daemonSend }); const H = "cmuxNope"; window.webkit?.messageHandlers?.[H]?.postMessage({}); `, @@ -354,6 +449,10 @@ describe("agent bridge contract (schemas/agent-bridge/contract.json)", () => { expect.stringContaining("link.inspect takes"), "webviews/src/agent-session/acpmux/fake.ts:11: the app has no method 'run.atach'", "webviews/src/agent-session/acpmux/fake.ts:14: the app has no method 'pane.setRemoteContrl'", + expect.stringContaining("the app has no method 'run.objct'"), + expect.stringContaining("the app has no method 'run.destructd'"), + expect.stringContaining("the app has no method 'run.renamd'"), + expect.stringContaining("the app has no method 'run.bdy'"), ]); expect([...handlerNames([file]).keys()]).toEqual(["cmuxNope"]); }); From c1915531273110a65a16b17d0d95f5164176045f Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 5 Nov 2026 12:25:35 -0800 Subject: [PATCH 2/8] agent bridge contract: list git.capabilities as unserved (found by the object-property scan) Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --- schemas/agent-bridge/contract.json | 1 + 1 file changed, 1 insertion(+) diff --git a/schemas/agent-bridge/contract.json b/schemas/agent-bridge/contract.json index a1958b88b62a..5cbb605b80e0 100644 --- a/schemas/agent-bridge/contract.json +++ b/schemas/agent-bridge/contract.json @@ -288,6 +288,7 @@ "shell.complete": "shell mode's Tab on the old bridge; no page sends it now." }, "unserved": { + "git.capabilities": "checkpoints/controller.tsx asks whether the app takes checkpoints; no app serves it yet, so the request is refused and the checkpoint review stays off. Remove this entry when the app serves it (the Swift test fails once AgentPageOps has it).", "models.catalog": "modelCatalogHost.ts asks for the app's model catalog; no app serves it yet, so the page keeps the bundled snapshot. Remove this entry when the app serves it (the Swift test fails once AgentPageOps has it)." }, "handlers": { From 4ea31acb28eab684c640e66cbd0d83709d1750c6 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 5 Nov 2026 12:30:28 -0800 Subject: [PATCH 3/8] cli-action-requests fixture: open-remote sends focus through --focus Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --- cmux-tui/spec/fixtures/cli-action-requests.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmux-tui/spec/fixtures/cli-action-requests.json b/cmux-tui/spec/fixtures/cli-action-requests.json index 792075d4f49d..2129f2c9027b 100644 --- a/cmux-tui/spec/fixtures/cli-action-requests.json +++ b/cmux-tui/spec/fixtures/cli-action-requests.json @@ -2836,7 +2836,7 @@ "attach": "/tmp/contract", "cmd": "/tmp/contract", "cwd": "/tmp/contract", - "focus": "true", + "focus": true, "host": "/tmp/contract", "label": "/tmp/contract", "name": "/tmp/contract", @@ -2844,9 +2844,10 @@ "split": "/tmp/contract" }, "cli": true, + "focus": true, "wait": true }, - "source": "cmux open-remote --host /tmp/contract --split /tmp/contract --cwd /tmp/contract --cmd /tmp/contract --name /tmp/contract --label /tmp/contract --attach /tmp/contract --ratio /tmp/contract --arg focus=true" + "source": "cmux open-remote --host /tmp/contract --split /tmp/contract --cwd /tmp/contract --cmd /tmp/contract --name /tmp/contract --label /tmp/contract --attach /tmp/contract --ratio /tmp/contract --focus" }, { "params": { From a3efce065b7eb7fe3827f52005481e89ae268766 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 5 Nov 2026 12:37:55 -0800 Subject: [PATCH 4/8] agentBridgeContract.test.ts: vp fmt Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --- .../acpmux/agentBridgeContract.test.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/webviews/src/agent-session/acpmux/agentBridgeContract.test.ts b/webviews/src/agent-session/acpmux/agentBridgeContract.test.ts index eba5082ef686..94120e62cb91 100644 --- a/webviews/src/agent-session/acpmux/agentBridgeContract.test.ts +++ b/webviews/src/agent-session/acpmux/agentBridgeContract.test.ts @@ -177,9 +177,10 @@ function sentMethods(files: { path: string; text: string }[]): Sent[] { : ts.isShorthandPropertyAssignment(property) ? property.name : undefined; - const key = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) - ? property.name.text - : undefined; + const key = + property.name && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) + ? property.name.text + : undefined; if (!key || !value || !ts.isIdentifier(value) || !senders.has(value.text)) continue; const byIndex = passedProps.get(callee) ?? new Map>(); const keys = byIndex.get(index) ?? new Set(); @@ -215,9 +216,10 @@ function sentMethods(files: { path: string; text: string }[]): Sent[] { if (ts.isObjectBindingPattern(parameter.name)) { for (const element of parameter.name.elements) { if (!ts.isIdentifier(element.name)) continue; - const key = element.propertyName && ts.isIdentifier(element.propertyName) - ? element.propertyName.text - : element.name.text; + const key = + element.propertyName && ts.isIdentifier(element.propertyName) + ? element.propertyName.text + : element.name.text; const initial = element.initializer; if (initial && ts.isIdentifier(initial) && senders.has(initial.text)) callbacks.set(element.name.text, senders.get(initial.text)!); @@ -245,9 +247,10 @@ function sentMethods(files: { path: string; text: string }[]): Sent[] { const props = objectParams.get(inner.initializer.text)!; for (const element of inner.name.elements) { if (!ts.isIdentifier(element.name)) continue; - const key = element.propertyName && ts.isIdentifier(element.propertyName) - ? element.propertyName.text - : element.name.text; + const key = + element.propertyName && ts.isIdentifier(element.propertyName) + ? element.propertyName.text + : element.name.text; if (props.has(key)) callbacks.set(element.name.text, true); } } From 4fb307fb910dc7017b000633555a1bba1a5cc586 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 5 Nov 2026 13:18:44 -0800 Subject: [PATCH 5/8] acpmux protocol: closed params for every method the app sends, and AcpmuxRequestMeta Each of the 63 untyped methods in the pane's knownParams now has a closed params schema (protocol/params.rs) of the keys acpmux's handler reads, plus `_meta`. ACP standard methods carry the spec's top-level keys. ForkParams and SessionAuthParams close schema-only (no serde change). Every request's `_meta.acpmux` names AcpmuxRequestMeta, the keys acpmux reads there, so it lands in the IR's types. IR and web types regenerated. Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --- .../crates/acpmux/spec/acpmux-protocol.json | 1082 +++++++++++++++-- cmux-tui/crates/acpmux/src/protocol/auth.rs | 1 + cmux-tui/crates/acpmux/src/protocol/mod.rs | 141 ++- cmux-tui/crates/acpmux/src/protocol/params.rs | 276 +++++ .../crates/acpmux/src/protocol/session.rs | 1 + cmux-tui/crates/acpmux/src/protocol/tests.rs | 17 + .../acpmux/protocol/generated/methods.ts | 254 ++-- .../acpmux/protocol/generated/types.ts | 510 +++++++- 8 files changed, 2017 insertions(+), 265 deletions(-) create mode 100644 cmux-tui/crates/acpmux/src/protocol/params.rs diff --git a/cmux-tui/crates/acpmux/spec/acpmux-protocol.json b/cmux-tui/crates/acpmux/spec/acpmux-protocol.json index 809ddfca372b..9dee51800e63 100644 --- a/cmux-tui/crates/acpmux/spec/acpmux-protocol.json +++ b/cmux-tui/crates/acpmux/spec/acpmux-protocol.json @@ -4,7 +4,9 @@ "acp": true, "kind": "request", "name": "initialize", - "params": true, + "params": { + "$ref": "#/types/InitializeParams" + }, "result": true }, { @@ -18,7 +20,9 @@ "acp": true, "kind": "request", "name": "session/new", - "params": true, + "params": { + "$ref": "#/types/SessionNewParams" + }, "result": true }, { @@ -67,34 +71,44 @@ "acp": true, "kind": "request", "name": "session/prompt", - "params": true, + "params": { + "$ref": "#/types/SessionPromptParams" + }, "result": true }, { "acp": true, "kind": "client_notification", "name": "session/cancel", - "params": true + "params": { + "$ref": "#/types/SessionCancelParams" + } }, { "acp": true, "kind": "request", "name": "session/set_mode", - "params": true, + "params": { + "$ref": "#/types/SessionSetModeParams" + }, "result": true }, { "acp": true, "kind": "request", "name": "session/set_model", - "params": true, + "params": { + "$ref": "#/types/SessionSetModelParams" + }, "result": true }, { "acp": true, "kind": "request", "name": "session/set_config_option", - "params": true, + "params": { + "$ref": "#/types/SessionSetConfigOptionParams" + }, "result": true }, { @@ -127,13 +141,17 @@ { "kind": "request", "name": "acp.trust.get", - "params": true, + "params": { + "$ref": "#/types/TrustGetParams" + }, "result": true }, { "kind": "request", "name": "acp.trust.set", - "params": true, + "params": { + "$ref": "#/types/TrustSetParams" + }, "result": true }, { @@ -180,7 +198,9 @@ { "kind": "request", "name": "_acpmux/status", - "params": true, + "params": { + "$ref": "#/types/StatusParams" + }, "result": true }, { @@ -310,7 +330,9 @@ { "kind": "request", "name": "_acpmux/offer_respond", - "params": true, + "params": { + "$ref": "#/types/OfferRespondParams" + }, "result": true }, { @@ -328,7 +350,9 @@ { "kind": "request", "name": "_acpmux/subsession/limits", - "params": true, + "params": { + "$ref": "#/types/SubsessionLimitsParams" + }, "result": true }, { @@ -376,13 +400,17 @@ { "kind": "request", "name": "_acpmux/forum/rules", - "params": true, + "params": { + "$ref": "#/types/ForumRulesParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/set_tools", - "params": true, + "params": { + "$ref": "#/types/SetToolsParams" + }, "result": true }, { @@ -394,25 +422,33 @@ { "kind": "request", "name": "_acpmux/mcp/reconnect", - "params": true, + "params": { + "$ref": "#/types/McpReconnectParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/subagent_stop", - "params": true, + "params": { + "$ref": "#/types/SubagentStopParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/subagent_transcript", - "params": true, + "params": { + "$ref": "#/types/SubagentTranscriptParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/native_history", - "params": true, + "params": { + "$ref": "#/types/NativeHistoryParams" + }, "result": true }, { @@ -424,7 +460,9 @@ { "kind": "request", "name": "_acpmux/app_reply", - "params": true, + "params": { + "$ref": "#/types/AppReplyParams" + }, "result": true }, { @@ -454,13 +492,17 @@ { "kind": "request", "name": "_acpmux/bookmark_get", - "params": true, + "params": { + "$ref": "#/types/BookmarkGetParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/bookmark_set", - "params": true, + "params": { + "$ref": "#/types/BookmarkSetParams" + }, "result": true }, { @@ -508,13 +550,17 @@ { "kind": "request", "name": "_acpmux/agent_pair_resume", - "params": true, + "params": { + "$ref": "#/types/AgentPairResumeParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/agent_hold", - "params": true, + "params": { + "$ref": "#/types/AgentHoldParams" + }, "result": true }, { @@ -540,19 +586,25 @@ { "kind": "request", "name": "_acpmux/process/list", - "params": true, + "params": { + "$ref": "#/types/ProcessListParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/process/tail", - "params": true, + "params": { + "$ref": "#/types/ProcessTailParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/process/kill", - "params": true, + "params": { + "$ref": "#/types/ProcessKillParams" + }, "result": true }, { @@ -639,7 +691,9 @@ { "kind": "request", "name": "_acpmux/kill", - "params": true, + "params": { + "$ref": "#/types/KillParams" + }, "result": true }, { @@ -651,25 +705,33 @@ { "kind": "request", "name": "_acpmux/watch_take_over", - "params": true, + "params": { + "$ref": "#/types/WatchTakeOverParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/archive", - "params": true, + "params": { + "$ref": "#/types/ArchiveParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/side", - "params": true, + "params": { + "$ref": "#/types/SideParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/btw", - "params": true, + "params": { + "$ref": "#/types/BtwParams" + }, "result": true }, { @@ -709,7 +771,9 @@ { "kind": "request", "name": "_acpmux/attachment", - "params": true, + "params": { + "$ref": "#/types/AttachmentParams" + }, "result": true }, { @@ -827,43 +891,57 @@ { "kind": "request", "name": "_acpmux/warm", - "params": true, + "params": { + "$ref": "#/types/WarmParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/prewarm", - "params": true, + "params": { + "$ref": "#/types/PrewarmParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/start_mode", - "params": true, + "params": { + "$ref": "#/types/StartModeParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/queue_remove", - "params": true, + "params": { + "$ref": "#/types/QueueRemoveParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/queue_edit", - "params": true, + "params": { + "$ref": "#/types/QueueEditParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/queue_approve", - "params": true, + "params": { + "$ref": "#/types/QueueApproveParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/queue_decline", - "params": true, + "params": { + "$ref": "#/types/QueueDeclineParams" + }, "result": true }, { @@ -875,19 +953,25 @@ { "kind": "request", "name": "_acpmux/queue_get", - "params": true, + "params": { + "$ref": "#/types/QueueGetParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/draft_get", - "params": true, + "params": { + "$ref": "#/types/DraftGetParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/draft_set", - "params": true, + "params": { + "$ref": "#/types/DraftSetParams" + }, "result": true }, { @@ -995,7 +1079,9 @@ { "kind": "request", "name": "_acpmux/resolve", - "params": true, + "params": { + "$ref": "#/types/ResolveParams" + }, "result": true }, { @@ -1089,7 +1175,9 @@ { "kind": "request", "name": "_acpmux/inbox_thread", - "params": true, + "params": { + "$ref": "#/types/InboxThreadParams" + }, "result": true }, { @@ -1167,7 +1255,9 @@ { "kind": "request", "name": "_acpmux/secret_submit", - "params": true, + "params": { + "$ref": "#/types/SecretSubmitParams" + }, "result": true }, { @@ -1332,43 +1422,57 @@ { "kind": "request", "name": "_acpmux/handoff_prepare", - "params": true, + "params": { + "$ref": "#/types/HandoffPrepareParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/handoff_get", - "params": true, + "params": { + "$ref": "#/types/HandoffGetParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/handoff_draft", - "params": true, + "params": { + "$ref": "#/types/HandoffDraftParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/handoff_start", - "params": true, + "params": { + "$ref": "#/types/HandoffStartParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/handoff_discard", - "params": true, + "params": { + "$ref": "#/types/HandoffDiscardParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/harnesses", - "params": true, + "params": { + "$ref": "#/types/HarnessesParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/harness_enable", - "params": true, + "params": { + "$ref": "#/types/HarnessEnableParams" + }, "result": true }, { @@ -1410,31 +1514,41 @@ { "kind": "request", "name": "_acpmux/harness/health", - "params": true, + "params": { + "$ref": "#/types/HarnessHealthParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/agent_login/start", - "params": true, + "params": { + "$ref": "#/types/AgentLoginStartParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/agent_login/finish", - "params": true, + "params": { + "$ref": "#/types/AgentLoginFinishParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/agent_login/cancel", - "params": true, + "params": { + "$ref": "#/types/AgentLoginCancelParams" + }, "result": true }, { "kind": "request", "name": "_acpmux/agent_login/api_key", - "params": true, + "params": { + "$ref": "#/types/AgentLoginApiKeyParams" + }, "result": true }, { @@ -1502,7 +1616,9 @@ { "kind": "request", "name": "_acpmux/models", - "params": true, + "params": { + "$ref": "#/types/ModelsParams" + }, "result": true }, { @@ -1514,7 +1630,9 @@ { "kind": "request", "name": "_acpmux/route/list", - "params": true, + "params": { + "$ref": "#/types/RouteListParams" + }, "result": true }, { @@ -1562,7 +1680,9 @@ { "kind": "request", "name": "_acpmux/chat/route.set", - "params": true, + "params": { + "$ref": "#/types/ChatRouteSetParams" + }, "result": true }, { @@ -1624,7 +1744,9 @@ { "kind": "request", "name": "_acpmux/peers", - "params": true, + "params": { + "$ref": "#/types/PeersParams" + }, "result": true }, { @@ -1672,7 +1794,9 @@ { "kind": "request", "name": "_acpmux/peer_reconnect", - "params": true, + "params": { + "$ref": "#/types/PeerReconnectParams" + }, "result": true }, { @@ -1684,7 +1808,9 @@ { "kind": "request", "name": "_acpmux/peer_snapshot", - "params": true, + "params": { + "$ref": "#/types/PeerSnapshotParams" + }, "result": true }, { @@ -1850,6 +1976,148 @@ } ], "types": { + "AcpmuxRequestMeta": { + "additionalProperties": false, + "properties": { + "adopt": true, + "agentSessionId": true, + "answeredBy": true, + "approverDecision": true, + "approverDecisions": true, + "argv": true, + "author": true, + "beforePromptId": true, + "callerSession": true, + "cliHost": true, + "context": true, + "cwd": true, + "decisionNonce": true, + "dispatchSource": true, + "env": true, + "ephemeral": true, + "forceAdmit": true, + "forwardProjection": true, + "fromAgent": true, + "fromHarness": true, + "fromSessionId": true, + "githubBroker": true, + "harness": true, + "harnessFrom": true, + "host": true, + "inlineParts": true, + "interrupt": true, + "localAppToken": true, + "mcpServers": true, + "messageId": true, + "messageSender": true, + "mode": true, + "model": true, + "name": true, + "notice": true, + "observer": true, + "parent": true, + "peer": true, + "peerHops": true, + "personDecision": true, + "personKey": true, + "personStop": true, + "policy": true, + "pr": true, + "preset": true, + "promptFileText": true, + "promptId": true, + "promptSuggestion": true, + "provenance": true, + "reportTo": true, + "requestId": true, + "requireFullAccess": true, + "resend": true, + "route": true, + "spawnToolCallId": true, + "startLater": true, + "steer": true, + "steerNow": true, + "steerOnly": true, + "supersede": true, + "surface": true, + "teamMessage": true, + "throughSeq": true, + "via": true, + "vmBoundary": true, + "waitStart": true, + "workflowAllowedTools": true, + "workspaceId": true + }, + "type": "object" + }, + "AgentHoldParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "held": true, + "sessionId": true + }, + "type": "object" + }, + "AgentLoginApiKeyParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "agent": true, + "key": true, + "sessionId": true + }, + "type": "object" + }, + "AgentLoginCancelParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "loginId": true, + "sessionId": true + }, + "type": "object" + }, + "AgentLoginFinishParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "code": true, + "loginId": true, + "sessionId": true + }, + "type": "object" + }, + "AgentLoginStartParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "agent": true, + "sessionId": true + }, + "type": "object" + }, + "AgentPairResumeParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "peerSessionId": true, + "sessionId": true + }, + "type": "object" + }, "AllowanceExpiry": { "enum": [ "session_stop_or_daemon_restart" @@ -1929,6 +2197,21 @@ ], "type": "object" }, + "AppReplyParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "replyId": true, + "replyTo": true, + "sessionId": true, + "text": true, + "threadId": true, + "to": true + }, + "type": "object" + }, "ApproverDecision": { "additionalProperties": false, "description": "`{approver, expiresAt, signature}`: ECDSA P-256 / SHA-256, raw `r || s` as\nlowercase hex, over the approver's message for this exact answer.", @@ -1953,6 +2236,17 @@ ], "type": "object" }, + "ArchiveParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "archived": true, + "sessionId": true + }, + "type": "object" + }, "AttachParams": { "additionalProperties": false, "properties": { @@ -2084,6 +2378,17 @@ ], "type": "object" }, + "AttachmentParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "id": true, + "sessionId": true + }, + "type": "object" + }, "Author": { "additionalProperties": false, "description": "The person a request comes from (`hub/author.rs`): stamped by the daemon that admitted\nthem, and accepted only from the unix socket or a peer forwarding for its own.", @@ -2184,6 +2489,41 @@ } ] }, + "BookmarkGetParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "sessionId": true + }, + "type": "object" + }, + "BookmarkSetParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "bookmarked": true, + "seq": true, + "sessionId": true + }, + "type": "object" + }, + "BtwParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "inline": true, + "model": true, + "question": true, + "sessionId": true + }, + "type": "object" + }, "ChangedSession": { "anyOf": [ { @@ -2209,6 +2549,18 @@ ], "type": "object" }, + "ChatRouteSetParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "routeId": true, + "sessionId": true, + "when": true + }, + "type": "object" + }, "CoverageLabel": { "enum": [ "acp_requests_only" @@ -2251,6 +2603,27 @@ "description": "An acknowledgement with nothing in it.", "type": "object" }, + "DraftGetParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "sessionId": true + }, + "type": "object" + }, + "DraftSetParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "sessionId": true, + "text": true + }, + "type": "object" + }, "Enforcement": { "description": "What limits the session's agent as far as acpmux can tell.", "properties": { @@ -2478,6 +2851,7 @@ "type": "object" }, "ForkParams": { + "additionalProperties": false, "description": "`acp.session.fork`: a completed-turn, live, or before-prompt fork.\nBefore-prompt requests require requestId and omit both throughSeq and live.", "properties": { "_meta": true, @@ -2518,6 +2892,102 @@ ], "type": "object" }, + "ForumRulesParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + } + }, + "type": "object" + }, + "HandoffDiscardParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "handoffId": true + }, + "type": "object" + }, + "HandoffDraftParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "capsule": true, + "checkpoint": true, + "draftKey": true, + "handoffId": true, + "revision": true + }, + "type": "object" + }, + "HandoffGetParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "handoffId": true, + "sessionId": true + }, + "type": "object" + }, + "HandoffPrepareParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "handoffKey": true, + "harness": true, + "memoryRefs": true, + "sessionId": true + }, + "type": "object" + }, + "HandoffStartParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "capsule": true, + "checkpoint": true, + "handoffId": true, + "promptId": true, + "revision": true + }, + "type": "object" + }, + "HarnessEnableParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "folder": true, + "id": true + }, + "type": "object" + }, + "HarnessHealthParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "autoUpdate": true, + "checkLatest": true, + "refresh": true, + "rescan": true, + "view": true + }, + "type": "object" + }, "HarnessUpdatePlanParams": { "additionalProperties": false, "properties": { @@ -2530,6 +3000,28 @@ ], "type": "object" }, + "HarnessesParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "cwd": true + }, + "type": "object" + }, + "InboxThreadParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "markRead": true, + "sessionId": true, + "threadId": true + }, + "type": "object" + }, "Indicator": { "enum": [ "input", @@ -2540,6 +3032,18 @@ ], "type": "string" }, + "InitializeParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "clientCapabilities": true, + "clientInfo": true, + "protocolVersion": true + }, + "type": "object" + }, "InventoryRefreshParams": { "additionalProperties": false, "type": "object" @@ -2563,6 +3067,17 @@ ], "type": "string" }, + "KillParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "purge": true, + "sessionId": true + }, + "type": "object" + }, "Lane": { "description": "The session's lane (cx-v9k2f), decided once by the daemon (`views::lane`).", "properties": { @@ -2986,9 +3501,20 @@ ] } }, - "required": [ - "recipient" - ], + "required": [ + "recipient" + ], + "type": "object" + }, + "McpReconnectParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "server": true, + "sessionId": true + }, "type": "object" }, "McpServerStatus": { @@ -3091,6 +3617,42 @@ ], "type": "object" }, + "ModelsParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "probe": true, + "refresh": true + }, + "type": "object" + }, + "NativeHistoryParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "before": true, + "mark": true, + "sessionId": true + }, + "type": "object" + }, + "OfferRespondParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "childSessionId": true, + "offerId": true, + "outcome": true, + "sessionId": true + }, + "type": "object" + }, "Participant": { "description": "One person in a chat. The hub never marks the caller's own entry: a connection may carry\nseveral people, so the client compares ids (a caller with no author is `owner`).", "properties": { @@ -3152,6 +3714,31 @@ } ] }, + "PeerReconnectParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "force": true, + "name": true, + "wait": true + }, + "type": "object" + }, + "PeerSnapshotParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "host": true, + "limit": true, + "maxBytes": true, + "sessionId": true + }, + "type": "object" + }, "PeerStateChanged": { "description": "`_acpmux/peer_state_changed`, to every watching connection (`_acpmux/watch`) when a peer's\nstate changes, never twice for the same state: a peer that is added sends its first state,\nand one that is removed a last `offline` with `error: \"removed\"`. Listed as the\n`peerStateEvents` feature.", "properties": { @@ -3194,6 +3781,15 @@ ], "type": "object" }, + "PeersParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + } + }, + "type": "object" + }, "PermissionBatching": { "properties": { "maxItems": { @@ -3419,7 +4015,7 @@ "_meta": { "anyOf": [ { - "$ref": "#/types/RequestMeta" + "$ref": "#/types/RequestMeta2" }, { "type": "null" @@ -3554,7 +4150,7 @@ "_meta": { "anyOf": [ { - "$ref": "#/types/RequestMeta" + "$ref": "#/types/RequestMeta2" }, { "type": "null" @@ -3610,6 +4206,18 @@ ], "type": "object" }, + "PrewarmParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "cwd": true, + "harness": true, + "release": true + }, + "type": "object" + }, "ProcessBackgroundParams": { "additionalProperties": false, "description": "`_acpmux/process/background` (Ctrl-B): the chat whose running work goes\nto the background, and the one tool call to move (none: every\nforeground task, as Claude Code's own Ctrl-B does).", @@ -3643,6 +4251,43 @@ ], "type": "object" }, + "ProcessKillParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "processId": true, + "sessionId": true, + "signal": true + }, + "type": "object" + }, + "ProcessListParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "cursor": true, + "limit": true, + "sessionId": true + }, + "type": "object" + }, + "ProcessTailParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "fromByte": true, + "maxBytes": true, + "processId": true, + "sessionId": true + }, + "type": "object" + }, "PromptClass": { "description": "What another agent's message asks of the session it goes to.", "oneOf": [ @@ -3685,6 +4330,67 @@ ], "type": "object" }, + "QueueApproveParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "promptId": true, + "revision": true, + "sessionId": true, + "turnId": true + }, + "type": "object" + }, + "QueueDeclineParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "promptId": true, + "sessionId": true + }, + "type": "object" + }, + "QueueEditParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "images": true, + "index": true, + "op": true, + "promptId": true, + "sessionId": true, + "text": true + }, + "type": "object" + }, + "QueueGetParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "promptId": true, + "sessionId": true + }, + "type": "object" + }, + "QueueRemoveParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "promptId": true, + "sessionId": true + }, + "type": "object" + }, "QueuedAuthor": { "description": "The person who queued a prompt in a shared chat.", "properties": { @@ -3886,6 +4592,14 @@ "type": "object" }, "RequestMeta": { + "properties": { + "acpmux": { + "$ref": "#/types/AcpmuxRequestMeta" + } + }, + "type": "object" + }, + "RequestMeta2": { "additionalProperties": false, "properties": { "acpmux": { @@ -3908,6 +4622,18 @@ }, "type": "object" }, + "ResolveParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "exactSessionId": true, + "query": true, + "sessionId": true + }, + "type": "object" + }, "RetryKind": { "description": "What `_acpmux/retry` retried: the chat's own turn, or one of its subagents.", "enum": [ @@ -4006,6 +4732,28 @@ ], "type": "object" }, + "RouteListParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "sessionId": true + }, + "type": "object" + }, + "SecretSubmitParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "secretId": true, + "sessionId": true, + "value": true + }, + "type": "object" + }, "SessionAuthLogin": { "description": "`terminal`: run `command` in a terminal on `host` (null: this machine). `route`: edit or\nsign in again to the route `routeId`.", "properties": { @@ -4044,6 +4792,7 @@ "type": "string" }, "SessionAuthParams": { + "additionalProperties": false, "description": "A chat by `sessionId`, or by `target` before it has a session (a new tab before its first\nsend). Exactly one of the two. `refresh`: probe the route and run the harness's own sign-in\ncheck that reads no keychain, when no newer evidence answers.", "properties": { "refresh": { @@ -4188,6 +4937,16 @@ ], "type": "object" }, + "SessionCancelParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "sessionId": true + }, + "type": "object" + }, "SessionChanged": { "description": "To watchers when a session changed: its new summary (only its id when\npurged), the coarse `kind` and the log record behind it.", "properties": { @@ -4775,6 +5534,30 @@ ], "type": "object" }, + "SessionNewParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "additionalDirectories": true, + "cwd": true, + "mcpServers": true + }, + "type": "object" + }, + "SessionPromptParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "prompt": true, + "sessionId": true, + "steer": true + }, + "type": "object" + }, "SessionRef": { "additionalProperties": false, "properties": { @@ -4787,6 +5570,40 @@ ], "type": "object" }, + "SessionSetConfigOptionParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "configId": true, + "sessionId": true, + "value": true + }, + "type": "object" + }, + "SessionSetModeParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "modeId": true, + "sessionId": true + }, + "type": "object" + }, + "SessionSetModelParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "modelId": true, + "sessionId": true + }, + "type": "object" + }, "SessionSummary": { "description": "One session as every list shows it (`Hub::session_summary`). A peer's\nsession arrives as the peer daemon's own summary plus `peer`, with\n`status` `unreachable` while the peer is gone; a peer on an older\nacpmux may lack newer fields, so nullable ones are optional here.", "properties": { @@ -5410,6 +6227,50 @@ ], "type": "object" }, + "SetToolsParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "enabled": true, + "id": true, + "sessionId": true + }, + "type": "object" + }, + "SideParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "chat": true, + "sessionId": true + }, + "type": "object" + }, + "StartModeParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "cwd": true, + "harness": true, + "preset": true + }, + "type": "object" + }, + "StatusParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + } + }, + "type": "object" + }, "StuckReason": { "enum": [ "harness_unavailable", @@ -5419,6 +6280,44 @@ ], "type": "string" }, + "SubagentStopParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "sessionId": true, + "subagentSessionId": true + }, + "type": "object" + }, + "SubagentTranscriptParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "after": true, + "agentId": true, + "before": true, + "limit": true, + "runId": true, + "sessionId": true + }, + "type": "object" + }, + "SubsessionLimitsParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "maxRunning": true, + "sessionId": true, + "workspaceId": true + }, + "type": "object" + }, "ToolSummary": { "description": "One row of a chat's tool list (`store::ToolRow`).", "properties": { @@ -5455,6 +6354,31 @@ ], "type": "object" }, + "TrustGetParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "cwd": true, + "peer": true, + "sessionId": true + }, + "type": "object" + }, + "TrustSetParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "cwd": true, + "level": true, + "peer": true, + "sessionId": true + }, + "type": "object" + }, "TurnSummary": { "description": "The running turn.", "properties": { @@ -5717,6 +6641,17 @@ ], "type": "object" }, + "WarmParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "limit": true, + "sessionIds": true + }, + "type": "object" + }, "WatchParams": { "additionalProperties": false, "properties": { @@ -5760,6 +6695,17 @@ } }, "type": "object" + }, + "WatchTakeOverParams": { + "additionalProperties": false, + "properties": { + "_meta": { + "$ref": "#/types/RequestMeta" + }, + "ifLive": true, + "sessionId": true + }, + "type": "object" } }, "version": "1" diff --git a/cmux-tui/crates/acpmux/src/protocol/auth.rs b/cmux-tui/crates/acpmux/src/protocol/auth.rs index 1c3a80fb0481..8a413ea04c4a 100644 --- a/cmux-tui/crates/acpmux/src/protocol/auth.rs +++ b/cmux-tui/crates/acpmux/src/protocol/auth.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; /// check that reads no keychain, when no newer evidence answers. #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] +#[schemars(deny_unknown_fields)] pub struct SessionAuthParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub session_id: Option, diff --git a/cmux-tui/crates/acpmux/src/protocol/mod.rs b/cmux-tui/crates/acpmux/src/protocol/mod.rs index e060d355de37..b7d4150ea5ab 100644 --- a/cmux-tui/crates/acpmux/src/protocol/mod.rs +++ b/cmux-tui/crates/acpmux/src/protocol/mod.rs @@ -13,6 +13,7 @@ pub mod auth; pub mod events; pub mod inventory; pub mod mailbox; +pub mod params; pub mod peer_state; pub mod permission; pub mod presence; @@ -137,6 +138,17 @@ fn notification(name: &'static str) -> MethodDecl { } } +/// A method whose params are the closed key set `P` (`params.rs`) and whose result is not +/// declared yet. +fn keyed(name: &'static str, kind: Kind) -> MethodDecl { + MethodDecl { name, kind, acp: false, params: Some(schema_of::

), result: None } +} + +/// [`keyed`] for a standard ACP method. +fn acp_keyed(name: &'static str, kind: Kind) -> MethodDecl { + MethodDecl { name, kind, acp: true, params: Some(schema_of::

), result: None } +} + /// Every method and notification acpmux serves or sends, in wire-name order /// within each group. pub fn registry() -> Vec { @@ -147,27 +159,30 @@ pub fn registry() -> Vec { use session::*; vec![ // Standard ACP. - acp(method::INITIALIZE, Request), + acp_keyed::(method::INITIALIZE, Request), acp(method::AUTHENTICATE, Request), - acp(method::SESSION_NEW, Request), + acp_keyed::(method::SESSION_NEW, Request), acp(method::SESSION_LOAD, Request), acp(method::SESSION_RESUME, Request), acp(method::SESSION_LIST, Request), acp(method::SESSION_FORK, Request), acp(method::SESSION_CLOSE, Request), acp(method::SESSION_DELETE, Request), - acp(method::SESSION_PROMPT, Request), - acp(method::SESSION_CANCEL, ClientNotification), - acp(method::SESSION_SET_MODE, Request), - acp(method::SESSION_SET_MODEL, Request), - acp(method::SESSION_SET_CONFIG_OPTION, Request), + acp_keyed::(method::SESSION_PROMPT, Request), + acp_keyed::(method::SESSION_CANCEL, ClientNotification), + acp_keyed::(method::SESSION_SET_MODE, Request), + acp_keyed::(method::SESSION_SET_MODEL, Request), + acp_keyed::( + method::SESSION_SET_CONFIG_OPTION, + Request, + ), acp(method::SESSION_UPDATE, Notification), acp(method::SESSION_REQUEST_PERMISSION, ServerRequest), acp(method::CANCEL_REQUEST, ClientNotification), // ACP extensions acpmux names in the acp.* space. request::(method::ACP_SESSION_FORK), - untyped(method::ACP_TRUST_GET, Request), - untyped(method::ACP_TRUST_SET, Request), + keyed::(method::ACP_TRUST_GET, Request), + keyed::(method::ACP_TRUST_SET, Request), untyped("catalog.get", Request), untyped("catalog.refresh", Request), untyped("catalog.changed", Notification), @@ -177,7 +192,7 @@ pub fn registry() -> Vec { untyped(method::MUX_TAILNET_START, Request), untyped(method::MUX_TAILNET_STATE, Request), // Sessions. - untyped(method::MUX_STATUS, Request), + keyed::(method::MUX_STATUS, Request), untyped(method::MUX_CONTEXT, Request), untyped(method::MUX_CONTEXT_OVERRIDE, Request), // Cloud whole-machine turn fencing. These methods are served by the @@ -198,11 +213,11 @@ pub fn registry() -> Vec { request::(method::MUX_WATCH), untyped(method::MUX_RENAME, Request), untyped(method::MUX_OFFER_SESSION, Request), - untyped(method::MUX_OFFER_RESPOND, Request), + keyed::(method::MUX_OFFER_RESPOND, Request), // Sub-sessions (`server/subsessions.rs`). untyped(method::MUX_SUBSESSION_ANSWER, Request), untyped(method::MUX_SUBSESSION_GET, Request), - untyped(method::MUX_SUBSESSION_LIMITS, Request), + keyed::(method::MUX_SUBSESSION_LIMITS, Request), untyped(method::MUX_SUBSESSION_LIST, Request), untyped(method::MUX_SUBSESSION_RETRY, Request), untyped(method::MUX_SUBSESSION_SEND, Request), @@ -211,21 +226,21 @@ pub fn registry() -> Vec { // Forum announcements (`server/forum.rs`). untyped(method::MUX_FORUM_ACK, Request), untyped(method::MUX_FORUM_ANNOUNCE, Request), - untyped(method::MUX_FORUM_RULES, Request), - untyped(method::MUX_SET_TOOLS, Request), + keyed::(method::MUX_FORUM_RULES, Request), + keyed::(method::MUX_SET_TOOLS, Request), untyped(method::MUX_SET_DIRECTORIES, Request), - untyped(method::MUX_MCP_RECONNECT, Request), - untyped(method::MUX_SUBAGENT_STOP, Request), - untyped(method::MUX_SUBAGENT_TRANSCRIPT, Request), - untyped(method::MUX_NATIVE_HISTORY, Request), + keyed::(method::MUX_MCP_RECONNECT, Request), + keyed::(method::MUX_SUBAGENT_STOP, Request), + keyed::(method::MUX_SUBAGENT_TRANSCRIPT, Request), + keyed::(method::MUX_NATIVE_HISTORY, Request), untyped(method::MUX_WORKFLOWS, Request), - untyped(method::MUX_APP_REPLY, Request), + keyed::(method::MUX_APP_REPLY, Request), untyped(method::MUX_STATS, Request), untyped(method::MUX_WORKSPACE_PURGE, Request), untyped(method::MUX_GITHUB_ACTION, Request), untyped(method::MUX_GITHUB_APP_LEASE, Request), - untyped(method::MUX_BOOKMARK_GET, Request), - untyped(method::MUX_BOOKMARK_SET, Request), + keyed::(method::MUX_BOOKMARK_GET, Request), + keyed::(method::MUX_BOOKMARK_SET, Request), untyped(method::MUX_PROMPT_WITHDRAW, Request), untyped(method::MUX_INBOX_DIGEST, Request), untyped(method::MUX_SEND_QUEUE, Request), @@ -234,13 +249,13 @@ pub fn registry() -> Vec { // The hub asks a session's agent adapter (claude_stdio, the Codex bridge). untyped(method::MUX_STEP_NOW, Request), untyped(method::MUX_STEER_WITHDRAW, Request), - untyped(method::MUX_AGENT_PAIR_RESUME, Request), - untyped(method::MUX_AGENT_HOLD, Request), + keyed::(method::MUX_AGENT_PAIR_RESUME, Request), + keyed::(method::MUX_AGENT_HOLD, Request), request::(method::MUX_RETRY), request::(method::MUX_SESSION_AUTH), - untyped(method::MUX_PROCESS_LIST, Request), - untyped(method::MUX_PROCESS_TAIL, Request), - untyped(method::MUX_PROCESS_KILL, Request), + keyed::(method::MUX_PROCESS_LIST, Request), + keyed::(method::MUX_PROCESS_TAIL, Request), + keyed::(method::MUX_PROCESS_KILL, Request), untyped(method::MUX_BG_START, Request), untyped(method::MUX_BG_LIST, Request), untyped(method::MUX_BG_OUTPUT, Request), @@ -254,18 +269,18 @@ pub fn registry() -> Vec { untyped(method::MUX_WORKTREE_RESTORE, Request), untyped(method::MUX_RETENTION, Request), untyped(method::MUX_PROCESS_UPDATE, Notification), - untyped(method::MUX_KILL, Request), + keyed::(method::MUX_KILL, Request), untyped(method::MUX_KEEP, Request), - untyped(method::MUX_WATCH_TAKE_OVER, Request), - untyped(method::MUX_ARCHIVE, Request), - untyped(method::MUX_SIDE, Request), - untyped(method::MUX_BTW, Request), + keyed::(method::MUX_WATCH_TAKE_OVER, Request), + keyed::(method::MUX_ARCHIVE, Request), + keyed::(method::MUX_SIDE, Request), + keyed::(method::MUX_BTW, Request), untyped(method::MUX_TAG, Request), untyped(method::MUX_DETACH_PARENT, Request), untyped(method::MUX_INFO, Request), untyped(method::MUX_SESSION_FILES, Request), request::(method::MUX_EVENTS), - untyped(method::MUX_ATTACHMENT, Request), + keyed::(method::MUX_ATTACHMENT, Request), request::(method::MUX_MAIL_SEND), request::(method::MUX_MAIL_READ), request::(method::MUX_MAIL_WAIT), @@ -282,17 +297,17 @@ pub fn registry() -> Vec { untyped(method::MUX_WATCH_LS, Request), untyped(method::MUX_WATCH_RM, Request), untyped(method::MUX_ASK, Request), - untyped(method::MUX_WARM, Request), - untyped(method::MUX_PREWARM, Request), - untyped(method::MUX_START_MODE, Request), - untyped(method::MUX_QUEUE_REMOVE, Request), - untyped(method::MUX_QUEUE_EDIT, Request), - untyped(method::MUX_QUEUE_APPROVE, Request), - untyped(method::MUX_QUEUE_DECLINE, Request), + keyed::(method::MUX_WARM, Request), + keyed::(method::MUX_PREWARM, Request), + keyed::(method::MUX_START_MODE, Request), + keyed::(method::MUX_QUEUE_REMOVE, Request), + keyed::(method::MUX_QUEUE_EDIT, Request), + keyed::(method::MUX_QUEUE_APPROVE, Request), + keyed::(method::MUX_QUEUE_DECLINE, Request), untyped(method::MUX_QUEUE_TEXT, Request), - untyped(method::MUX_QUEUE_GET, Request), - untyped(method::MUX_DRAFT_GET, Request), - untyped(method::MUX_DRAFT_SET, Request), + keyed::(method::MUX_QUEUE_GET, Request), + keyed::(method::MUX_DRAFT_GET, Request), + keyed::(method::MUX_DRAFT_SET, Request), untyped(method::MUX_COMPOSER_SET, Request), untyped(method::MUX_COMPOSERS, Request), untyped(method::MUX_EXPORT, Request), @@ -310,7 +325,7 @@ pub fn registry() -> Vec { untyped(method::MUX_REPORT, Request), untyped(method::MUX_TASK_NEW, Request), untyped(method::MUX_TASKS, Request), - untyped(method::MUX_RESOLVE, Request), + keyed::(method::MUX_RESOLVE, Request), untyped(method::MUX_WHO_ELSE, Request), untyped(method::MUX_MEDIA_OPEN, Request), untyped(method::MUX_MEDIA_RANGE, Request), @@ -328,7 +343,7 @@ pub fn registry() -> Vec { untyped(method::MUX_SET_RULES, Request), untyped(method::MUX_SET_TURN_CAP, Request), untyped(method::MUX_INBOX_THREADS, Request), - untyped(method::MUX_INBOX_THREAD, Request), + keyed::(method::MUX_INBOX_THREAD, Request), untyped(method::MUX_INBOX_REPLY, Request), untyped(method::MUX_PERSON_ENROLL, Request), untyped(method::MUX_COMPUTER_USE_ENROLL, Request), @@ -343,7 +358,7 @@ pub fn registry() -> Vec { untyped(method::MUX_NEW_CHAT_PICKS_SET, Request), untyped(method::MUX_WORKSPACE_ROOTS_SET, Request), untyped(method::MUX_SECRET_REQUEST, Request), - untyped(method::MUX_SECRET_SUBMIT, Request), + keyed::(method::MUX_SECRET_SUBMIT, Request), untyped(method::MUX_SECRET_RUN, Request), untyped(method::MUX_MCP_CONNECT_REQUEST, Request), untyped(method::MUX_MCP_CONNECT_ANSWER, Request), @@ -375,25 +390,25 @@ pub fn registry() -> Vec { untyped("_acpmux/transfer_receive", Request), untyped("_acpmux/transfer_write", Request), // Handoff. - untyped(method::MUX_HANDOFF_PREPARE, Request), - untyped(method::MUX_HANDOFF_GET, Request), - untyped(method::MUX_HANDOFF_DRAFT, Request), - untyped(method::MUX_HANDOFF_START, Request), - untyped(method::MUX_HANDOFF_DISCARD, Request), + keyed::(method::MUX_HANDOFF_PREPARE, Request), + keyed::(method::MUX_HANDOFF_GET, Request), + keyed::(method::MUX_HANDOFF_DRAFT, Request), + keyed::(method::MUX_HANDOFF_START, Request), + keyed::(method::MUX_HANDOFF_DISCARD, Request), // Harnesses, models, routes. - untyped(method::MUX_HARNESSES, Request), - untyped(method::MUX_HARNESS_ENABLE, Request), + keyed::(method::MUX_HARNESSES, Request), + keyed::(method::MUX_HARNESS_ENABLE, Request), untyped(method::MUX_HARNESS_ADD, Request), untyped(method::MUX_HARNESS_REMOVE, Request), untyped(method::MUX_HARNESS_RESTORE, Request), untyped(method::MUX_HARNESS_DOCTOR, Request), untyped(method::MUX_HARNESS_SECRET_SET, Request), untyped(method::MUX_HARNESS_ICON, Request), - untyped(method::MUX_HARNESS_HEALTH, Request), - untyped(method::MUX_AGENT_LOGIN_START, Request), - untyped(method::MUX_AGENT_LOGIN_FINISH, Request), - untyped(method::MUX_AGENT_LOGIN_CANCEL, Request), - untyped(method::MUX_AGENT_LOGIN_API_KEY, Request), + keyed::(method::MUX_HARNESS_HEALTH, Request), + keyed::(method::MUX_AGENT_LOGIN_START, Request), + keyed::(method::MUX_AGENT_LOGIN_FINISH, Request), + keyed::(method::MUX_AGENT_LOGIN_CANCEL, Request), + keyed::(method::MUX_AGENT_LOGIN_API_KEY, Request), request::(method::MUX_HARNESS_INVENTORY_WATCH), request::(method::MUX_HARNESS_INVENTORY_REFRESH), request::(method::MUX_HARNESS_UPDATE_PLAN), @@ -403,9 +418,9 @@ pub fn registry() -> Vec { untyped(method::MUX_REGISTRY, Request), untyped(method::MUX_DEFAULTS, Request), untyped(method::MUX_PRESETS, Request), - untyped("_acpmux/models", Request), + keyed::("_acpmux/models", Request), untyped("_acpmux/directories", Request), - untyped(method::MUX_ROUTE_LIST, Request), + keyed::(method::MUX_ROUTE_LIST, Request), untyped(method::MUX_ROUTE_SHOW, Request), untyped(method::MUX_ROUTE_ADD, Request), untyped(method::MUX_ROUTE_EDIT, Request), @@ -413,7 +428,7 @@ pub fn registry() -> Vec { untyped(method::MUX_ROUTE_RESTORE, Request), untyped(method::MUX_ROUTE_TEST, Request), untyped(method::MUX_ROUTE_DEFAULT_SET, Request), - untyped(method::MUX_CHAT_ROUTE_SET, Request), + keyed::(method::MUX_CHAT_ROUTE_SET, Request), untyped(method::MUX_ACCOUNTS, Request), untyped(method::MUX_ACCOUNT_SET, Request), untyped(method::MUX_ACCOUNT_USAGE, Request), @@ -426,7 +441,7 @@ pub fn registry() -> Vec { // schema would describe itself. request::(method::MUX_PROTOCOL), untyped(method::MUX_SHUTDOWN, Request), - untyped("_acpmux/peers", Request), + keyed::("_acpmux/peers", Request), untyped("_acpmux/peer_status", Request), untyped("_acpmux/host_health", Request), untyped("_acpmux/vm_host", Request), @@ -434,9 +449,9 @@ pub fn registry() -> Vec { untyped("_acpmux/place", Request), untyped("_acpmux/peer_add", Request), untyped("_acpmux/peer_lease", Request), - untyped("_acpmux/peer_reconnect", Request), + keyed::("_acpmux/peer_reconnect", Request), untyped("_acpmux/peer_remove", Request), - untyped("_acpmux/peer_snapshot", Request), + keyed::("_acpmux/peer_snapshot", Request), untyped("_acpmux/peer_snapshots_clear", Request), untyped(method::MUX_PAIR_TICKET_NEW, Request), untyped(method::MUX_PAIR_LIST, Request), diff --git a/cmux-tui/crates/acpmux/src/protocol/params.rs b/cmux-tui/crates/acpmux/src/protocol/params.rs new file mode 100644 index 000000000000..5474b16d0c83 --- /dev/null +++ b/cmux-tui/crates/acpmux/src/protocol/params.rs @@ -0,0 +1,276 @@ +//! The params of the methods that take a fixed set of keys and no serde type: each a closed +//! object of the keys its handler reads, every value open (`true`). The Swift pane's own list +//! (`AcpmuxPaneMethods+Params.swift` `knownParams`) is checked against these +//! (`scripts/ci/check-swift-acpmux-protocol.py`), so a key the app sends that acpmux never reads +//! fails CI. Every request also takes `_meta` ([`RequestMeta`]): the local app's token and the +//! person's key ride on any of them (`server/local_app.rs`, `hub/person.rs`). + +use std::borrow::Cow; + +use schemars::generate::SchemaGenerator; +use schemars::{JsonSchema, Schema}; +use serde_json::{Map, Value, json}; + +/// A closed object of `keys` plus `_meta`, each value any JSON. +fn closed(generator: &mut SchemaGenerator, keys: &[&str]) -> Schema { + let mut properties: Map = + keys.iter().map(|key| ((*key).to_owned(), Value::Bool(true))).collect(); + properties.insert("_meta".into(), generator.subschema_for::().to_value()); + Schema::try_from( + json!({"type": "object", "properties": properties, "additionalProperties": false}), + ) + .unwrap_or_default() +} + +macro_rules! keyed { + ($($(#[$doc:meta])* $name:ident: [$($key:literal),* $(,)?];)*) => {$( + $(#[$doc])* + pub struct $name; + + impl JsonSchema for $name { + fn schema_name() -> Cow<'static, str> { + stringify!($name).into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + closed(generator, &[$($key),*]) + } + } + )*}; +} + +/// A request's `_meta`: open to other vendors' keys, with acpmux's own under `acpmux`. +pub struct RequestMeta; + +impl JsonSchema for RequestMeta { + fn schema_name() -> Cow<'static, str> { + "RequestMeta".into() + } + + fn json_schema(generator: &mut SchemaGenerator) -> Schema { + let acpmux = generator.subschema_for::().to_value(); + Schema::try_from(json!({"type": "object", "properties": {"acpmux": acpmux}})) + .unwrap_or_default() + } +} + +/// Every key acpmux reads from a request's `params._meta.acpmux`, on any method. Not here: the +/// keys of a reply's or a notification's `_meta.acpmux` (`features`, `ownerCloud`), and a content +/// block's own (`upload`, `uploadPath`, `context: true` on a block). +pub struct AcpmuxRequestMeta; + +impl JsonSchema for AcpmuxRequestMeta { + fn schema_name() -> Cow<'static, str> { + "AcpmuxRequestMeta".into() + } + + fn json_schema(_: &mut SchemaGenerator) -> Schema { + let properties: Map = + ACPMUX_REQUEST_META.iter().map(|key| ((*key).to_owned(), Value::Bool(true))).collect(); + Schema::try_from( + json!({"type": "object", "properties": properties, "additionalProperties": false}), + ) + .unwrap_or_default() + } +} + +/// [`AcpmuxRequestMeta`]'s keys. +pub const ACPMUX_REQUEST_META: &[&str] = &[ + "adopt", + "agentSessionId", + "answeredBy", + "approverDecision", + "approverDecisions", + "argv", + "author", + "beforePromptId", + "callerSession", + "cliHost", + "context", + "cwd", + "decisionNonce", + "dispatchSource", + "env", + "ephemeral", + "forceAdmit", + "forwardProjection", + "fromAgent", + "fromHarness", + "fromSessionId", + "githubBroker", + "harness", + "harnessFrom", + "host", + "inlineParts", + "interrupt", + "localAppToken", + "mcpServers", + "messageId", + "messageSender", + "mode", + "model", + "name", + "notice", + "observer", + "parent", + "peer", + "peerHops", + "personDecision", + "personKey", + "personStop", + "policy", + "pr", + "preset", + "promptFileText", + "promptId", + "promptSuggestion", + "provenance", + "reportTo", + "requestId", + "requireFullAccess", + "resend", + "route", + "spawnToolCallId", + "startLater", + "steer", + "steerNow", + "steerOnly", + "supersede", + "surface", + "teamMessage", + "throughSeq", + "via", + "vmBoundary", + "waitStart", + "workflowAllowedTools", + "workspaceId", +]; + +keyed! { + /// `_acpmux/agent_hold`. + AgentHoldParams: ["sessionId", "held"]; + /// `_acpmux/agent_login/api_key`. + AgentLoginApiKeyParams: ["agent", "key", "sessionId"]; + /// `_acpmux/agent_login/cancel`. + AgentLoginCancelParams: ["loginId", "sessionId"]; + /// `_acpmux/agent_login/finish`. + AgentLoginFinishParams: ["loginId", "code", "sessionId"]; + /// `_acpmux/agent_login/start`. + AgentLoginStartParams: ["agent", "sessionId"]; + /// `_acpmux/agent_pair_resume`. + AgentPairResumeParams: ["sessionId", "peerSessionId"]; + /// `_acpmux/app_reply`. + AppReplyParams: ["sessionId", "to", "replyTo", "threadId", "text", "replyId"]; + /// `_acpmux/archive`. + ArchiveParams: ["sessionId", "archived"]; + /// `_acpmux/attachment`. + AttachmentParams: ["sessionId", "id"]; + /// `_acpmux/bookmark_get`. + BookmarkGetParams: ["sessionId"]; + /// `_acpmux/bookmark_set`. + BookmarkSetParams: ["sessionId", "seq", "bookmarked"]; + /// `_acpmux/btw`. + BtwParams: ["sessionId", "question", "model", "inline"]; + /// `_acpmux/chat/route.set`. + ChatRouteSetParams: ["sessionId", "routeId", "when"]; + /// `_acpmux/draft_get`. + DraftGetParams: ["sessionId"]; + /// `_acpmux/draft_set`. + DraftSetParams: ["sessionId", "text"]; + /// `_acpmux/forum/rules`. + ForumRulesParams: []; + /// `_acpmux/handoff_discard`. + HandoffDiscardParams: ["handoffId"]; + /// `_acpmux/handoff_draft`. + HandoffDraftParams: ["handoffId", "revision", "draftKey", "capsule", "checkpoint"]; + /// `_acpmux/handoff_get`. + HandoffGetParams: ["sessionId", "handoffId"]; + /// `_acpmux/handoff_prepare`. + HandoffPrepareParams: ["sessionId", "harness", "handoffKey", "memoryRefs"]; + /// `_acpmux/handoff_start`. + HandoffStartParams: ["handoffId", "revision", "promptId", "capsule", "checkpoint"]; + /// `_acpmux/harness_enable`. + HarnessEnableParams: ["folder", "id"]; + /// `_acpmux/harness/health`. + HarnessHealthParams: ["refresh", "rescan", "checkLatest", "autoUpdate", "view"]; + /// `_acpmux/harnesses`. + HarnessesParams: ["cwd"]; + /// `_acpmux/inbox_thread`. + InboxThreadParams: ["sessionId", "threadId", "markRead"]; + /// `initialize`. + InitializeParams: ["protocolVersion", "clientCapabilities", "clientInfo"]; + /// `_acpmux/kill`. + KillParams: ["sessionId", "purge"]; + /// `_acpmux/mcp/reconnect`. + McpReconnectParams: ["sessionId", "server"]; + /// `_acpmux/models`. + ModelsParams: ["refresh", "probe"]; + /// `_acpmux/native_history`. + NativeHistoryParams: ["sessionId", "before", "mark"]; + /// `_acpmux/offer_respond`. + OfferRespondParams: ["sessionId", "offerId", "outcome", "childSessionId"]; + /// `_acpmux/peer_reconnect`. + PeerReconnectParams: ["name", "wait", "force"]; + /// `_acpmux/peer_snapshot`. + PeerSnapshotParams: ["sessionId", "host", "limit", "maxBytes"]; + /// `_acpmux/peers`. + PeersParams: []; + /// `_acpmux/prewarm`. + PrewarmParams: ["harness", "cwd", "release"]; + /// `_acpmux/process/kill`. + ProcessKillParams: ["sessionId", "processId", "signal"]; + /// `_acpmux/process/list`. + ProcessListParams: ["sessionId", "cursor", "limit"]; + /// `_acpmux/process/tail`. + ProcessTailParams: ["sessionId", "processId", "fromByte", "maxBytes"]; + /// `_acpmux/queue_approve`. + QueueApproveParams: ["sessionId", "promptId", "turnId", "revision"]; + /// `_acpmux/queue_decline`. + QueueDeclineParams: ["sessionId", "promptId"]; + /// `_acpmux/queue_edit`. + QueueEditParams: ["sessionId", "promptId", "op", "text", "images", "index"]; + /// `_acpmux/queue_get`. + QueueGetParams: ["sessionId", "promptId"]; + /// `_acpmux/queue_remove`. + QueueRemoveParams: ["sessionId", "promptId"]; + /// `_acpmux/resolve`. + ResolveParams: ["query", "sessionId", "exactSessionId"]; + /// `_acpmux/route/list`. + RouteListParams: ["sessionId"]; + /// `_acpmux/secret_submit`. + SecretSubmitParams: ["sessionId", "secretId", "value"]; + /// `session/cancel`. + SessionCancelParams: ["sessionId"]; + /// `session/new`. + SessionNewParams: ["cwd", "mcpServers", "additionalDirectories"]; + /// `session/prompt`. + SessionPromptParams: ["sessionId", "prompt", "steer"]; + /// `session/set_config_option`. + SessionSetConfigOptionParams: ["sessionId", "configId", "value"]; + /// `session/set_mode`. + SessionSetModeParams: ["sessionId", "modeId"]; + /// `session/set_model`. + SessionSetModelParams: ["sessionId", "modelId"]; + /// `_acpmux/set_tools`. + SetToolsParams: ["sessionId", "id", "enabled"]; + /// `_acpmux/side`. + SideParams: ["sessionId", "chat"]; + /// `_acpmux/start_mode`. + StartModeParams: ["harness", "preset", "cwd"]; + /// `_acpmux/status`. + StatusParams: []; + /// `_acpmux/subagent_stop`. + SubagentStopParams: ["sessionId", "subagentSessionId"]; + /// `_acpmux/subagent_transcript`. + SubagentTranscriptParams: ["sessionId", "runId", "agentId", "before", "after", "limit"]; + /// `_acpmux/subsession/limits`. + SubsessionLimitsParams: ["sessionId", "maxRunning", "workspaceId"]; + /// `acp.trust.get`. + TrustGetParams: ["cwd", "sessionId", "peer"]; + /// `acp.trust.set`. + TrustSetParams: ["cwd", "level", "sessionId", "peer"]; + /// `_acpmux/warm`. + WarmParams: ["sessionIds", "limit"]; + /// `_acpmux/watch_take_over`. + WatchTakeOverParams: ["sessionId", "ifLive"]; +} diff --git a/cmux-tui/crates/acpmux/src/protocol/session.rs b/cmux-tui/crates/acpmux/src/protocol/session.rs index 199cff2b253d..aac2277bc329 100644 --- a/cmux-tui/crates/acpmux/src/protocol/session.rs +++ b/cmux-tui/crates/acpmux/src/protocol/session.rs @@ -12,6 +12,7 @@ use super::require_all; /// Before-prompt requests require requestId and omit both throughSeq and live. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] +#[schemars(deny_unknown_fields)] pub struct ForkParams { pub session_id: String, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/cmux-tui/crates/acpmux/src/protocol/tests.rs b/cmux-tui/crates/acpmux/src/protocol/tests.rs index 519996ac8e23..afd7ff039076 100644 --- a/cmux-tui/crates/acpmux/src/protocol/tests.rs +++ b/cmux-tui/crates/acpmux/src/protocol/tests.rs @@ -138,3 +138,20 @@ fn protocol_author_approval_round_trips() { assert_eq!(ir["types"]["AuthorApproval"]["enum"], json!(["owner"])); assert!(!schema["required"].as_array().unwrap().contains(&json!("approval"))); } + +/// A keyed method's params are closed, and every request's `_meta.acpmux` names +/// [`super::params::AcpmuxRequestMeta`], so the IR's `types` carries it. +#[test] +fn protocol_keyed_params_are_closed_and_name_the_request_meta() { + let ir: serde_json::Value = serde_json::from_str(&ir_text()).expect("IR is JSON"); + let agent_hold = &ir["types"]["AgentHoldParams"]; + assert_eq!(agent_hold["additionalProperties"], false, "{agent_hold}"); + assert_eq!(agent_hold["properties"]["_meta"]["$ref"], "#/types/RequestMeta"); + let meta = &ir["types"]["AcpmuxRequestMeta"]; + assert_eq!(meta["additionalProperties"], false, "{meta}"); + let keys: BTreeSet<&str> = + meta["properties"].as_object().expect("properties").keys().map(String::as_str).collect(); + let listed: BTreeSet<&str> = super::params::ACPMUX_REQUEST_META.iter().copied().collect(); + assert_eq!(keys, listed); + assert!(keys.contains("localAppToken") && keys.contains("personKey"), "{keys:?}"); +} diff --git a/webviews/src/agent-session/acpmux/protocol/generated/methods.ts b/webviews/src/agent-session/acpmux/protocol/generated/methods.ts index d385bfc5dcf5..276c54cb09dd 100644 --- a/webviews/src/agent-session/acpmux/protocol/generated/methods.ts +++ b/webviews/src/agent-session/acpmux/protocol/generated/methods.ts @@ -1,32 +1,32 @@ // @generated by webviews/scripts/acpmux-protocol-codegen.ts from cmux-tui/crates/acpmux/spec/acpmux-protocol.json. Do not edit; rerun the codegen. -import type { AttachParams, AttachResult, Done, EventsParams, EventsResult, ForkParams, HarnessUpdatePlanParams, InventoryRefreshParams, InventoryWatchParams, MailAckParams, MailAckResult, MailPageResult, MailReadParams, MailSendParams, MailSendResult, MailWaitParams, MessageEvent, PeerStateChanged, PermissionChatRevokeParams, PermissionChatRevokeReceipt, PermissionGroupList, PermissionGroupReceipt, PermissionGroupRespondParams, PermissionGroupsParams, PermissionPending, PermissionRespondParams, PermissionRespondResult, PresenceNote, ProcessBackgroundParams, ProcessBackgroundResult, ProtocolParams, RetryParams, RetryResult, SessionAuthParams, SessionAuthResult, SessionChanged, SessionEvent, SessionRef, SessionsParams, SessionsResult, TypingParams, ViewParams, WatchParams } from "./types"; +import type { AgentHoldParams, AgentLoginApiKeyParams, AgentLoginCancelParams, AgentLoginFinishParams, AgentLoginStartParams, AgentPairResumeParams, AppReplyParams, ArchiveParams, AttachParams, AttachResult, AttachmentParams, BookmarkGetParams, BookmarkSetParams, BtwParams, ChatRouteSetParams, Done, DraftGetParams, DraftSetParams, EventsParams, EventsResult, ForkParams, ForumRulesParams, HandoffDiscardParams, HandoffDraftParams, HandoffGetParams, HandoffPrepareParams, HandoffStartParams, HarnessEnableParams, HarnessHealthParams, HarnessUpdatePlanParams, HarnessesParams, InboxThreadParams, InitializeParams, InventoryRefreshParams, InventoryWatchParams, KillParams, MailAckParams, MailAckResult, MailPageResult, MailReadParams, MailSendParams, MailSendResult, MailWaitParams, McpReconnectParams, MessageEvent, ModelsParams, NativeHistoryParams, OfferRespondParams, PeerReconnectParams, PeerSnapshotParams, PeerStateChanged, PeersParams, PermissionChatRevokeParams, PermissionChatRevokeReceipt, PermissionGroupList, PermissionGroupReceipt, PermissionGroupRespondParams, PermissionGroupsParams, PermissionPending, PermissionRespondParams, PermissionRespondResult, PresenceNote, PrewarmParams, ProcessBackgroundParams, ProcessBackgroundResult, ProcessKillParams, ProcessListParams, ProcessTailParams, ProtocolParams, QueueApproveParams, QueueDeclineParams, QueueEditParams, QueueGetParams, QueueRemoveParams, ResolveParams, RetryParams, RetryResult, RouteListParams, SecretSubmitParams, SessionAuthParams, SessionAuthResult, SessionCancelParams, SessionChanged, SessionEvent, SessionNewParams, SessionPromptParams, SessionRef, SessionSetConfigOptionParams, SessionSetModeParams, SessionSetModelParams, SessionsParams, SessionsResult, SetToolsParams, SideParams, StartModeParams, StatusParams, SubagentStopParams, SubagentTranscriptParams, SubsessionLimitsParams, TrustGetParams, TrustSetParams, TypingParams, ViewParams, WarmParams, WatchParams, WatchTakeOverParams } from "./types"; /** Client to daemon requests. */ export interface AcpmuxRequests { - "initialize": { params: unknown; result: unknown }; + "initialize": { params: InitializeParams; result: unknown }; "authenticate": { params: unknown; result: unknown }; - "session/new": { params: unknown; result: unknown }; + "session/new": { params: SessionNewParams; result: unknown }; "session/load": { params: unknown; result: unknown }; "session/resume": { params: unknown; result: unknown }; "session/list": { params: unknown; result: unknown }; "session/fork": { params: unknown; result: unknown }; "session/close": { params: unknown; result: unknown }; "session/delete": { params: unknown; result: unknown }; - "session/prompt": { params: unknown; result: unknown }; - "session/set_mode": { params: unknown; result: unknown }; - "session/set_model": { params: unknown; result: unknown }; - "session/set_config_option": { params: unknown; result: unknown }; + "session/prompt": { params: SessionPromptParams; result: unknown }; + "session/set_mode": { params: SessionSetModeParams; result: unknown }; + "session/set_model": { params: SessionSetModelParams; result: unknown }; + "session/set_config_option": { params: SessionSetConfigOptionParams; result: unknown }; "acp.session.fork": { params: ForkParams; result: unknown }; - "acp.trust.get": { params: unknown; result: unknown }; - "acp.trust.set": { params: unknown; result: unknown }; + "acp.trust.get": { params: TrustGetParams; result: unknown }; + "acp.trust.set": { params: TrustSetParams; result: unknown }; "catalog.get": { params: unknown; result: unknown }; "catalog.refresh": { params: unknown; result: unknown }; "_acpmux/tailnet/login": { params: unknown; result: unknown }; "_acpmux/tailnet/logout": { params: unknown; result: unknown }; "_acpmux/tailnet/start": { params: unknown; result: unknown }; "_acpmux/tailnet/state": { params: unknown; result: unknown }; - "_acpmux/status": { params: unknown; result: unknown }; + "_acpmux/status": { params: StatusParams; result: unknown }; "_acpmux/context": { params: unknown; result: unknown }; "_acpmux/context_override": { params: unknown; result: unknown }; "agent-session-turn-prepare": { params: unknown; result: unknown }; @@ -43,10 +43,10 @@ export interface AcpmuxRequests { "_acpmux/watch": { params: WatchParams; result: SessionsResult }; "_acpmux/rename": { params: unknown; result: unknown }; "_acpmux/offer_session": { params: unknown; result: unknown }; - "_acpmux/offer_respond": { params: unknown; result: unknown }; + "_acpmux/offer_respond": { params: OfferRespondParams; result: unknown }; "_acpmux/subsession/answer": { params: unknown; result: unknown }; "_acpmux/subsession/get": { params: unknown; result: unknown }; - "_acpmux/subsession/limits": { params: unknown; result: unknown }; + "_acpmux/subsession/limits": { params: SubsessionLimitsParams; result: unknown }; "_acpmux/subsession/list": { params: unknown; result: unknown }; "_acpmux/subsession/retry": { params: unknown; result: unknown }; "_acpmux/subsession/send": { params: unknown; result: unknown }; @@ -54,21 +54,21 @@ export interface AcpmuxRequests { "_acpmux/subsession/stop": { params: unknown; result: unknown }; "_acpmux/forum/ack": { params: unknown; result: unknown }; "_acpmux/forum/announce": { params: unknown; result: unknown }; - "_acpmux/forum/rules": { params: unknown; result: unknown }; - "_acpmux/set_tools": { params: unknown; result: unknown }; + "_acpmux/forum/rules": { params: ForumRulesParams; result: unknown }; + "_acpmux/set_tools": { params: SetToolsParams; result: unknown }; "_acpmux/set_directories": { params: unknown; result: unknown }; - "_acpmux/mcp/reconnect": { params: unknown; result: unknown }; - "_acpmux/subagent_stop": { params: unknown; result: unknown }; - "_acpmux/subagent_transcript": { params: unknown; result: unknown }; - "_acpmux/native_history": { params: unknown; result: unknown }; + "_acpmux/mcp/reconnect": { params: McpReconnectParams; result: unknown }; + "_acpmux/subagent_stop": { params: SubagentStopParams; result: unknown }; + "_acpmux/subagent_transcript": { params: SubagentTranscriptParams; result: unknown }; + "_acpmux/native_history": { params: NativeHistoryParams; result: unknown }; "_acpmux/workflows": { params: unknown; result: unknown }; - "_acpmux/app_reply": { params: unknown; result: unknown }; + "_acpmux/app_reply": { params: AppReplyParams; result: unknown }; "_acpmux/stats": { params: unknown; result: unknown }; "_acpmux/workspace_purge": { params: unknown; result: unknown }; "_acpmux/github_action": { params: unknown; result: unknown }; "_acpmux/github_app_lease": { params: unknown; result: unknown }; - "_acpmux/bookmark_get": { params: unknown; result: unknown }; - "_acpmux/bookmark_set": { params: unknown; result: unknown }; + "_acpmux/bookmark_get": { params: BookmarkGetParams; result: unknown }; + "_acpmux/bookmark_set": { params: BookmarkSetParams; result: unknown }; "_acpmux/prompt_withdraw": { params: unknown; result: unknown }; "_acpmux/inbox_digest": { params: unknown; result: unknown }; "_acpmux/send_queue": { params: unknown; result: unknown }; @@ -76,13 +76,13 @@ export interface AcpmuxRequests { "_acpmux/mesh_control": { params: unknown; result: unknown }; "_acpmux/step_now": { params: unknown; result: unknown }; "_acpmux/steer_withdraw": { params: unknown; result: unknown }; - "_acpmux/agent_pair_resume": { params: unknown; result: unknown }; - "_acpmux/agent_hold": { params: unknown; result: unknown }; + "_acpmux/agent_pair_resume": { params: AgentPairResumeParams; result: unknown }; + "_acpmux/agent_hold": { params: AgentHoldParams; result: unknown }; "_acpmux/retry": { params: RetryParams; result: RetryResult }; "_acpmux/session/auth": { params: SessionAuthParams; result: SessionAuthResult }; - "_acpmux/process/list": { params: unknown; result: unknown }; - "_acpmux/process/tail": { params: unknown; result: unknown }; - "_acpmux/process/kill": { params: unknown; result: unknown }; + "_acpmux/process/list": { params: ProcessListParams; result: unknown }; + "_acpmux/process/tail": { params: ProcessTailParams; result: unknown }; + "_acpmux/process/kill": { params: ProcessKillParams; result: unknown }; "_acpmux/bg/start": { params: unknown; result: unknown }; "_acpmux/bg/list": { params: unknown; result: unknown }; "_acpmux/bg/output": { params: unknown; result: unknown }; @@ -95,18 +95,18 @@ export interface AcpmuxRequests { "_acpmux/worktree/clean": { params: unknown; result: unknown }; "_acpmux/worktree/restore": { params: unknown; result: unknown }; "_acpmux/retention": { params: unknown; result: unknown }; - "_acpmux/kill": { params: unknown; result: unknown }; + "_acpmux/kill": { params: KillParams; result: unknown }; "_acpmux/keep": { params: unknown; result: unknown }; - "_acpmux/watch_take_over": { params: unknown; result: unknown }; - "_acpmux/archive": { params: unknown; result: unknown }; - "_acpmux/side": { params: unknown; result: unknown }; - "_acpmux/btw": { params: unknown; result: unknown }; + "_acpmux/watch_take_over": { params: WatchTakeOverParams; result: unknown }; + "_acpmux/archive": { params: ArchiveParams; result: unknown }; + "_acpmux/side": { params: SideParams; result: unknown }; + "_acpmux/btw": { params: BtwParams; result: unknown }; "_acpmux/tag": { params: unknown; result: unknown }; "_acpmux/detach_parent": { params: unknown; result: unknown }; "_acpmux/info": { params: unknown; result: unknown }; "_acpmux/session_files": { params: unknown; result: unknown }; "_acpmux/events": { params: EventsParams; result: EventsResult }; - "_acpmux/attachment": { params: unknown; result: unknown }; + "_acpmux/attachment": { params: AttachmentParams; result: unknown }; "_acpmux/mail/send": { params: MailSendParams; result: MailSendResult }; "_acpmux/mail/read": { params: MailReadParams; result: MailPageResult }; "_acpmux/mail/wait": { params: MailWaitParams; result: MailPageResult }; @@ -123,17 +123,17 @@ export interface AcpmuxRequests { "_acpmux/watch_ls": { params: unknown; result: unknown }; "_acpmux/watch_rm": { params: unknown; result: unknown }; "_acpmux/ask": { params: unknown; result: unknown }; - "_acpmux/warm": { params: unknown; result: unknown }; - "_acpmux/prewarm": { params: unknown; result: unknown }; - "_acpmux/start_mode": { params: unknown; result: unknown }; - "_acpmux/queue_remove": { params: unknown; result: unknown }; - "_acpmux/queue_edit": { params: unknown; result: unknown }; - "_acpmux/queue_approve": { params: unknown; result: unknown }; - "_acpmux/queue_decline": { params: unknown; result: unknown }; + "_acpmux/warm": { params: WarmParams; result: unknown }; + "_acpmux/prewarm": { params: PrewarmParams; result: unknown }; + "_acpmux/start_mode": { params: StartModeParams; result: unknown }; + "_acpmux/queue_remove": { params: QueueRemoveParams; result: unknown }; + "_acpmux/queue_edit": { params: QueueEditParams; result: unknown }; + "_acpmux/queue_approve": { params: QueueApproveParams; result: unknown }; + "_acpmux/queue_decline": { params: QueueDeclineParams; result: unknown }; "_acpmux/queue_text": { params: unknown; result: unknown }; - "_acpmux/queue_get": { params: unknown; result: unknown }; - "_acpmux/draft_get": { params: unknown; result: unknown }; - "_acpmux/draft_set": { params: unknown; result: unknown }; + "_acpmux/queue_get": { params: QueueGetParams; result: unknown }; + "_acpmux/draft_get": { params: DraftGetParams; result: unknown }; + "_acpmux/draft_set": { params: DraftSetParams; result: unknown }; "_acpmux/composer_set": { params: unknown; result: unknown }; "_acpmux/composers": { params: unknown; result: unknown }; "_acpmux/export": { params: unknown; result: unknown }; @@ -151,7 +151,7 @@ export interface AcpmuxRequests { "_acpmux/report": { params: unknown; result: unknown }; "_acpmux/task_new": { params: unknown; result: unknown }; "_acpmux/tasks": { params: unknown; result: unknown }; - "_acpmux/resolve": { params: unknown; result: unknown }; + "_acpmux/resolve": { params: ResolveParams; result: unknown }; "_acpmux/who_else": { params: unknown; result: unknown }; "_acpmux/media_open": { params: unknown; result: unknown }; "_acpmux/media_range": { params: unknown; result: unknown }; @@ -164,7 +164,7 @@ export interface AcpmuxRequests { "_acpmux/set_rules": { params: unknown; result: unknown }; "_acpmux/set_turn_cap": { params: unknown; result: unknown }; "_acpmux/inbox_threads": { params: unknown; result: unknown }; - "_acpmux/inbox_thread": { params: unknown; result: unknown }; + "_acpmux/inbox_thread": { params: InboxThreadParams; result: unknown }; "_acpmux/inbox_reply": { params: unknown; result: unknown }; "_acpmux/person_enroll": { params: unknown; result: unknown }; "_acpmux/computer_use_enroll": { params: unknown; result: unknown }; @@ -177,7 +177,7 @@ export interface AcpmuxRequests { "_acpmux/new_chat_picks_set": { params: unknown; result: unknown }; "_acpmux/workspace_roots_set": { params: unknown; result: unknown }; "_acpmux/secret_request": { params: unknown; result: unknown }; - "_acpmux/secret_submit": { params: unknown; result: unknown }; + "_acpmux/secret_submit": { params: SecretSubmitParams; result: unknown }; "_acpmux/secret_run": { params: unknown; result: unknown }; "_acpmux/mcp_connect_request": { params: unknown; result: unknown }; "_acpmux/mcp_connect_answer": { params: unknown; result: unknown }; @@ -202,24 +202,24 @@ export interface AcpmuxRequests { "_acpmux/transfer_read": { params: unknown; result: unknown }; "_acpmux/transfer_receive": { params: unknown; result: unknown }; "_acpmux/transfer_write": { params: unknown; result: unknown }; - "_acpmux/handoff_prepare": { params: unknown; result: unknown }; - "_acpmux/handoff_get": { params: unknown; result: unknown }; - "_acpmux/handoff_draft": { params: unknown; result: unknown }; - "_acpmux/handoff_start": { params: unknown; result: unknown }; - "_acpmux/handoff_discard": { params: unknown; result: unknown }; - "_acpmux/harnesses": { params: unknown; result: unknown }; - "_acpmux/harness_enable": { params: unknown; result: unknown }; + "_acpmux/handoff_prepare": { params: HandoffPrepareParams; result: unknown }; + "_acpmux/handoff_get": { params: HandoffGetParams; result: unknown }; + "_acpmux/handoff_draft": { params: HandoffDraftParams; result: unknown }; + "_acpmux/handoff_start": { params: HandoffStartParams; result: unknown }; + "_acpmux/handoff_discard": { params: HandoffDiscardParams; result: unknown }; + "_acpmux/harnesses": { params: HarnessesParams; result: unknown }; + "_acpmux/harness_enable": { params: HarnessEnableParams; result: unknown }; "_acpmux/harness/add": { params: unknown; result: unknown }; "_acpmux/harness/remove": { params: unknown; result: unknown }; "_acpmux/harness/restore": { params: unknown; result: unknown }; "_acpmux/harness/doctor": { params: unknown; result: unknown }; "_acpmux/harness/secret/set": { params: unknown; result: unknown }; "_acpmux/harness/icon": { params: unknown; result: unknown }; - "_acpmux/harness/health": { params: unknown; result: unknown }; - "_acpmux/agent_login/start": { params: unknown; result: unknown }; - "_acpmux/agent_login/finish": { params: unknown; result: unknown }; - "_acpmux/agent_login/cancel": { params: unknown; result: unknown }; - "_acpmux/agent_login/api_key": { params: unknown; result: unknown }; + "_acpmux/harness/health": { params: HarnessHealthParams; result: unknown }; + "_acpmux/agent_login/start": { params: AgentLoginStartParams; result: unknown }; + "_acpmux/agent_login/finish": { params: AgentLoginFinishParams; result: unknown }; + "_acpmux/agent_login/cancel": { params: AgentLoginCancelParams; result: unknown }; + "_acpmux/agent_login/api_key": { params: AgentLoginApiKeyParams; result: unknown }; "_acpmux/harness/inventory/watch": { params: InventoryWatchParams; result: unknown }; "_acpmux/harness/inventory/refresh": { params: InventoryRefreshParams; result: unknown }; "_acpmux/harness/update/plan": { params: HarnessUpdatePlanParams; result: unknown }; @@ -229,9 +229,9 @@ export interface AcpmuxRequests { "_acpmux/registry": { params: unknown; result: unknown }; "_acpmux/defaults": { params: unknown; result: unknown }; "_acpmux/presets": { params: unknown; result: unknown }; - "_acpmux/models": { params: unknown; result: unknown }; + "_acpmux/models": { params: ModelsParams; result: unknown }; "_acpmux/directories": { params: unknown; result: unknown }; - "_acpmux/route/list": { params: unknown; result: unknown }; + "_acpmux/route/list": { params: RouteListParams; result: unknown }; "_acpmux/route/show": { params: unknown; result: unknown }; "_acpmux/route/add": { params: unknown; result: unknown }; "_acpmux/route/edit": { params: unknown; result: unknown }; @@ -239,7 +239,7 @@ export interface AcpmuxRequests { "_acpmux/route/restore": { params: unknown; result: unknown }; "_acpmux/route/test": { params: unknown; result: unknown }; "_acpmux/route/default.set": { params: unknown; result: unknown }; - "_acpmux/chat/route.set": { params: unknown; result: unknown }; + "_acpmux/chat/route.set": { params: ChatRouteSetParams; result: unknown }; "_acpmux/accounts": { params: unknown; result: unknown }; "_acpmux/account/set": { params: unknown; result: unknown }; "_acpmux/account/usage": { params: unknown; result: unknown }; @@ -249,7 +249,7 @@ export interface AcpmuxRequests { "_acpmux/schema": { params: unknown; result: unknown }; "_acpmux/protocol": { params: ProtocolParams; result: unknown }; "_acpmux/shutdown": { params: unknown; result: unknown }; - "_acpmux/peers": { params: unknown; result: unknown }; + "_acpmux/peers": { params: PeersParams; result: unknown }; "_acpmux/peer_status": { params: unknown; result: unknown }; "_acpmux/host_health": { params: unknown; result: unknown }; "_acpmux/vm_host": { params: unknown; result: unknown }; @@ -257,9 +257,9 @@ export interface AcpmuxRequests { "_acpmux/place": { params: unknown; result: unknown }; "_acpmux/peer_add": { params: unknown; result: unknown }; "_acpmux/peer_lease": { params: unknown; result: unknown }; - "_acpmux/peer_reconnect": { params: unknown; result: unknown }; + "_acpmux/peer_reconnect": { params: PeerReconnectParams; result: unknown }; "_acpmux/peer_remove": { params: unknown; result: unknown }; - "_acpmux/peer_snapshot": { params: unknown; result: unknown }; + "_acpmux/peer_snapshot": { params: PeerSnapshotParams; result: unknown }; "_acpmux/peer_snapshots_clear": { params: unknown; result: unknown }; "_acpmux/pair_ticket_new": { params: unknown; result: unknown }; "_acpmux/pair_list": { params: unknown; result: unknown }; @@ -278,7 +278,7 @@ export interface AcpmuxRequests { /** Client to daemon notifications (no response). */ export interface AcpmuxClientNotifications { - "session/cancel": unknown; + "session/cancel": SessionCancelParams; "$/cancel_request": unknown; } @@ -318,26 +318,26 @@ export type AcpmuxMethodKind = "request" | "client_notification" | "notification /** Every registered method, in registry order. `typed` is false while params and result are both untyped. */ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKind; acp: boolean; typed: boolean }> = [ - { name: "initialize", kind: "request", acp: true, typed: false }, + { name: "initialize", kind: "request", acp: true, typed: true }, { name: "authenticate", kind: "request", acp: true, typed: false }, - { name: "session/new", kind: "request", acp: true, typed: false }, + { name: "session/new", kind: "request", acp: true, typed: true }, { name: "session/load", kind: "request", acp: true, typed: false }, { name: "session/resume", kind: "request", acp: true, typed: false }, { name: "session/list", kind: "request", acp: true, typed: false }, { name: "session/fork", kind: "request", acp: true, typed: false }, { name: "session/close", kind: "request", acp: true, typed: false }, { name: "session/delete", kind: "request", acp: true, typed: false }, - { name: "session/prompt", kind: "request", acp: true, typed: false }, - { name: "session/cancel", kind: "client_notification", acp: true, typed: false }, - { name: "session/set_mode", kind: "request", acp: true, typed: false }, - { name: "session/set_model", kind: "request", acp: true, typed: false }, - { name: "session/set_config_option", kind: "request", acp: true, typed: false }, + { name: "session/prompt", kind: "request", acp: true, typed: true }, + { name: "session/cancel", kind: "client_notification", acp: true, typed: true }, + { name: "session/set_mode", kind: "request", acp: true, typed: true }, + { name: "session/set_model", kind: "request", acp: true, typed: true }, + { name: "session/set_config_option", kind: "request", acp: true, typed: true }, { name: "session/update", kind: "notification", acp: true, typed: false }, { name: "session/request_permission", kind: "server_request", acp: true, typed: false }, { name: "$/cancel_request", kind: "client_notification", acp: true, typed: false }, { name: "acp.session.fork", kind: "request", acp: false, typed: true }, - { name: "acp.trust.get", kind: "request", acp: false, typed: false }, - { name: "acp.trust.set", kind: "request", acp: false, typed: false }, + { name: "acp.trust.get", kind: "request", acp: false, typed: true }, + { name: "acp.trust.set", kind: "request", acp: false, typed: true }, { name: "catalog.get", kind: "request", acp: false, typed: false }, { name: "catalog.refresh", kind: "request", acp: false, typed: false }, { name: "catalog.changed", kind: "notification", acp: false, typed: false }, @@ -345,7 +345,7 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/tailnet/logout", kind: "request", acp: false, typed: false }, { name: "_acpmux/tailnet/start", kind: "request", acp: false, typed: false }, { name: "_acpmux/tailnet/state", kind: "request", acp: false, typed: false }, - { name: "_acpmux/status", kind: "request", acp: false, typed: false }, + { name: "_acpmux/status", kind: "request", acp: false, typed: true }, { name: "_acpmux/context", kind: "request", acp: false, typed: false }, { name: "_acpmux/context_override", kind: "request", acp: false, typed: false }, { name: "agent-session-turn-prepare", kind: "request", acp: false, typed: false }, @@ -362,10 +362,10 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/watch", kind: "request", acp: false, typed: true }, { name: "_acpmux/rename", kind: "request", acp: false, typed: false }, { name: "_acpmux/offer_session", kind: "request", acp: false, typed: false }, - { name: "_acpmux/offer_respond", kind: "request", acp: false, typed: false }, + { name: "_acpmux/offer_respond", kind: "request", acp: false, typed: true }, { name: "_acpmux/subsession/answer", kind: "request", acp: false, typed: false }, { name: "_acpmux/subsession/get", kind: "request", acp: false, typed: false }, - { name: "_acpmux/subsession/limits", kind: "request", acp: false, typed: false }, + { name: "_acpmux/subsession/limits", kind: "request", acp: false, typed: true }, { name: "_acpmux/subsession/list", kind: "request", acp: false, typed: false }, { name: "_acpmux/subsession/retry", kind: "request", acp: false, typed: false }, { name: "_acpmux/subsession/send", kind: "request", acp: false, typed: false }, @@ -373,21 +373,21 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/subsession/stop", kind: "request", acp: false, typed: false }, { name: "_acpmux/forum/ack", kind: "request", acp: false, typed: false }, { name: "_acpmux/forum/announce", kind: "request", acp: false, typed: false }, - { name: "_acpmux/forum/rules", kind: "request", acp: false, typed: false }, - { name: "_acpmux/set_tools", kind: "request", acp: false, typed: false }, + { name: "_acpmux/forum/rules", kind: "request", acp: false, typed: true }, + { name: "_acpmux/set_tools", kind: "request", acp: false, typed: true }, { name: "_acpmux/set_directories", kind: "request", acp: false, typed: false }, - { name: "_acpmux/mcp/reconnect", kind: "request", acp: false, typed: false }, - { name: "_acpmux/subagent_stop", kind: "request", acp: false, typed: false }, - { name: "_acpmux/subagent_transcript", kind: "request", acp: false, typed: false }, - { name: "_acpmux/native_history", kind: "request", acp: false, typed: false }, + { name: "_acpmux/mcp/reconnect", kind: "request", acp: false, typed: true }, + { name: "_acpmux/subagent_stop", kind: "request", acp: false, typed: true }, + { name: "_acpmux/subagent_transcript", kind: "request", acp: false, typed: true }, + { name: "_acpmux/native_history", kind: "request", acp: false, typed: true }, { name: "_acpmux/workflows", kind: "request", acp: false, typed: false }, - { name: "_acpmux/app_reply", kind: "request", acp: false, typed: false }, + { name: "_acpmux/app_reply", kind: "request", acp: false, typed: true }, { name: "_acpmux/stats", kind: "request", acp: false, typed: false }, { name: "_acpmux/workspace_purge", kind: "request", acp: false, typed: false }, { name: "_acpmux/github_action", kind: "request", acp: false, typed: false }, { name: "_acpmux/github_app_lease", kind: "request", acp: false, typed: false }, - { name: "_acpmux/bookmark_get", kind: "request", acp: false, typed: false }, - { name: "_acpmux/bookmark_set", kind: "request", acp: false, typed: false }, + { name: "_acpmux/bookmark_get", kind: "request", acp: false, typed: true }, + { name: "_acpmux/bookmark_set", kind: "request", acp: false, typed: true }, { name: "_acpmux/prompt_withdraw", kind: "request", acp: false, typed: false }, { name: "_acpmux/inbox_digest", kind: "request", acp: false, typed: false }, { name: "_acpmux/send_queue", kind: "request", acp: false, typed: false }, @@ -395,13 +395,13 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/mesh_control", kind: "request", acp: false, typed: false }, { name: "_acpmux/step_now", kind: "request", acp: false, typed: false }, { name: "_acpmux/steer_withdraw", kind: "request", acp: false, typed: false }, - { name: "_acpmux/agent_pair_resume", kind: "request", acp: false, typed: false }, - { name: "_acpmux/agent_hold", kind: "request", acp: false, typed: false }, + { name: "_acpmux/agent_pair_resume", kind: "request", acp: false, typed: true }, + { name: "_acpmux/agent_hold", kind: "request", acp: false, typed: true }, { name: "_acpmux/retry", kind: "request", acp: false, typed: true }, { name: "_acpmux/session/auth", kind: "request", acp: false, typed: true }, - { name: "_acpmux/process/list", kind: "request", acp: false, typed: false }, - { name: "_acpmux/process/tail", kind: "request", acp: false, typed: false }, - { name: "_acpmux/process/kill", kind: "request", acp: false, typed: false }, + { name: "_acpmux/process/list", kind: "request", acp: false, typed: true }, + { name: "_acpmux/process/tail", kind: "request", acp: false, typed: true }, + { name: "_acpmux/process/kill", kind: "request", acp: false, typed: true }, { name: "_acpmux/bg/start", kind: "request", acp: false, typed: false }, { name: "_acpmux/bg/list", kind: "request", acp: false, typed: false }, { name: "_acpmux/bg/output", kind: "request", acp: false, typed: false }, @@ -415,18 +415,18 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/worktree/restore", kind: "request", acp: false, typed: false }, { name: "_acpmux/retention", kind: "request", acp: false, typed: false }, { name: "_acpmux/process_update", kind: "notification", acp: false, typed: false }, - { name: "_acpmux/kill", kind: "request", acp: false, typed: false }, + { name: "_acpmux/kill", kind: "request", acp: false, typed: true }, { name: "_acpmux/keep", kind: "request", acp: false, typed: false }, - { name: "_acpmux/watch_take_over", kind: "request", acp: false, typed: false }, - { name: "_acpmux/archive", kind: "request", acp: false, typed: false }, - { name: "_acpmux/side", kind: "request", acp: false, typed: false }, - { name: "_acpmux/btw", kind: "request", acp: false, typed: false }, + { name: "_acpmux/watch_take_over", kind: "request", acp: false, typed: true }, + { name: "_acpmux/archive", kind: "request", acp: false, typed: true }, + { name: "_acpmux/side", kind: "request", acp: false, typed: true }, + { name: "_acpmux/btw", kind: "request", acp: false, typed: true }, { name: "_acpmux/tag", kind: "request", acp: false, typed: false }, { name: "_acpmux/detach_parent", kind: "request", acp: false, typed: false }, { name: "_acpmux/info", kind: "request", acp: false, typed: false }, { name: "_acpmux/session_files", kind: "request", acp: false, typed: false }, { name: "_acpmux/events", kind: "request", acp: false, typed: true }, - { name: "_acpmux/attachment", kind: "request", acp: false, typed: false }, + { name: "_acpmux/attachment", kind: "request", acp: false, typed: true }, { name: "_acpmux/mail/send", kind: "request", acp: false, typed: true }, { name: "_acpmux/mail/read", kind: "request", acp: false, typed: true }, { name: "_acpmux/mail/wait", kind: "request", acp: false, typed: true }, @@ -443,17 +443,17 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/watch_ls", kind: "request", acp: false, typed: false }, { name: "_acpmux/watch_rm", kind: "request", acp: false, typed: false }, { name: "_acpmux/ask", kind: "request", acp: false, typed: false }, - { name: "_acpmux/warm", kind: "request", acp: false, typed: false }, - { name: "_acpmux/prewarm", kind: "request", acp: false, typed: false }, - { name: "_acpmux/start_mode", kind: "request", acp: false, typed: false }, - { name: "_acpmux/queue_remove", kind: "request", acp: false, typed: false }, - { name: "_acpmux/queue_edit", kind: "request", acp: false, typed: false }, - { name: "_acpmux/queue_approve", kind: "request", acp: false, typed: false }, - { name: "_acpmux/queue_decline", kind: "request", acp: false, typed: false }, + { name: "_acpmux/warm", kind: "request", acp: false, typed: true }, + { name: "_acpmux/prewarm", kind: "request", acp: false, typed: true }, + { name: "_acpmux/start_mode", kind: "request", acp: false, typed: true }, + { name: "_acpmux/queue_remove", kind: "request", acp: false, typed: true }, + { name: "_acpmux/queue_edit", kind: "request", acp: false, typed: true }, + { name: "_acpmux/queue_approve", kind: "request", acp: false, typed: true }, + { name: "_acpmux/queue_decline", kind: "request", acp: false, typed: true }, { name: "_acpmux/queue_text", kind: "request", acp: false, typed: false }, - { name: "_acpmux/queue_get", kind: "request", acp: false, typed: false }, - { name: "_acpmux/draft_get", kind: "request", acp: false, typed: false }, - { name: "_acpmux/draft_set", kind: "request", acp: false, typed: false }, + { name: "_acpmux/queue_get", kind: "request", acp: false, typed: true }, + { name: "_acpmux/draft_get", kind: "request", acp: false, typed: true }, + { name: "_acpmux/draft_set", kind: "request", acp: false, typed: true }, { name: "_acpmux/composer_set", kind: "request", acp: false, typed: false }, { name: "_acpmux/composers", kind: "request", acp: false, typed: false }, { name: "_acpmux/export", kind: "request", acp: false, typed: false }, @@ -471,7 +471,7 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/report", kind: "request", acp: false, typed: false }, { name: "_acpmux/task_new", kind: "request", acp: false, typed: false }, { name: "_acpmux/tasks", kind: "request", acp: false, typed: false }, - { name: "_acpmux/resolve", kind: "request", acp: false, typed: false }, + { name: "_acpmux/resolve", kind: "request", acp: false, typed: true }, { name: "_acpmux/who_else", kind: "request", acp: false, typed: false }, { name: "_acpmux/media_open", kind: "request", acp: false, typed: false }, { name: "_acpmux/media_range", kind: "request", acp: false, typed: false }, @@ -484,7 +484,7 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/set_rules", kind: "request", acp: false, typed: false }, { name: "_acpmux/set_turn_cap", kind: "request", acp: false, typed: false }, { name: "_acpmux/inbox_threads", kind: "request", acp: false, typed: false }, - { name: "_acpmux/inbox_thread", kind: "request", acp: false, typed: false }, + { name: "_acpmux/inbox_thread", kind: "request", acp: false, typed: true }, { name: "_acpmux/inbox_reply", kind: "request", acp: false, typed: false }, { name: "_acpmux/person_enroll", kind: "request", acp: false, typed: false }, { name: "_acpmux/computer_use_enroll", kind: "request", acp: false, typed: false }, @@ -497,7 +497,7 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/new_chat_picks_set", kind: "request", acp: false, typed: false }, { name: "_acpmux/workspace_roots_set", kind: "request", acp: false, typed: false }, { name: "_acpmux/secret_request", kind: "request", acp: false, typed: false }, - { name: "_acpmux/secret_submit", kind: "request", acp: false, typed: false }, + { name: "_acpmux/secret_submit", kind: "request", acp: false, typed: true }, { name: "_acpmux/secret_run", kind: "request", acp: false, typed: false }, { name: "_acpmux/mcp_connect_request", kind: "request", acp: false, typed: false }, { name: "_acpmux/mcp_connect_answer", kind: "request", acp: false, typed: false }, @@ -525,24 +525,24 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/transfer_read", kind: "request", acp: false, typed: false }, { name: "_acpmux/transfer_receive", kind: "request", acp: false, typed: false }, { name: "_acpmux/transfer_write", kind: "request", acp: false, typed: false }, - { name: "_acpmux/handoff_prepare", kind: "request", acp: false, typed: false }, - { name: "_acpmux/handoff_get", kind: "request", acp: false, typed: false }, - { name: "_acpmux/handoff_draft", kind: "request", acp: false, typed: false }, - { name: "_acpmux/handoff_start", kind: "request", acp: false, typed: false }, - { name: "_acpmux/handoff_discard", kind: "request", acp: false, typed: false }, - { name: "_acpmux/harnesses", kind: "request", acp: false, typed: false }, - { name: "_acpmux/harness_enable", kind: "request", acp: false, typed: false }, + { name: "_acpmux/handoff_prepare", kind: "request", acp: false, typed: true }, + { name: "_acpmux/handoff_get", kind: "request", acp: false, typed: true }, + { name: "_acpmux/handoff_draft", kind: "request", acp: false, typed: true }, + { name: "_acpmux/handoff_start", kind: "request", acp: false, typed: true }, + { name: "_acpmux/handoff_discard", kind: "request", acp: false, typed: true }, + { name: "_acpmux/harnesses", kind: "request", acp: false, typed: true }, + { name: "_acpmux/harness_enable", kind: "request", acp: false, typed: true }, { name: "_acpmux/harness/add", kind: "request", acp: false, typed: false }, { name: "_acpmux/harness/remove", kind: "request", acp: false, typed: false }, { name: "_acpmux/harness/restore", kind: "request", acp: false, typed: false }, { name: "_acpmux/harness/doctor", kind: "request", acp: false, typed: false }, { name: "_acpmux/harness/secret/set", kind: "request", acp: false, typed: false }, { name: "_acpmux/harness/icon", kind: "request", acp: false, typed: false }, - { name: "_acpmux/harness/health", kind: "request", acp: false, typed: false }, - { name: "_acpmux/agent_login/start", kind: "request", acp: false, typed: false }, - { name: "_acpmux/agent_login/finish", kind: "request", acp: false, typed: false }, - { name: "_acpmux/agent_login/cancel", kind: "request", acp: false, typed: false }, - { name: "_acpmux/agent_login/api_key", kind: "request", acp: false, typed: false }, + { name: "_acpmux/harness/health", kind: "request", acp: false, typed: true }, + { name: "_acpmux/agent_login/start", kind: "request", acp: false, typed: true }, + { name: "_acpmux/agent_login/finish", kind: "request", acp: false, typed: true }, + { name: "_acpmux/agent_login/cancel", kind: "request", acp: false, typed: true }, + { name: "_acpmux/agent_login/api_key", kind: "request", acp: false, typed: true }, { name: "_acpmux/harness/inventory/watch", kind: "request", acp: false, typed: true }, { name: "_acpmux/harness/inventory/refresh", kind: "request", acp: false, typed: true }, { name: "_acpmux/harness/update/plan", kind: "request", acp: false, typed: true }, @@ -552,9 +552,9 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/registry", kind: "request", acp: false, typed: false }, { name: "_acpmux/defaults", kind: "request", acp: false, typed: false }, { name: "_acpmux/presets", kind: "request", acp: false, typed: false }, - { name: "_acpmux/models", kind: "request", acp: false, typed: false }, + { name: "_acpmux/models", kind: "request", acp: false, typed: true }, { name: "_acpmux/directories", kind: "request", acp: false, typed: false }, - { name: "_acpmux/route/list", kind: "request", acp: false, typed: false }, + { name: "_acpmux/route/list", kind: "request", acp: false, typed: true }, { name: "_acpmux/route/show", kind: "request", acp: false, typed: false }, { name: "_acpmux/route/add", kind: "request", acp: false, typed: false }, { name: "_acpmux/route/edit", kind: "request", acp: false, typed: false }, @@ -562,7 +562,7 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/route/restore", kind: "request", acp: false, typed: false }, { name: "_acpmux/route/test", kind: "request", acp: false, typed: false }, { name: "_acpmux/route/default.set", kind: "request", acp: false, typed: false }, - { name: "_acpmux/chat/route.set", kind: "request", acp: false, typed: false }, + { name: "_acpmux/chat/route.set", kind: "request", acp: false, typed: true }, { name: "_acpmux/accounts", kind: "request", acp: false, typed: false }, { name: "_acpmux/account/set", kind: "request", acp: false, typed: false }, { name: "_acpmux/account/usage", kind: "request", acp: false, typed: false }, @@ -572,7 +572,7 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/schema", kind: "request", acp: false, typed: false }, { name: "_acpmux/protocol", kind: "request", acp: false, typed: true }, { name: "_acpmux/shutdown", kind: "request", acp: false, typed: false }, - { name: "_acpmux/peers", kind: "request", acp: false, typed: false }, + { name: "_acpmux/peers", kind: "request", acp: false, typed: true }, { name: "_acpmux/peer_status", kind: "request", acp: false, typed: false }, { name: "_acpmux/host_health", kind: "request", acp: false, typed: false }, { name: "_acpmux/vm_host", kind: "request", acp: false, typed: false }, @@ -580,9 +580,9 @@ export const ACPMUX_METHODS: ReadonlyArray<{ name: string; kind: AcpmuxMethodKin { name: "_acpmux/place", kind: "request", acp: false, typed: false }, { name: "_acpmux/peer_add", kind: "request", acp: false, typed: false }, { name: "_acpmux/peer_lease", kind: "request", acp: false, typed: false }, - { name: "_acpmux/peer_reconnect", kind: "request", acp: false, typed: false }, + { name: "_acpmux/peer_reconnect", kind: "request", acp: false, typed: true }, { name: "_acpmux/peer_remove", kind: "request", acp: false, typed: false }, - { name: "_acpmux/peer_snapshot", kind: "request", acp: false, typed: false }, + { name: "_acpmux/peer_snapshot", kind: "request", acp: false, typed: true }, { name: "_acpmux/peer_snapshots_clear", kind: "request", acp: false, typed: false }, { name: "_acpmux/pair_ticket_new", kind: "request", acp: false, typed: false }, { name: "_acpmux/pair_list", kind: "request", acp: false, typed: false }, diff --git a/webviews/src/agent-session/acpmux/protocol/generated/types.ts b/webviews/src/agent-session/acpmux/protocol/generated/types.ts index dcc1cc3c2eca..e6ab261dc334 100644 --- a/webviews/src/agent-session/acpmux/protocol/generated/types.ts +++ b/webviews/src/agent-session/acpmux/protocol/generated/types.ts @@ -1,5 +1,114 @@ // @generated by webviews/scripts/acpmux-protocol-codegen.ts from cmux-tui/crates/acpmux/spec/acpmux-protocol.json. Do not edit; rerun the codegen. +export interface AcpmuxRequestMeta { + adopt?: unknown; + agentSessionId?: unknown; + answeredBy?: unknown; + approverDecision?: unknown; + approverDecisions?: unknown; + argv?: unknown; + author?: unknown; + beforePromptId?: unknown; + callerSession?: unknown; + cliHost?: unknown; + context?: unknown; + cwd?: unknown; + decisionNonce?: unknown; + dispatchSource?: unknown; + env?: unknown; + ephemeral?: unknown; + forceAdmit?: unknown; + forwardProjection?: unknown; + fromAgent?: unknown; + fromHarness?: unknown; + fromSessionId?: unknown; + githubBroker?: unknown; + harness?: unknown; + harnessFrom?: unknown; + host?: unknown; + inlineParts?: unknown; + interrupt?: unknown; + localAppToken?: unknown; + mcpServers?: unknown; + messageId?: unknown; + messageSender?: unknown; + mode?: unknown; + model?: unknown; + name?: unknown; + notice?: unknown; + observer?: unknown; + parent?: unknown; + peer?: unknown; + peerHops?: unknown; + personDecision?: unknown; + personKey?: unknown; + personStop?: unknown; + policy?: unknown; + pr?: unknown; + preset?: unknown; + promptFileText?: unknown; + promptId?: unknown; + promptSuggestion?: unknown; + provenance?: unknown; + reportTo?: unknown; + requestId?: unknown; + requireFullAccess?: unknown; + resend?: unknown; + route?: unknown; + spawnToolCallId?: unknown; + startLater?: unknown; + steer?: unknown; + steerNow?: unknown; + steerOnly?: unknown; + supersede?: unknown; + surface?: unknown; + teamMessage?: unknown; + throughSeq?: unknown; + via?: unknown; + vmBoundary?: unknown; + waitStart?: unknown; + workflowAllowedTools?: unknown; + workspaceId?: unknown; +} + +export interface AgentHoldParams { + _meta?: RequestMeta; + held?: unknown; + sessionId?: unknown; +} + +export interface AgentLoginApiKeyParams { + _meta?: RequestMeta; + agent?: unknown; + key?: unknown; + sessionId?: unknown; +} + +export interface AgentLoginCancelParams { + _meta?: RequestMeta; + loginId?: unknown; + sessionId?: unknown; +} + +export interface AgentLoginFinishParams { + _meta?: RequestMeta; + code?: unknown; + loginId?: unknown; + sessionId?: unknown; +} + +export interface AgentLoginStartParams { + _meta?: RequestMeta; + agent?: unknown; + sessionId?: unknown; +} + +export interface AgentPairResumeParams { + _meta?: RequestMeta; + peerSessionId?: unknown; + sessionId?: unknown; +} + export type AllowanceExpiry = "session_stop_or_daemon_restart"; /** @@ -30,6 +139,16 @@ export interface AnsweredBy { feedItem: string; } +export interface AppReplyParams { + _meta?: RequestMeta; + replyId?: unknown; + replyTo?: unknown; + sessionId?: unknown; + text?: unknown; + threadId?: unknown; + to?: unknown; +} + /** * `{approver, expiresAt, signature}`: ECDSA P-256 / SHA-256, raw `r || s` as * lowercase hex, over the approver's message for this exact answer. @@ -41,6 +160,12 @@ export interface ApproverDecision { signature: string; } +export interface ArchiveParams { + _meta?: RequestMeta; + archived?: unknown; + sessionId?: unknown; +} + export interface AttachParams { /** Page forward from here (oldest first) when `beforeSeq` is absent. */ afterSeq?: (number | null); @@ -86,6 +211,12 @@ export type AttachResult = { turnOpener: (SessionEvent | null); } & Record; +export interface AttachmentParams { + _meta?: RequestMeta; + id?: unknown; + sessionId?: unknown; +} + /** * The person a request comes from (`hub/author.rs`): stamped by the daemon that admitted * them, and accepted only from the unix socket or a peer forwarding for its own. @@ -116,6 +247,26 @@ export type AuthorMetaFields = { /** Whether a harness's autocompact is capped (`UsageStats.autocompact`). */ export type Autocompact = ("capped" | "none" | "unknown"); +export interface BookmarkGetParams { + _meta?: RequestMeta; + sessionId?: unknown; +} + +export interface BookmarkSetParams { + _meta?: RequestMeta; + bookmarked?: unknown; + seq?: unknown; + sessionId?: unknown; +} + +export interface BtwParams { + _meta?: RequestMeta; + inline?: unknown; + model?: unknown; + question?: unknown; + sessionId?: unknown; +} + export type ChangedSession = (SessionSummary | PurgedSession); export type ChatAllowance = { @@ -123,6 +274,13 @@ export type ChatAllowance = { expires: AllowanceExpiry; } & Record; +export interface ChatRouteSetParams { + _meta?: RequestMeta; + routeId?: unknown; + sessionId?: unknown; + when?: unknown; +} + export type CoverageLabel = "acp_requests_only"; export type DeliveryReceipt = { @@ -137,6 +295,17 @@ export type DeliveryState = "queued" | "acknowledged"; /** An acknowledgement with nothing in it. */ export type Done = Record; +export interface DraftGetParams { + _meta?: RequestMeta; + sessionId?: unknown; +} + +export interface DraftSetParams { + _meta?: RequestMeta; + sessionId?: unknown; + text?: unknown; +} + /** What limits the session's agent as far as acpmux can tell. */ export type Enforcement = { detail: string; @@ -214,7 +383,7 @@ export type EventsResult = { * `acp.session.fork`: a completed-turn, live, or before-prompt fork. * Before-prompt requests require requestId and omit both throughSeq and live. */ -export type ForkParams = { +export interface ForkParams { _meta?: unknown; /** Recorded prompt identity, nonempty and at most 512 UTF-8 bytes. Claude/Codex only. */ beforePromptId?: (string | null); @@ -223,14 +392,89 @@ export type ForkParams = { requestId?: (string | null); sessionId: string; throughSeq?: (number | null); -} & Record; +} + +export interface ForumRulesParams { + _meta?: RequestMeta; +} + +export interface HandoffDiscardParams { + _meta?: RequestMeta; + handoffId?: unknown; +} + +export interface HandoffDraftParams { + _meta?: RequestMeta; + capsule?: unknown; + checkpoint?: unknown; + draftKey?: unknown; + handoffId?: unknown; + revision?: unknown; +} + +export interface HandoffGetParams { + _meta?: RequestMeta; + handoffId?: unknown; + sessionId?: unknown; +} + +export interface HandoffPrepareParams { + _meta?: RequestMeta; + handoffKey?: unknown; + harness?: unknown; + memoryRefs?: unknown; + sessionId?: unknown; +} + +export interface HandoffStartParams { + _meta?: RequestMeta; + capsule?: unknown; + checkpoint?: unknown; + handoffId?: unknown; + promptId?: unknown; + revision?: unknown; +} + +export interface HarnessEnableParams { + _meta?: RequestMeta; + folder?: unknown; + id?: unknown; +} + +export interface HarnessHealthParams { + _meta?: RequestMeta; + autoUpdate?: unknown; + checkLatest?: unknown; + refresh?: unknown; + rescan?: unknown; + view?: unknown; +} export interface HarnessUpdatePlanParams { cli: string; } +export interface HarnessesParams { + _meta?: RequestMeta; + cwd?: unknown; +} + +export interface InboxThreadParams { + _meta?: RequestMeta; + markRead?: unknown; + sessionId?: unknown; + threadId?: unknown; +} + export type Indicator = "input" | "working" | "failed" | "lost" | "done"; +export interface InitializeParams { + _meta?: RequestMeta; + clientCapabilities?: unknown; + clientInfo?: unknown; + protocolVersion?: unknown; +} + export type InventoryRefreshParams = Record; export interface InventoryWatchParams { @@ -240,6 +484,12 @@ export interface InventoryWatchParams { /** acpmux never claims host isolation. */ export type Isolation = "unverified"; +export interface KillParams { + _meta?: RequestMeta; + purge?: unknown; + sessionId?: unknown; +} + /** The session's lane (cx-v9k2f), decided once by the daemon (`views::lane`). */ export type Lane = { /** The failed or lost turn's error text, secrets masked (`secret_scan`). */ @@ -374,6 +624,12 @@ export interface MailWaitParams { timeoutMs?: (number | null); } +export interface McpReconnectParams { + _meta?: RequestMeta; + server?: unknown; + sessionId?: unknown; +} + /** `failed` also for one that waits on a sign-in (Claude Code `needs-auth`). */ export type McpServerStatus = "connected" | "failed" | "pending"; @@ -406,6 +662,27 @@ export type MessageSender = { sessionId: string; } & Record; +export interface ModelsParams { + _meta?: RequestMeta; + probe?: unknown; + refresh?: unknown; +} + +export interface NativeHistoryParams { + _meta?: RequestMeta; + before?: unknown; + mark?: unknown; + sessionId?: unknown; +} + +export interface OfferRespondParams { + _meta?: RequestMeta; + childSessionId?: unknown; + offerId?: unknown; + outcome?: unknown; + sessionId?: unknown; +} + /** * One person in a chat. The hub never marks the caller's own entry: a connection may carry * several people, so the client compares ids (a caller with no author is `owner`). @@ -422,6 +699,21 @@ export type Participant = { /** A peer link's state (`peer_link.rs` `PeerState`; `state` in `_acpmux/peers`). */ export type PeerLinkState = ("connecting" | "connected" | "degraded" | "reconnecting" | "offline"); +export interface PeerReconnectParams { + _meta?: RequestMeta; + force?: unknown; + name?: unknown; + wait?: unknown; +} + +export interface PeerSnapshotParams { + _meta?: RequestMeta; + host?: unknown; + limit?: unknown; + maxBytes?: unknown; + sessionId?: unknown; +} + /** * `_acpmux/peer_state_changed`, to every watching connection (`_acpmux/watch`) when a peer's * state changes, never twice for the same state: a peer that is added sends its first state, @@ -440,6 +732,10 @@ export type PeerStateChanged = { state: PeerLinkState; } & Record; +export interface PeersParams { + _meta?: RequestMeta; +} + export type PermissionBatching = { maxItems: number; maxPendingGroups: number; @@ -514,7 +810,7 @@ export type PermissionGroupReceipt = { } & Record; export interface PermissionGroupRespondParams { - _meta?: (RequestMeta | null); + _meta?: (RequestMeta2 | null); decision: PermissionDecision; /** 1 to 256 bytes; the same key with the same body replays the receipt. */ decisionKey: string; @@ -556,7 +852,7 @@ export type PermissionPending = { } & Record; export interface PermissionRespondParams { - _meta?: (RequestMeta | null); + _meta?: (RequestMeta2 | null); /** Claude Code AskUserQuestion annotations beside the answers, by question text: `{preview?, notes?}`. */ annotations?: unknown; /** A question's answers, by question. */ @@ -579,6 +875,13 @@ export type PresenceNote = { sessionId: string; } & Record; +export interface PrewarmParams { + _meta?: RequestMeta; + cwd?: unknown; + harness?: unknown; + release?: unknown; +} + /** * `_acpmux/process/background` (Ctrl-B): the chat whose running work goes * to the background, and the one tool call to move (none: every @@ -600,6 +903,28 @@ export type ProcessBackgroundResult = { backgrounded: boolean; } & Record; +export interface ProcessKillParams { + _meta?: RequestMeta; + processId?: unknown; + sessionId?: unknown; + signal?: unknown; +} + +export interface ProcessListParams { + _meta?: RequestMeta; + cursor?: unknown; + limit?: unknown; + sessionId?: unknown; +} + +export interface ProcessTailParams { + _meta?: RequestMeta; + fromByte?: unknown; + maxBytes?: unknown; + processId?: unknown; + sessionId?: unknown; +} + /** What another agent's message asks of the session it goes to. */ export type PromptClass = ("block" | "question" | "result" | "fyi"); @@ -611,6 +936,42 @@ export type PurgedSession = { sessionId: string; } & Record; +export interface QueueApproveParams { + _meta?: RequestMeta; + promptId?: unknown; + revision?: unknown; + sessionId?: unknown; + turnId?: unknown; +} + +export interface QueueDeclineParams { + _meta?: RequestMeta; + promptId?: unknown; + sessionId?: unknown; +} + +export interface QueueEditParams { + _meta?: RequestMeta; + images?: unknown; + index?: unknown; + op?: unknown; + promptId?: unknown; + sessionId?: unknown; + text?: unknown; +} + +export interface QueueGetParams { + _meta?: RequestMeta; + promptId?: unknown; + sessionId?: unknown; +} + +export interface QueueRemoveParams { + _meta?: RequestMeta; + promptId?: unknown; + sessionId?: unknown; +} + /** The person who queued a prompt in a shared chat. */ export type QueuedAuthor = { /** `tailnet:` or `user_…`. */ @@ -705,7 +1066,11 @@ export type QueuedSubagentRetry = { name: (string | null); } & Record; -export interface RequestMeta { +export type RequestMeta = { + acpmux?: AcpmuxRequestMeta; +} & Record; + +export interface RequestMeta2 { acpmux?: (AnswerMeta | null); /** * The app's gesture ticket for this exact pick (`transport.gesture`); @@ -714,6 +1079,13 @@ export interface RequestMeta { cmuxGesture?: (string | null); } +export interface ResolveParams { + _meta?: RequestMeta; + exactSessionId?: unknown; + query?: unknown; + sessionId?: unknown; +} + /** What `_acpmux/retry` retried: the chat's own turn, or one of its subagents. */ export type RetryKind = "turn" | "subagent"; @@ -763,6 +1135,18 @@ export type RetrySummary = { turnId: string; } & Record; +export interface RouteListParams { + _meta?: RequestMeta; + sessionId?: unknown; +} + +export interface SecretSubmitParams { + _meta?: RequestMeta; + secretId?: unknown; + sessionId?: unknown; + value?: unknown; +} + /** * `terminal`: run `command` in a terminal on `host` (null: this machine). `route`: edit or * sign in again to the route `routeId`. @@ -781,11 +1165,11 @@ export type SessionAuthLoginKind = "terminal" | "route"; * send). Exactly one of the two. `refresh`: probe the route and run the harness's own sign-in * check that reads no keychain, when no newer evidence answers. */ -export type SessionAuthParams = { +export interface SessionAuthParams { refresh?: boolean; sessionId?: (string | null); target?: (SessionAuthTarget | null); -} & Record; +} /** * What `_acpmux/session/auth` answers. A client blocks a send only on `signedOut`, `expired` @@ -829,6 +1213,11 @@ export type SessionAuthTarget = { routeId?: (string | null); } & Record; +export interface SessionCancelParams { + _meta?: RequestMeta; + sessionId?: unknown; +} + /** * To watchers when a session changed: its new summary (only its id when * purged), the coarse `kind` and the log record behind it. @@ -1079,10 +1468,43 @@ export type SessionEvent = { sessionId: string; } & Record; +export interface SessionNewParams { + _meta?: RequestMeta; + additionalDirectories?: unknown; + cwd?: unknown; + mcpServers?: unknown; +} + +export interface SessionPromptParams { + _meta?: RequestMeta; + prompt?: unknown; + sessionId?: unknown; + steer?: unknown; +} + export interface SessionRef { sessionId: string; } +export interface SessionSetConfigOptionParams { + _meta?: RequestMeta; + configId?: unknown; + sessionId?: unknown; + value?: unknown; +} + +export interface SessionSetModeParams { + _meta?: RequestMeta; + modeId?: unknown; + sessionId?: unknown; +} + +export interface SessionSetModelParams { + _meta?: RequestMeta; + modelId?: unknown; + sessionId?: unknown; +} + /** * One session as every list shows it (`Hub::session_summary`). A peer's * session arrives as the peer daemon's own summary plus `peer`, with @@ -1346,8 +1768,55 @@ export type SessionsResult = { unchanged?: (number | null); } & Record; +export interface SetToolsParams { + _meta?: RequestMeta; + enabled?: unknown; + id?: unknown; + sessionId?: unknown; +} + +export interface SideParams { + _meta?: RequestMeta; + chat?: unknown; + sessionId?: unknown; +} + +export interface StartModeParams { + _meta?: RequestMeta; + cwd?: unknown; + harness?: unknown; + preset?: unknown; +} + +export interface StatusParams { + _meta?: RequestMeta; +} + export type StuckReason = "harness_unavailable" | "capacity" | "auth" | "spawn_failed"; +export interface SubagentStopParams { + _meta?: RequestMeta; + sessionId?: unknown; + subagentSessionId?: unknown; +} + +export interface SubagentTranscriptParams { + _meta?: RequestMeta; + after?: unknown; + agentId?: unknown; + before?: unknown; + limit?: unknown; + runId?: unknown; + sessionId?: unknown; +} + +export interface SubsessionLimitsParams { + _meta?: RequestMeta; + maxRunning?: unknown; + sessionId?: unknown; + workspaceId?: unknown; +} + /** One row of a chat's tool list (`store::ToolRow`). */ export type ToolSummary = { /** `_acpmux/set_tools` can turn it on or off in this chat. */ @@ -1362,6 +1831,21 @@ export type ToolSummary = { source: string; } & Record; +export interface TrustGetParams { + _meta?: RequestMeta; + cwd?: unknown; + peer?: unknown; + sessionId?: unknown; +} + +export interface TrustSetParams { + _meta?: RequestMeta; + cwd?: unknown; + level?: unknown; + peer?: unknown; + sessionId?: unknown; +} + /** The running turn. */ export type TurnSummary = { /** @@ -1467,6 +1951,12 @@ export interface ViewParams { visible: boolean; } +export interface WarmParams { + _meta?: RequestMeta; + limit?: unknown; + sessionIds?: unknown; +} + export interface WatchParams { cursor?: (string | null); /** @@ -1484,3 +1974,9 @@ export interface WatchParams { /** Include tool inventories explicitly; default census rows omit them. */ withTools?: (boolean | null); } + +export interface WatchTakeOverParams { + _meta?: RequestMeta; + ifLive?: unknown; + sessionId?: unknown; +} From b9d5d1abda5991ee5bee553f3ac31fe78bf2f92d Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 5 Nov 2026 13:19:52 -0800 Subject: [PATCH 6/8] swift-acpmux-protocol: list the app-only noProject and the reply-side ownerCloud and features keys Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --- scripts/ci/swift-acpmux-protocol-allowlist.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/ci/swift-acpmux-protocol-allowlist.txt b/scripts/ci/swift-acpmux-protocol-allowlist.txt index ff4e0058a01a..75adcb8a7492 100644 --- a/scripts/ci/swift-acpmux-protocol-allowlist.txt +++ b/scripts/ci/swift-acpmux-protocol-allowlist.txt @@ -3,3 +3,6 @@ # entry is a param key the agent pane's knownParams lets through that the method's typed params # lack. The check fails on a listed name that is no longer used or that the protocol now defines. acp.trust.peer-v1 an initialize extension name acpmux advertises in _meta.acpmux.extensions (AcpmuxPaneSocket), not a method +_meta.acpmux#noProject the pane's own key (Do not work in a project): AcpmuxPathPolicy reads and strips it before the request reaches acpmux +_meta.acpmux#ownerCloud a reply key: RemoteAcpmuxWire answers initialize as the remote daemon, and acpmux's own initialize reply carries it too +_meta.acpmux#features a reply key: RemoteAcpmuxWire answers initialize as the remote daemon (agent_login), as acpmux's own initialize reply does From 97e8c40794a9980d4689b4fec8a784d6ad6d8ddb Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 5 Nov 2026 13:44:05 -0800 Subject: [PATCH 7/8] cli app.rs: rustfmt Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --- cmux-tui/crates/cmux-tui/src/cli/app.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmux-tui/crates/cmux-tui/src/cli/app.rs b/cmux-tui/crates/cmux-tui/src/cli/app.rs index 7e3905ecf33c..259be14ad6ed 100644 --- a/cmux-tui/crates/cmux-tui/src/cli/app.rs +++ b/cmux-tui/crates/cmux-tui/src/cli/app.rs @@ -555,7 +555,8 @@ pub(super) fn run_action( // with an optional `true`/`false` after it; the run's own focus follows it. let mut value = true; if super::action_hint::action_takes_argument(action, "focus") { - if let Some(word @ ("true" | "false")) = args.get(index + 1).map(String::as_str) { + if let Some(word @ ("true" | "false")) = args.get(index + 1).map(String::as_str) + { value = word == "true"; index += 1; } From a9143c8ced1c239a56f58561b06a191702e3c3ad Mon Sep 17 00:00:00 2001 From: Leo Li Date: Thu, 5 Nov 2026 15:13:03 -0800 Subject: [PATCH 8/8] action_hint: read the catalog with get(), no indexing Co-Authored-By: Claude Opus 5.5 Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5 --- cmux-tui/crates/cmux-tui/src/cli/action_hint.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cmux-tui/crates/cmux-tui/src/cli/action_hint.rs b/cmux-tui/crates/cmux-tui/src/cli/action_hint.rs index 1b9000f58e21..fab14265bed9 100644 --- a/cmux-tui/crates/cmux-tui/src/cli/action_hint.rs +++ b/cmux-tui/crates/cmux-tui/src/cli/action_hint.rs @@ -39,17 +39,16 @@ pub(super) fn action_takes_argument(action: &str, argument: &str) -> bool { ARGS.get_or_init(|| { let doc: Value = serde_json::from_str(ACTIONS_JSON).unwrap_or(Value::Null); let mut map = HashMap::new(); - for action in doc["actions"].as_array().into_iter().flatten() { - let names: Vec = action["args"] - .as_array() + for action in doc.get("actions").and_then(Value::as_array).into_iter().flatten() { + let names: Vec = action + .get("args") + .and_then(Value::as_array) .into_iter() .flatten() - .filter_map(|arg| arg["name"].as_str().map(str::to_owned)) + .filter_map(|arg| arg.get("name").and_then(Value::as_str).map(str::to_owned)) .collect(); - for key in [&action["id"], &action["cli_name"]] { - if let Some(key) = key.as_str() { - map.insert(key.to_owned(), names.clone()); - } + for key in ["id", "cli_name"].into_iter().filter_map(|key| action.get(key)?.as_str()) { + map.insert(key.to_owned(), names.clone()); } } map