Skip to content

acpmux: add authenticated Cloud trigger chat delivery - #30526

Merged
teamleaderleo merged 40 commits into
feat-cmux-nextfrom
lane/cloud-trigger-chat-auth
Nov 1, 2026
Merged

teamleaderleo merged 40 commits into
feat-cmux-nextfrom
lane/cloud-trigger-chat-auth

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Implements authenticated Cloud trigger chat delivery on acpmux with fixed session identities, scoped RPCs, durable receipts, quiet turn isolation, restart recovery, and purge-safe reuse.

The daemon now validates host identity and trigger scope on every operation, fences readback to the exact durable turn and prompt digest, rejects collisions and stale imports, and exposes the trigger-chat CLI path. The test coverage exercises authorization, fixed-id lifecycle, collision and expiry behavior, restart/readback recovery, delivery receipts, quiet notifications, and malformed credentials.

This is one cohesive security and lifecycle change: authorization must bind the delivery to a fixed chat, the journal must attest that exact prompt and turn, and recovery/quiet/purge handling must preserve those same boundaries. Splitting those paths into independently shipped changes would leave incomplete enforcement.

Base integration preserves sender metadata, human-turn admission/interrupt protection, and inbox release. It also restores the prompt_sender module registration that an earlier merge lost, which caused the Windows compile error.

Authorization is pinned by build configuration to the existing environment/origin table and defaults to production. Release builds reject development and staging before network access. HTTPS certificate/hostname validation, no redirects, and no environment proxy constrain the authority that can attest each active principal. Debug acceptance artifacts explicitly select development at compile time. An enrolled Linux guest with the new root host identity IPC is required; non-Linux trigger authorization fails closed, and older guests without an identity receipt need fresh enrollment.

Validation at a3aac43 passed in https://github.com/manaflow-ai/cmux/actions/runs/41290619157: authority 2/2, trigger chat 10/10, durable delivery 10/10, quiet 5/5, fixed IDs 7/7, background turns 8/8, purge 1/1, sender metadata 3/3, agent inbox 19/19, release authority refusal 3/3. The previously failing human-steer test now passes, and the Linux cmux-tui build passed.

That clippy run exposed three warnings (unused accessor and two owned path comparisons). Final head 68bfabb fixes only those warnings. Strict cargo clippy -p acpmux --lib -- -D warnings, the same suites, and a replacement build/artifact are running in https://github.com/manaflow-ai/cmux/actions/runs/41291157112. Output includes the stripped executable, source snapshot SHA, PR head, SHA-256 digest, compile-time authority choice, file type, and required runtime libraries.

Agent session: agent session

teamleaderleo and others added 30 commits November 1, 2026 07:00
… it is quiet (#30228)

* acpmux: a turn the person did not start never pings, and says whether it is quiet

Claude Code runs turns of its own (a task notification, a scheduled wakeup or
loop tick, a hook). acpmux now brackets each with background_turn records,
keeps its words off the chat's preview, and marks it quiet when its reply is
one short line with no question and no link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5

* acpmux: rustfmt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5

* acpmux: background_turn reaches transcript readers; a turn met mid-way after a restart and a long reply are handled

Review of 56303e2 (router-review, gpt-6.1-sol): the records were left out of
TRANSCRIPT_MUX_KINDS, so the pane never heard them. Also: model work with no
acpmux turn and no system/init seen (a daemon restart mid-turn) starts a
background turn, and a reply past the kept text is never quiet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5

* acpmux: rustfmt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (claude-opus-5-5 medium, cloud-reviewer): SHIP at 68bfabb

Delta review over my SHIP at a3aac43. The one new commit, 68bfabb ("clear trigger integration clippy warnings"), is lint only:

  • server/mod.rs: removes the unused Conn::trigger_scope() accessor. Nothing calls it at this head, and admission still goes through the exact-scope admit path below it.
  • server/trigger_chat.rs bound_session and ensure: meta.cwd != PathBuf::from(&scope.cwd) becomes meta.cwd != Path::new(&scope.cwd). The comparison is the same (PathBuf: PartialEq<&Path>) without an allocation, so the cwd, harness and workspace binding checks are unchanged. PathBuf is still used elsewhere in the file, so the import stays.

Authority, the restored persons and sender behaviour, the human-steer guard and the transport are all unchanged, so the a3aac43 review carries over in full. The strict clippy run and the artifact run for this head are still in progress; any red there belongs to this PR.

Follow-ups: carried unchanged from the a3aac43 receipt:

  • release trigger authorization against the dev-joined DEFAULT_HUB: check end to end on nightly;
  • "pinned TLS" wording;
  • the release-mode rustc --test run in CI;
  • bounding the trigger_delivery scan;
  • mock-authority and serve_connection refusal tests;
  • the 4 s retry doc;
  • non-Linux identity().

@cursor

cursor Bot commented Nov 1, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmux-tui/crates/acpmux/src/hub/log_load.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/hub/log_load.rs:318">
P2: Make this recovery idempotent before appending the terminal background marker. A crash after shutdown/cancellation has persisted `ended` but before `turn_result` causes each restart to append another `ended` event for the same trigger turn, producing an invalid duplicate lifecycle bracket.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/server/mod.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/server/mod.rs:1621">
P1: This check does not close the connection's scope race: the trigger handler is spawned asynchronously and only sets `trigger` after a network authorization await, so a client can pipeline an ordinary request or notification that runs while `is_trigger()` is still false. Mark the connection as trigger-pending and serialize or reject all non-trigger work until admission succeeds; apply the same fence to already-admitted notification work.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/prompt_provenance.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/prompt_provenance.rs:596">
P3: This test does not exercise trigger-field validation because `Sender::Local` rejects every trigger provenance before checking the newline. Call `of` with `Sender::Trigger` so the test fails if trigger connections start accepting newline-bearing fields.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/server/trigger_chat.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/server/trigger_chat.rs:366">
P2: A send that takes longer than four seconds to reach admission returns failure, but its detached `prompt_with` task keeps running and can deliver the prompt later. Cancel the task on timeout or keep the operation attached until admission has a definitive outcome.</violation>

<violation number="2" location="cmux-tui/crates/acpmux/src/server/trigger_chat.rs:429">
P1: The identity check ends before the local trigger operation starts, leaving a TOCTOU window during session startup and prompt delivery. Keep the identity fence through the side effect, including a final check at the dispatch point and a check before returning the operation result.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/hub/hosts.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/hub/hosts.rs:1183">
P2: Recovery writes an unknown outcome for an interrupted trigger turn without closing its background bracket. End the trigger background record before recording the unknown outcome, otherwise the durable transcript can leave this trigger running indefinitely.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/hub/background_turn.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/hub/background_turn.rs:185">
P2: `start_trigger_background` leaves `BackgroundTurns::hint` intact when replacing a pending or running background bracket. Clear the hint here, otherwise a later autonomous `system/init` can inherit an old `task_notification` or `scheduled` trigger label.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/hub/prompt_ledger.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/hub/prompt_ledger.rs:83">
P2: Every trigger status poll and send currently replays the entire session journal from sequence zero, so delivery checks become O(log size) and repeatedly parse unrelated history. Use an indexed or tail-bounded readback for the delivery instead of rescanning the complete log.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/cli/trigger_chat.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/cli/trigger_chat.rs:70">
P1: `trigger-chat` always reports the current daemon as unsupported because `daemon_lists` reads `features`, while `trigger-chat-v1` is advertised under `capabilities` and is absent from `features`. Read the initialize capability IDs (or add this ID to the feature list) before rejecting the operation.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

// principal it is a single-purpose channel. In particular,
// the local socket must not keep granting ordinary person
// RPCs after trigger authentication.
if conn.is_trigger() && !m.starts_with("trigger/chat.") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This check does not close the connection's scope race: the trigger handler is spawned asynchronously and only sets trigger after a network authorization await, so a client can pipeline an ordinary request or notification that runs while is_trigger() is still false. Mark the connection as trigger-pending and serialize or reject all non-trigger work until admission succeeds; apply the same fence to already-admitted notification work.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/mod.rs, line 1621:

<comment>This check does not close the connection's scope race: the trigger handler is spawned asynchronously and only sets `trigger` after a network authorization await, so a client can pipeline an ordinary request or notification that runs while `is_trigger()` is still false. Mark the connection as trigger-pending and serialize or reject all non-trigger work until admission succeeds; apply the same fence to already-admitted notification work.</comment>

<file context>
@@ -1565,6 +1614,21 @@ async fn serve_connection_from(
+                // principal it is a single-purpose channel. In particular,
+                // the local socket must not keep granting ordinary person
+                // RPCs after trigger authentication.
+                if conn.is_trigger() && !m.starts_with("trigger/chat.") {
+                    conn.send(&Message::err(
+                        id,
</file context>

match expected {
"ensure" => {
same_identity(&identity).await?;
ensure(hub, &scope).await

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The identity check ends before the local trigger operation starts, leaving a TOCTOU window during session startup and prompt delivery. Keep the identity fence through the side effect, including a final check at the dispatch point and a check before returning the operation result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/trigger_chat.rs, line 429:

<comment>The identity check ends before the local trigger operation starts, leaving a TOCTOU window during session startup and prompt delivery. Keep the identity fence through the side effect, including a final check at the dispatch point and a check before returning the operation result.</comment>

<file context>
@@ -0,0 +1,483 @@
+    match expected {
+        "ensure" => {
+            same_identity(&identity).await?;
+            ensure(hub, &scope).await
+        }
+        "send" => {
</file context>

Comment thread cmux-tui/crates/acpmux/src/hub/turns.rs Resolved
})),
..Default::default()
};
tokio::spawn(async move {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A send that takes longer than four seconds to reach admission returns failure, but its detached prompt_with task keeps running and can deliver the prompt later. Cancel the task on timeout or keep the operation attached until admission has a definitive outcome.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/trigger_chat.rs, line 366:

<comment>A send that takes longer than four seconds to reach admission returns failure, but its detached `prompt_with` task keeps running and can deliver the prompt later. Cancel the task on timeout or keep the operation attached until admission has a definitive outcome.</comment>

<file context>
@@ -0,0 +1,483 @@
+        })),
+        ..Default::default()
+    };
+    tokio::spawn(async move {
+        let _ = hub_for_task
+            .prompt_with(
</file context>

let mut closed_window = false;
let mut prompt_hash = None::<String>;
let mut permissions = HashSet::<String>::new();
let scanned = self.store.scan_strict(&session.id, 0, &mut |rec| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Every trigger status poll and send currently replays the entire session journal from sequence zero, so delivery checks become O(log size) and repeatedly parse unrelated history. Use an indexed or tail-bounded readback for the delivery instead of rescanning the complete log.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/hub/prompt_ledger.rs, line 83:

<comment>Every trigger status poll and send currently replays the entire session journal from sequence zero, so delivery checks become O(log size) and repeatedly parse unrelated history. Use an indexed or tail-bounded readback for the delivery instead of rescanning the complete log.</comment>

<file context>
@@ -63,6 +64,182 @@ impl Hub {
+        let mut closed_window = false;
+        let mut prompt_hash = None::<String>;
+        let mut permissions = HashSet::<String>::new();
+        let scanned = self.store.scan_strict(&session.id, 0, &mut |rec| {
+            let field = |key: &str| rec.msg.get(key).and_then(Value::as_str);
+            let mux = rec.dir == "mux";
</file context>

Comment thread cmux-tui/crates/acpmux/src/hub/turns.rs Resolved
let malformed = params(
json!({"kind": "trigger", "triggerId": "tr\n", "deliveryId": "d", "owner": "o", "team": "t", "machine": "m"}),
);
assert!(of(&malformed, Sender::Local).is_err());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This test does not exercise trigger-field validation because Sender::Local rejects every trigger provenance before checking the newline. Call of with Sender::Trigger so the test fails if trigger connections start accepting newline-bearing fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/prompt_provenance.rs, line 596:

<comment>This test does not exercise trigger-field validation because `Sender::Local` rejects every trigger provenance before checking the newline. Call `of` with `Sender::Trigger` so the test fails if trigger connections start accepting newline-bearing fields.</comment>

<file context>
@@ -505,3 +556,43 @@ mod sender_tests {
+        let malformed = params(
+            json!({"kind": "trigger", "triggerId": "tr\n", "deliveryId": "d", "owner": "o", "team": "t", "machine": "m"}),
+        );
+        assert!(of(&malformed, Sender::Local).is_err());
+    }
+}
</file context>
Suggested change
assert!(of(&malformed, Sender::Local).is_err());
assert!(of(&malformed, Sender::Trigger).is_err());

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (acpmux area owner, daemon ownership and turn paths; claude-opus-5-5 subagent plus owner check): BLOCK at 80ade41

The trigger paths themselves meet the HQ#5846 contract: a scoped principal, fail-closed authorize, exact delivery/turn readback, and no auto-approval. The block is a regression in the merge 31a006b (base into the lane), which drops existing behaviour for every session.

Blocking:

  1. [blocking] hub/turns.rs loses acpmux: other agents' messages never go into the person's own turn; they run as a turn of their own after it #30157's person-turn path and chief: agents' messages carry their sender and take the ask floor; tell(session:NAME) as the Chief #30170's sender params. At the merge-base bcaf263, turns.rs has by_person / persons_held / persons_turn_steer_refused (6 hits) and prompt_sender::run_params(&agent_sid, ...) (:869). At this head there are none: .by_person(persons) is gone, so TurnInfo::persons is always false; the "persons" field is gone from turn_started; run_params is replaced by a bare {sessionId, prompt}; and inbox_turn_ended is gone from the cancelled-start path. So inbox notices and other agents' steers fold back into a person's own turn (Leo's "piggyback" ask, fixed by acpmux: other agents' messages never go into the person's own turn; they run as a turn of their own after it #30157), and the now-dead helpers will likely fail clippy -D warnings. Fix: restore the base lines and add the trigger checks beside them.
  2. [blocking, appears once 1 is fixed] hub/steer_end.rs:255-280 now passes held for every steer, and its persons_turn branch is not limited to agent steers. With persons restored, a person's own steer into their own turn would be refused with STEER_NO_TURN. Limit that branch to agent_steer || inbox_notice.

Conditions (acpmux-owner ACK 6554539408):

  1. CLI. Met: one RPC per call, token only on stdin/fd 0, exit 82, TRIGGER_CHAT in CAPABILITIES. The command is a hidden top-level trigger-chat rather than daemon trigger-chat; fine if the platform calls that name.
  2. Principal scope. Met: is_person() is person && !is_trigger(), a trigger connection is refused every non-trigger method, and authorize fails closed (timeout, non-2xx, inactive, oversized), with audience, ops, expiry, machine, session, delivery, cwd and harness all checked.
  3. Durable delivery. Met: digest-conflict refusal, and no second turn after a restart.
  4. Exact readback. Met: exact-turn readback; permissions still ask.
  5. Own turn. Not met (blocking item 1).

Follow-ups (file each one; none blocks):

  • The authority URL is hard-coded to cmux-platform-development.debussy.workers.dev (server/trigger_chat.rs:195). It should come from config, with a production default.
  • Two concurrent sends with one delivery id both answer "sent". Only one turn runs, but the second should say "replayed".
  • ensure reuses any existing session matching id, cwd, harness and workspace, including a person's chat. Refuse a session that was not created by a trigger.
  • A continuation page with filters on is untested (from HQ#5873 review; unrelated).

ACK withheld until 1 and 2 are fixed. Send the delta (head..fix) and I will re-check just those paths.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 7 new issues found across 39 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmux-tui/crates/acpmux/src/daemon.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/daemon.rs:1175">
P1: This wrapper does not preserve the no-caller guarantee on the alternate-socket fallback: `start_and_connect` probes the lock holder through `Client::connect`, which can send ambient `callerSession` metadata. Pass the no-caller connection strategy through `holder_elsewhere` as well, or make that probe use `Client::connect_without_caller`.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/hub/prompt_ledger.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/hub/prompt_ledger.rs:221">
P2: A terminal trigger can be reported as `attention` instead of `failed`: the pending-permission override runs after the cancellation, refusal, error, and interruption branches. Apply the override only while the turn is still non-terminal, so stale permission requests cannot resurrect a completed or failed receipt.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/hub/lifecycle.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/hub/lifecycle.rs:374">
P2: This unconditional disarm drops the retry path when failed-session cleanup errors, leaving the failed session registered and its fixed ID unavailable. Keep the `CreationGuard` armed until cleanup succeeds, or return cleanup status and disarm only after a successful purge.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/hub/trigger_quiet_tests.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/hub/trigger_quiet_tests.rs:154">
P2: This test ignores `prompt_with`'s result, so a trigger question that fails instead of being cleanly cancelled can still pass. Unwrap the inner RPC result to make the cancellation path part of the assertion.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/hub/withdraw.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/hub/withdraw.rs:171">
P1: This parameter trusts mutable connection-level trigger state rather than the authorization state captured when the request was admitted. Snapshot the trigger admission before spawning the request, or require the trigger scope to be validated for this exact prompt/session before selecting `Sender::Trigger`; otherwise a concurrent in-flight prompt can be treated as an authenticated trigger operation without its scope checks.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/server/requests.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/server/requests.rs:687">
P1: This branch bypasses the normal origin guard for trigger operations, allowing Web/Peer connections with a trigger credential to deliver to the local trigger chat. Restrict these RPCs to the local Unix-socket connection (or apply an explicit equivalent origin check) before dispatching them.</violation>
</file>

<file name="cmux-tui/crates/acpmux/src/server/trigger_chat.rs">

<violation number="1" location="cmux-tui/crates/acpmux/src/server/trigger_chat.rs:285">
P1: This fast path reuses any matching session ID, including a human or stale imported session, because it checks only mutable binding fields and no trigger ownership marker. Persist and verify trigger ownership before reusing a session; otherwise reject the collision so trigger delivery cannot enter a human-facing chat.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 8 unresolved issues already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

pub async fn connect_without_caller(autostart: bool) -> Result<Arc<Client>> {
let home = home();
let path = socket_path();
connect_with_systemctl("systemctl", &home, &path, autostart, || {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This wrapper does not preserve the no-caller guarantee on the alternate-socket fallback: start_and_connect probes the lock holder through Client::connect, which can send ambient callerSession metadata. Pass the no-caller connection strategy through holder_elsewhere as well, or make that probe use Client::connect_without_caller.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/daemon.rs, line 1175:

<comment>This wrapper does not preserve the no-caller guarantee on the alternate-socket fallback: `start_and_connect` probes the lock holder through `Client::connect`, which can send ambient `callerSession` metadata. Pass the no-caller connection strategy through `holder_elsewhere` as well, or make that probe use `Client::connect_without_caller`.</comment>

<file context>
@@ -1167,6 +1167,17 @@ pub async fn connect(autostart: bool) -> Result<Arc<Client>> {
+pub async fn connect_without_caller(autostart: bool) -> Result<Arc<Client>> {
+    let home = home();
+    let path = socket_path();
+    connect_with_systemctl("systemctl", &home, &path, autostart, || {
+        Client::connect_without_caller(&path)
+    })
</file context>

pub(crate) fn prompt_provenance(
params: &Value,
origin: crate::server::Origin,
trigger: bool,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This parameter trusts mutable connection-level trigger state rather than the authorization state captured when the request was admitted. Snapshot the trigger admission before spawning the request, or require the trigger scope to be validated for this exact prompt/session before selecting Sender::Trigger; otherwise a concurrent in-flight prompt can be treated as an authenticated trigger operation without its scope checks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/hub/withdraw.rs, line 171:

<comment>This parameter trusts mutable connection-level trigger state rather than the authorization state captured when the request was admitted. Snapshot the trigger admission before spawning the request, or require the trigger scope to be validated for this exact prompt/session before selecting `Sender::Trigger`; otherwise a concurrent in-flight prompt can be treated as an authenticated trigger operation without its scope checks.</comment>

<file context>
@@ -168,10 +168,16 @@ impl Hub {
     pub(crate) fn prompt_provenance(
         params: &Value,
         origin: crate::server::Origin,
+        trigger: bool,
     ) -> Result<Option<Value>, RpcError> {
         use crate::prompt_provenance::Sender;
</file context>

"trigger connection accepts trigger/chat operations only",
));
}
if matches!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This branch bypasses the normal origin guard for trigger operations, allowing Web/Peer connections with a trigger credential to deliver to the local trigger chat. Restrict these RPCs to the local Unix-socket connection (or apply an explicit equivalent origin check) before dispatching them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/requests.rs, line 687:

<comment>This branch bypasses the normal origin guard for trigger operations, allowing Web/Peer connections with a trigger credential to deliver to the local trigger chat. Restrict these RPCs to the local Unix-socket connection (or apply an explicit equivalent origin check) before dispatching them.</comment>

<file context>
@@ -678,6 +678,18 @@ pub(super) async fn handle_request(
+            "trigger connection accepts trigger/chat operations only",
+        ));
+    }
+    if matches!(
+        m,
+        method::TRIGGER_CHAT_ENSURE | method::TRIGGER_CHAT_SEND | method::TRIGGER_CHAT_STATUS
</file context>

}

async fn ensure(hub: &Arc<Hub>, scope: &TriggerScope) -> Result<Value, RpcError> {
if let Ok(session) = exact_session(hub, &scope.session) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This fast path reuses any matching session ID, including a human or stale imported session, because it checks only mutable binding fields and no trigger ownership marker. Persist and verify trigger ownership before reusing a session; otherwise reject the collision so trigger delivery cannot enter a human-facing chat.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/server/trigger_chat.rs, line 285:

<comment>This fast path reuses any matching session ID, including a human or stale imported session, because it checks only mutable binding fields and no trigger ownership marker. Persist and verify trigger ownership before reusing a session; otherwise reject the collision so trigger delivery cannot enter a human-facing chat.</comment>

<file context>
@@ -0,0 +1,483 @@
+}
+
+async fn ensure(hub: &Arc<Hub>, scope: &TriggerScope) -> Result<Value, RpcError> {
+    if let Ok(session) = exact_session(hub, &scope.session) {
+        let meta = session.meta();
+        if meta.cwd != PathBuf::from(&scope.cwd)
</file context>

Comment thread cmux-tui/crates/acpmux/src/hub/turns.rs Resolved
// A partial journal is not evidence of completion or of a safe resend.
return json!({"delivery": delivery, "state": "failed", "error": "trigger delivery journal could not be read"});
}
if !permissions.is_empty() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A terminal trigger can be reported as attention instead of failed: the pending-permission override runs after the cancellation, refusal, error, and interruption branches. Apply the override only while the turn is still non-terminal, so stale permission requests cannot resurrect a completed or failed receipt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/hub/prompt_ledger.rs, line 221:

<comment>A terminal trigger can be reported as `attention` instead of `failed`: the pending-permission override runs after the cancellation, refusal, error, and interruption branches. Apply the override only while the turn is still non-terminal, so stale permission requests cannot resurrect a completed or failed receipt.</comment>

<file context>
@@ -63,6 +64,182 @@ impl Hub {
+            // A partial journal is not evidence of completion or of a safe resend.
+            return json!({"delivery": delivery, "state": "failed", "error": "trigger delivery journal could not be read"});
+        }
+        if !permissions.is_empty() {
+            state = "attention";
+        }
</file context>
Suggested change
if !permissions.is_empty() {
if !permissions.is_empty() && matches!(state, "queued" | "running") {

// A session whose agent never started is not left behind, and
// neither is a child that spawned but failed to initialize.
self.discard_failed_creation(&session, created_stops).await;
creation_guard.disarm();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This unconditional disarm drops the retry path when failed-session cleanup errors, leaving the failed session registered and its fixed ID unavailable. Keep the CreationGuard armed until cleanup succeeds, or return cleanup status and disarm only after a successful purge.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/hub/lifecycle.rs, line 374:

<comment>This unconditional disarm drops the retry path when failed-session cleanup errors, leaving the failed session registered and its fixed ID unavailable. Keep the `CreationGuard` armed until cleanup succeeds, or return cleanup status and disarm only after a successful purge.</comment>

<file context>
@@ -336,6 +371,7 @@ impl Hub {
             // A session whose agent never started is not left behind, and
             // neither is a child that spawned but failed to initialize.
             self.discard_failed_creation(&session, created_stops).await;
+            creation_guard.disarm();
             return Err(e);
         }
</file context>

assert!(!session.permissions.lock().unwrap().pending.is_empty());
// Cancellation settles the outstanding question; it never auto-approves it.
hub.cancel(&session).await.unwrap();
let _ = tokio::time::timeout(WAIT, task).await.unwrap().unwrap();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This test ignores prompt_with's result, so a trigger question that fails instead of being cleanly cancelled can still pass. Unwrap the inner RPC result to make the cancellation path part of the assertion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/hub/trigger_quiet_tests.rs, line 154:

<comment>This test ignores `prompt_with`'s result, so a trigger question that fails instead of being cleanly cancelled can still pass. Unwrap the inner RPC result to make the cancellation path part of the assertion.</comment>

<file context>
@@ -0,0 +1,201 @@
+    assert!(!session.permissions.lock().unwrap().pending.is_empty());
+    // Cancellation settles the outstanding question; it never auto-approves it.
+    hub.cancel(&session).await.unwrap();
+    let _ = tokio::time::timeout(WAIT, task).await.unwrap().unwrap();
+    let records = hub.events(&session.id, 0, 10_000).unwrap();
+    let ended = records
</file context>
Suggested change
let _ = tokio::time::timeout(WAIT, task).await.unwrap().unwrap();
tokio::time::timeout(WAIT, task).await.unwrap().unwrap().unwrap();

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cmux-tui/crates/acpmux/src/hub/turns.rs">

<violation number="1">
P2: Digest turns contain messages from multiple agents, but this passes the first queued run to `run_params`, attaching that sender to the whole digest. Omit sender metadata for digests so relays do not misattribute the combined prompt.</violation>

<violation number="2" location="cmux-tui/crates/acpmux/src/hub/turns.rs:598">
P2: Trigger prompts are agent-less but this marks them as the person's turn. That makes `inbox_turn_started` hold pending agent messages behind a cloud trigger and applies human-turn inbox guards; exclude trigger provenance when computing `persons`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment on lines 598 to 599
let persons = super::inbox::persons_prompt(run.agent.as_deref(), run.provenance.as_ref())
&& self.config.read().await.agent_inbox();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Trigger prompts are agent-less but this marks them as the person's turn. That makes inbox_turn_started hold pending agent messages behind a cloud trigger and applies human-turn inbox guards; exclude trigger provenance when computing persons.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/hub/turns.rs, line 598:

<comment>Trigger prompts are agent-less but this marks them as the person's turn. That makes `inbox_turn_started` hold pending agent messages behind a cloud trigger and applies human-turn inbox guards; exclude trigger provenance when computing `persons`.</comment>

<file context>
@@ -586,8 +595,11 @@ impl Hub {
         // A continuation Stop overtook since it fired is refused here (`auto_resume.rs`).
         // Another agent's message shows as its sender and words (`prompt_line.rs`).
         let trigger = crate::prompt_provenance::is_trigger(run.provenance.as_ref());
+        let persons = super::inbox::persons_prompt(run.agent.as_deref(), run.provenance.as_ref())
+            && self.config.read().await.agent_inbox();
         let mut turn = TurnInfo::prompted(client, &text, &turn_id, &prompt_id, control)
</file context>
Suggested change
let persons = super::inbox::persons_prompt(run.agent.as_deref(), run.provenance.as_ref())
&& self.config.read().await.agent_inbox();
let persons = !crate::prompt_provenance::is_trigger(run.provenance.as_ref())
&& super::inbox::persons_prompt(run.agent.as_deref(), run.provenance.as_ref())
&& self.config.read().await.agent_inbox();

@@ -431,17 +431,15 @@ impl Hub {
&& running.is_some()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Digest turns contain messages from multiple agents, but this passes the first queued run to run_params, attaching that sender to the whole digest. Omit sender metadata for digests so relays do not misattribute the combined prompt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/acpmux/src/hub/turns.rs, line 874:

<comment>Digest turns contain messages from multiple agents, but this passes the first queued run to `run_params`, attaching that sender to the whole digest. Omit sender metadata for digests so relays do not misattribute the combined prompt.</comment>

<file context>
@@ -859,7 +871,7 @@ impl Hub {
             session,
             &child,
-            json!({"sessionId": agent_sid, "prompt": wire}),
+            super::prompt_sender::run_params(&agent_sid, json!(wire), &run),
             &turn_id,
             false,
</file context>

@blacksmith-sh

blacksmith-sh Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Found 20 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp4zleq1dx/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmpz7f2jh40/summary-fail.json (live):/summary-fail.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
main.DeveloperIdAppTests.test_download_prints_the_active_profile_only/
test_download_prints_the_active_profile_only
View Logs
nightly must build and hand off the macOS 26 helper and Xcode 26.5 app before publishin
g/
nightly must build and hand off the macOS 26 helper and Xcode 26.5 app before publishin
g
View Logs
stubs: /System/Library/Frameworks/Security.framework/Versions/A/Security has no stub/
Security has no stub
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_CFRetain']/
CoreFoundation, missing 1: ['_CFRetain']
View Logs

...and 10 more test failures. View all on Blacksmith: run 41291141231, run 41291141175

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 68bfabb: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr30526 -f ref=lane/cloud-trigger-chat-auth -f platform=macos -f backend_mode=local

red: cmux-next crash ratchet; NEW: green on the base 7745465; do not merge until green; see lane-rules

Shipped in:

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (claude-opus-5-5 medium, cloud-reviewer): SHIP at e4cb845

This is a delta review against my SHIP at 68bfabb. The only new commit is e4cb845, a merge of feat-cmux-next at 7745465 into 68bfabb.

Carry check

Not proven here: a semantic compile and tests of the merged tree. A clean textual merge with #30337's hub, session and turn changes is not a compile receipt, so this head's own CI (acpmux build, tests and clippy) is the check for that. At the time of writing those checks are pending. The only failure is the risk-tier "hold high until a person looked" gate, not a code red. Failures inherited from base (for example #30337's registry and Rustls tests until #30775 lands) are base reds, not this PR's.

Follow-ups, carried unchanged from 68bfabb (via the a3aac43 receipt):

  • Check release trigger authorization against the dev-joined DEFAULT_HUB end to end on nightly.
  • Fix the "pinned TLS" wording.
  • Run the release-mode rustc --test in CI.
  • Bound the trigger_delivery scan.
  • Add mock-authority and serve_connection refusal tests.
  • Fix the 4 s retry doc.
  • Handle non-Linux identity().

🤖 Generated with Claude Code

@teamleaderleo
teamleaderleo merged commit 68cab88 into feat-cmux-next Nov 1, 2026
117 of 123 checks passed
@teamleaderleo
teamleaderleo deleted the lane/cloud-trigger-chat-auth branch November 1, 2026 11:40
@lawrencecchen

Copy link
Copy Markdown
Contributor

This PR made the Rust panic ratchet red on feat-cmux-next: acpmux clippy::unreachable 4 > 2 (job 770f13cbbcccb1d764e99793 at 98dbb85). The cause is the two _ => unreachable!() arms in cli/trigger_chat.rs and server/trigger_chat.rs. #30853 fixes them forward with the typed errors that these callers already return, and does not change behavior. Before you land, run cmux-ci run --class light --script scripts/ci/cmux-tui-rust-check.sh --arg panic --os linux --ref SHA.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (independent Codex gpt-6.1-sol, high, via the review router; full diff from merge base 7745465 to this head): BLOCK at e4cb845.

[P1] [blocking] trigger_chat.rs:438-448 - The host identity is checked before ensure/send, but never after these asynchronous operations. If the VM is cloned or rebound while session creation or prompt delivery is in progress, an authorization issued for the old machine can execute on the new identity, violating the machine-bound trigger scope - add an operation identity fence and revalidate after every awaited mutation, aborting and cleaning up stale work.

lawrencecchen added a commit that referenced this pull request Nov 1, 2026
… trigger chat use get and JsonSet::set

At the first fix head 138e777 (job 4c82b1115ebde52dc9e7a883) the
ratchet still has a second row: acpmux indexing_slicing 702 > 686. Every
new hit is #30526's (68cab88); the other acpmux commits since the
last green tip (57be1e3) add none in production code.
- hub/prompt_ledger.rs: provenance and record reads now use get with
  as_str/as_bool (`v["k"] == "x"` is true only for that string, as
  `get(..).and_then(as_str) == Some("x")`); the `out[..] =` writes on
  the json! object are JsonSet::set.
- server/trigger_chat.rs: the receipt/result writes are JsonSet::set
  (each target is the object validate_durable_receipt returned).
No behavior change; the baseline is not raised.
lawrencecchen added a commit that referenced this pull request Nov 1, 2026
prompt_provenance.rs test `!of_provenance(..).is_some()` is
`of_provenance(..).is_none()` (clippy: this boolean expression can be
simplified). Fixes #30526.
lawrencecchen added a commit that referenced this pull request Nov 1, 2026
- acpmux server/trigger_chat.rs identity(): the non-Linux cfg block is
  the tail expression on macOS, so its `return` is needless there
  (clippy on macOS). It is now the block's value; on Linux that block
  is not compiled, as before. Fixes #30526.
- cmux-tui link/start_chat_cli.rs plan_with: `create_key:
  keys.create.clone()` is the last use of keys.create (the workspace
  closure just before has already run), so it moves. Fixes #30574.
lawrencecchen added a commit that referenced this pull request Nov 1, 2026
…s and lints, #30574's redundant clone (#30853)

* base red: panic ratchet, #30526's two unreachable! in trigger chat return the caller's typed error

The panic ratchet is red at 98dbb85 (job 770f13cbbcccb1d764e99793):
acpmux clippy::unreachable 4 > 2. #30526 (68cab88) added the two:
- cli/trigger_chat.rs run: the match over `operation(..)`'s result. Its
  fallback arm now returns the same anyhow error `operation` gives for
  a bad value.
- server/trigger_chat.rs handle: the match over `expected`, set from
  the method a few lines up. Its fallback arm now returns the same
  RpcError::method_not_found(m) the first match gives.
Both arms cannot run (the value is one of the three matched strings),
so no path changes. Fixes #30526.

* base red: panic ratchet, #30526's new Value index reads and writes in trigger chat use get and JsonSet::set

At the first fix head 138e777 (job 4c82b1115ebde52dc9e7a883) the
ratchet still has a second row: acpmux indexing_slicing 702 > 686. Every
new hit is #30526's (68cab88); the other acpmux commits since the
last green tip (57be1e3) add none in production code.
- hub/prompt_ledger.rs: provenance and record reads now use get with
  as_str/as_bool (`v["k"] == "x"` is true only for that string, as
  `get(..).and_then(as_str) == Some("x")`); the `out[..] =` writes on
  the json! object are JsonSet::set.
- server/trigger_chat.rs: the receipt/result writes are JsonSet::set
  (each target is the object validate_durable_receipt returned).
No behavior change; the baseline is not raised.

* base red: clippy nonminimal_bool in #30526's trigger provenance test

prompt_provenance.rs test `!of_provenance(..).is_some()` is
`of_provenance(..).is_none()` (clippy: this boolean expression can be
simplified). Fixes #30526.

* base red: clippy needless_return (#30526) and redundant_clone (#30574)

- acpmux server/trigger_chat.rs identity(): the non-Linux cfg block is
  the tail expression on macOS, so its `return` is needless there
  (clippy on macOS). It is now the block's value; on Linux that block
  is not compiled, as before. Fixes #30526.
- cmux-tui link/start_chat_cli.rs plan_with: `create_key:
  keys.create.clone()` is the last use of keys.create (the workspace
  closure just before has already run), so it moves. Fixes #30574.

This branch was successfully deployed

2 inactive deployments
sentry-dsyms — e4cb8450 Deployed Nov 1, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #95953
artifacts — e4cb8450 Deployed Nov 1, 2026 by teamleaderleo via publish to R2 #95953
ci-routing-agent — e4cb8450 Deployed Nov 1, 2026 by teamleaderleo via cmux-tui tree preflight #95953
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants