@team-agent/installer 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +118 -9
- package/Cargo.toml +2 -2
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/app_server_test_support.rs +258 -0
- package/crates/team-agent/src/cli/adapters.rs +32 -3
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
- package/crates/team-agent/src/cli/diagnose.rs +14 -5
- package/crates/team-agent/src/cli/emit.rs +74 -1
- package/crates/team-agent/src/cli/mod.rs +70 -25
- package/crates/team-agent/src/cli/named_address.rs +111 -0
- package/crates/team-agent/src/cli/send.rs +98 -3
- package/crates/team-agent/src/cli/status_port.rs +44 -6
- package/crates/team-agent/src/cli/tests/named_address.rs +131 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/types.rs +17 -0
- package/crates/team-agent/src/codex_app_server.rs +488 -0
- package/crates/team-agent/src/conpty/backend.rs +710 -0
- package/crates/team-agent/src/conpty/mod.rs +32 -0
- package/crates/team-agent/src/coordinator/backoff.rs +45 -6
- package/crates/team-agent/src/diagnose/orphans.rs +11 -3
- package/crates/team-agent/src/leader/lease.rs +144 -7
- package/crates/team-agent/src/leader/owner_bind.rs +5 -2
- package/crates/team-agent/src/leader/start.rs +18 -5
- package/crates/team-agent/src/leader/tests/lease_api.rs +172 -0
- package/crates/team-agent/src/lib.rs +23 -0
- package/crates/team-agent/src/lifecycle/launch.rs +127 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +63 -0
- package/crates/team-agent/src/messaging/delivery.rs +329 -9
- package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +78 -21
- package/crates/team-agent/src/messaging/tests/runtime.rs +232 -0
- package/crates/team-agent/src/tmux_backend.rs +31 -0
- package/crates/team-agent/src/transport_factory.rs +632 -0
- package/package.json +4 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//! 0.5.x Windows-native transport Phase 1: ConPTY backend.
|
|
2
|
+
//!
|
|
3
|
+
//! Wire protocol + portable shim state machine live in the
|
|
4
|
+
//! `conpty-transport` crate (portable, no team-agent runtime deps —
|
|
5
|
+
//! consumed directly by the Windows `windows-shim` binary). This
|
|
6
|
+
//! module wires that portable machinery into team-agent's `Transport`
|
|
7
|
+
//! trait via `ConPtyBackend`.
|
|
8
|
+
//!
|
|
9
|
+
//! Truth sources (READ-ONLY):
|
|
10
|
+
//! - Design: `.team/artifacts/0.5.x-windows-native-transport-design.md`
|
|
11
|
+
//! - CR verdict: `.team/artifacts/0.5.x-windows-transport-cr-verdict.md`
|
|
12
|
+
//!
|
|
13
|
+
//! CR anchors:
|
|
14
|
+
//! - **C-1 pipe_token**: only lives in `ConPtyBackend.pipe_token`
|
|
15
|
+
//! (Mutex) and the shim's in-memory state; never persisted. Grep
|
|
16
|
+
//! guard test (`conpty_pipe_token_no_persist_guard.rs`) locks this.
|
|
17
|
+
//! - **C-2 kill_server**: forwards to `Op::Shutdown` per-team, not
|
|
18
|
+
//! global. See CR pre-check in the gate report.
|
|
19
|
+
//! - **C-3 stale event**: emitted by the backend when `state.shim_pid`
|
|
20
|
+
//! disagrees with the shim's `hello` response.
|
|
21
|
+
//! - **C-5 pipe_token_mismatch**: `ProtocolError::PipeTokenMismatch`
|
|
22
|
+
//! maps to `TransportError::MuxUnavailable`, NEVER silent retry.
|
|
23
|
+
|
|
24
|
+
pub mod backend;
|
|
25
|
+
|
|
26
|
+
pub use backend::{ConPtyBackend, PipeClientAdapter, PipeClientTrait};
|
|
27
|
+
// Re-export the portable protocol + shim types from the standalone
|
|
28
|
+
// crate so downstream code can `use team_agent::conpty::{Shim,
|
|
29
|
+
// FakePaneRuntime, ...}` without a second dependency.
|
|
30
|
+
pub use conpty_transport::{
|
|
31
|
+
protocol, shim, FakePaneRuntime, LocalShimClient, PaneRuntime, PipeClient, Shim,
|
|
32
|
+
};
|
|
@@ -36,19 +36,58 @@ pub fn run_daemon(args: DaemonArgs) -> Result<(), DaemonError> {
|
|
|
36
36
|
// CP-1: the daemon's whole tick surface (has_session / capture / inject / list_windows / kill)
|
|
37
37
|
// runs through this backend. Prefer the persisted runtime endpoint so attached explicit-socket
|
|
38
38
|
// teams are checked on the same socket as lifecycle worker operations.
|
|
39
|
+
//
|
|
40
|
+
// 0.5.x Phase 1d Batch 3: coordinator boot now routes through
|
|
41
|
+
// `transport_factory::resolve_transport` so `state.transport.kind =
|
|
42
|
+
// conpty` boots the ConPTY backend, not a tmux backend that would
|
|
43
|
+
// fake `has_session()`. Tmux behavior is byte-equivalent (Layer 3
|
|
44
|
+
// legacy `tmux_endpoint` → same `tmux_backend_for_runtime_state_or_workspace`
|
|
45
|
+
// shape inside the factory's `build_tmux`).
|
|
39
46
|
let state = crate::state::persist::load_runtime_state(args.workspace.as_path()).ok();
|
|
40
|
-
let
|
|
47
|
+
let factory_input = crate::transport_factory::TransportFactoryInput::new(
|
|
41
48
|
args.workspace.as_path(),
|
|
42
|
-
|
|
43
|
-
)
|
|
49
|
+
crate::transport_factory::TransportPurpose::Coordinator,
|
|
50
|
+
)
|
|
51
|
+
.with_state(state.as_ref());
|
|
52
|
+
let resolved = match crate::transport_factory::resolve_transport(factory_input) {
|
|
53
|
+
Ok(resolved) => resolved,
|
|
54
|
+
Err(e) => {
|
|
55
|
+
// Fail-closed honest degradation: coordinator boots with a
|
|
56
|
+
// tmux-workspace backend so it doesn't crash, but records
|
|
57
|
+
// the assembly refusal in the boot metadata source so the
|
|
58
|
+
// event log shows the reason. This preserves the daemon
|
|
59
|
+
// liveness path while making the failure explicit.
|
|
60
|
+
eprintln!(
|
|
61
|
+
"coordinator: transport_factory refused ({e}); falling back to tmux workspace for daemon liveness"
|
|
62
|
+
);
|
|
63
|
+
let sel = crate::tmux_backend::tmux_backend_for_runtime_state_or_workspace(
|
|
64
|
+
args.workspace.as_path(),
|
|
65
|
+
state.as_ref(),
|
|
66
|
+
);
|
|
67
|
+
let metadata = DaemonTmuxEndpointMetadata {
|
|
68
|
+
tmux_endpoint_used: sel.tmux_endpoint_used.clone(),
|
|
69
|
+
tmux_endpoint_source: "factory_refused_fallback",
|
|
70
|
+
};
|
|
71
|
+
let coord = Coordinator::new(
|
|
72
|
+
args.workspace.clone(),
|
|
73
|
+
Box::new(RealProviderRegistry),
|
|
74
|
+
Box::new(sel.backend),
|
|
75
|
+
);
|
|
76
|
+
return run_daemon_with_coordinator_and_boot_tmux(&args, &coord, Some(metadata));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
// Preserve tmux boot metadata byte-equivalent for tmux teams.
|
|
80
|
+
// ConPTY teams get their source string but no tmux endpoint (design
|
|
81
|
+
// §Behavior Equivalence: same `tmux_endpoint_used` for tmux; None
|
|
82
|
+
// + honest source for conpty).
|
|
44
83
|
let tmux_metadata = DaemonTmuxEndpointMetadata {
|
|
45
|
-
tmux_endpoint_used:
|
|
46
|
-
tmux_endpoint_source:
|
|
84
|
+
tmux_endpoint_used: resolved.tmux_endpoint_used.clone(),
|
|
85
|
+
tmux_endpoint_source: resolved.source,
|
|
47
86
|
};
|
|
48
87
|
let coordinator = Coordinator::new(
|
|
49
88
|
args.workspace.clone(),
|
|
50
89
|
Box::new(RealProviderRegistry),
|
|
51
|
-
|
|
90
|
+
resolved.backend,
|
|
52
91
|
);
|
|
53
92
|
run_daemon_with_coordinator_and_boot_tmux(&args, &coordinator, Some(tmux_metadata))
|
|
54
93
|
}
|
|
@@ -10,7 +10,8 @@ use crate::coordinator::health::{
|
|
|
10
10
|
coordinator_metadata_ok, pid_is_running, read_coordinator_metadata, terminate_pid_tree,
|
|
11
11
|
};
|
|
12
12
|
use crate::coordinator::types::{OrphanReason, Pid, WorkspacePath};
|
|
13
|
-
|
|
13
|
+
// Phase 1d Batch 6: `TmuxBackend` import removed — all sites now use
|
|
14
|
+
// `transport_factory::tmux_socket_name_transport` for grep-visibility.
|
|
14
15
|
use crate::transport::{SessionName, Transport};
|
|
15
16
|
|
|
16
17
|
#[derive(Debug, Clone)]
|
|
@@ -352,7 +353,11 @@ fn tmux_socket_roots() -> Vec<PathBuf> {
|
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
fn tmux_list_panes(socket: &str) -> Vec<TmuxPaneRow> {
|
|
355
|
-
|
|
356
|
+
// Phase 1d Batch 6: factory tmux-socket-name helper for
|
|
357
|
+
// grep-visibility. Tmux-only orphan scanner (enumerates tmux socket
|
|
358
|
+
// roots); ConPTY orphan diagnostics are a separate shim registry
|
|
359
|
+
// command, not routed here.
|
|
360
|
+
crate::transport_factory::tmux_socket_name_transport(socket)
|
|
356
361
|
.list_targets()
|
|
357
362
|
.unwrap_or_default()
|
|
358
363
|
.into_iter()
|
|
@@ -664,7 +669,10 @@ fn kill_tmux_session(orphan: &OrphanRecord) -> bool {
|
|
|
664
669
|
let (Some(socket), Some(session)) = (&orphan.tmux_socket, &orphan.session) else {
|
|
665
670
|
return false;
|
|
666
671
|
};
|
|
667
|
-
|
|
672
|
+
// Phase 1d Batch 6: factory tmux-socket-name helper. Explicit
|
|
673
|
+
// tmux-only kill; conpty shim orphans are NOT routed here (design
|
|
674
|
+
// §Batch 6).
|
|
675
|
+
crate::transport_factory::tmux_socket_name_transport(socket)
|
|
668
676
|
.kill_session(&SessionName::new(session.clone()))
|
|
669
677
|
.is_ok()
|
|
670
678
|
}
|
|
@@ -134,6 +134,58 @@ pub fn attach_leader(
|
|
|
134
134
|
})
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/// Explicit app-server leader binding. Validates the supplied socket/thread tuple
|
|
138
|
+
/// before writing the typed physical-channel anchor through the lease primitive.
|
|
139
|
+
pub fn attach_app_server_leader(
|
|
140
|
+
workspace: &Path,
|
|
141
|
+
team: Option<&str>,
|
|
142
|
+
socket: &str,
|
|
143
|
+
thread_id: &str,
|
|
144
|
+
) -> Result<Value, LeaderError> {
|
|
145
|
+
let binding = crate::codex_app_server::attach_probe(socket, thread_id)
|
|
146
|
+
.map_err(|error| LeaderError::Validation(error.to_string()))?;
|
|
147
|
+
let event_log = crate::event_log::EventLog::new(workspace);
|
|
148
|
+
let scoped_team = team.filter(|value| !value.is_empty());
|
|
149
|
+
let mut state = if scoped_team.is_some() {
|
|
150
|
+
crate::state::projection::select_runtime_state(workspace, scoped_team)?
|
|
151
|
+
} else {
|
|
152
|
+
crate::state::persist::load_runtime_state(workspace)?
|
|
153
|
+
};
|
|
154
|
+
if !state.is_object() {
|
|
155
|
+
state = json!({});
|
|
156
|
+
}
|
|
157
|
+
if let Some(team) = scoped_team {
|
|
158
|
+
state["active_team_key"] = json!(team);
|
|
159
|
+
}
|
|
160
|
+
let team_key = canonical_owner_write_key(&state);
|
|
161
|
+
let next_epoch = OwnerEpoch(current_owner_epoch(&state).0.saturating_add(1));
|
|
162
|
+
let receiver = app_server_receiver_value(&binding, next_epoch);
|
|
163
|
+
let owner = app_server_owner_value(next_epoch);
|
|
164
|
+
let record = crate::state::ownership::OwnershipWrite::new()
|
|
165
|
+
.with_leader_receiver(receiver.clone())
|
|
166
|
+
.with_team_owner(owner.clone())
|
|
167
|
+
.with_owner_epoch(next_epoch.0);
|
|
168
|
+
crate::state::ownership::write_owner(&mut state, &team_key, record);
|
|
169
|
+
write_claim_state(workspace, &state, scoped_team, Some(&team_key))?;
|
|
170
|
+
event_log.write(
|
|
171
|
+
super::LeaderEvent::ReceiverAttached.name(),
|
|
172
|
+
json!({
|
|
173
|
+
"transport_kind": "codex_app_server",
|
|
174
|
+
"thread_id": binding.thread_id,
|
|
175
|
+
"owner_epoch": next_epoch.0,
|
|
176
|
+
"team": team_key,
|
|
177
|
+
}),
|
|
178
|
+
)?;
|
|
179
|
+
Ok(json!({
|
|
180
|
+
"ok": true,
|
|
181
|
+
"status": "claimed",
|
|
182
|
+
"team": team_key,
|
|
183
|
+
"owner_epoch": next_epoch.0,
|
|
184
|
+
"leader_receiver": receiver,
|
|
185
|
+
"team_owner": owner,
|
|
186
|
+
}))
|
|
187
|
+
}
|
|
188
|
+
|
|
137
189
|
#[derive(Clone)]
|
|
138
190
|
struct AttachLeaderTarget {
|
|
139
191
|
info: PaneInfo,
|
|
@@ -156,8 +208,11 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
|
|
|
156
208
|
}),
|
|
157
209
|
);
|
|
158
210
|
}
|
|
211
|
+
// Phase 1d Batch 6: factory tmux workspace helper for
|
|
212
|
+
// grep-visibility. Semantics unchanged; leader lease discovery is
|
|
213
|
+
// tmux-only (caller pane = tmux pane, MUST-12 anchor).
|
|
159
214
|
targets.extend(
|
|
160
|
-
crate::
|
|
215
|
+
crate::transport_factory::tmux_workspace_transport(workspace)
|
|
161
216
|
.list_targets()
|
|
162
217
|
.unwrap_or_default()
|
|
163
218
|
.into_iter()
|
|
@@ -167,10 +222,12 @@ fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTar
|
|
|
167
222
|
}
|
|
168
223
|
|
|
169
224
|
fn tmux_backend_for_endpoint(endpoint: &str) -> crate::tmux_backend::TmuxBackend {
|
|
225
|
+
// Phase 1d Batch 6: factory tmux channel helpers for grep-visibility.
|
|
226
|
+
// Same tmux-only semantics as before.
|
|
170
227
|
if endpoint.is_empty() || endpoint == "default" {
|
|
171
|
-
crate::
|
|
228
|
+
crate::transport_factory::tmux_default_transport()
|
|
172
229
|
} else {
|
|
173
|
-
crate::
|
|
230
|
+
crate::transport_factory::tmux_endpoint_transport(endpoint)
|
|
174
231
|
}
|
|
175
232
|
}
|
|
176
233
|
|
|
@@ -320,11 +377,13 @@ pub fn claim_leader(
|
|
|
320
377
|
.unwrap_or_default();
|
|
321
378
|
let raw_state = crate::state::persist::load_runtime_state(workspace)?;
|
|
322
379
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
323
|
-
|
|
380
|
+
// Phase 1d Batch 6: factory tmux channel helpers for grep-visibility.
|
|
381
|
+
// Tmux-only leader-claim path (caller pane enumeration).
|
|
382
|
+
let mut targets = crate::transport_factory::tmux_workspace_transport(workspace)
|
|
324
383
|
.list_targets()
|
|
325
384
|
.unwrap_or_default();
|
|
326
385
|
targets.extend(
|
|
327
|
-
crate::
|
|
386
|
+
crate::transport_factory::tmux_default_transport()
|
|
328
387
|
.list_targets()
|
|
329
388
|
.unwrap_or_default(),
|
|
330
389
|
);
|
|
@@ -1055,7 +1114,7 @@ fn write_binding_to_state(
|
|
|
1055
1114
|
}
|
|
1056
1115
|
let team_key = canonical_owner_write_key(state);
|
|
1057
1116
|
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1058
|
-
.with_leader_receiver(
|
|
1117
|
+
.with_leader_receiver(tmux_receiver_value(receiver)?)
|
|
1059
1118
|
.with_team_owner(serde_json::to_value(owner)?)
|
|
1060
1119
|
.with_owner_epoch(owner.owner_epoch.0);
|
|
1061
1120
|
crate::state::ownership::write_owner(state, &team_key, record);
|
|
@@ -1074,11 +1133,89 @@ fn write_receiver_to_state(
|
|
|
1074
1133
|
}
|
|
1075
1134
|
let team_key = canonical_owner_write_key(state);
|
|
1076
1135
|
let record = crate::state::ownership::OwnershipWrite::new()
|
|
1077
|
-
.with_leader_receiver(
|
|
1136
|
+
.with_leader_receiver(tmux_receiver_value(receiver)?);
|
|
1078
1137
|
crate::state::ownership::write_owner(state, &team_key, record);
|
|
1079
1138
|
Ok(())
|
|
1080
1139
|
}
|
|
1081
1140
|
|
|
1141
|
+
fn tmux_receiver_value(receiver: &LeaderReceiver) -> Result<Value, LeaderError> {
|
|
1142
|
+
Ok(write_leader_receiver_transport(
|
|
1143
|
+
serde_json::to_value(receiver)?,
|
|
1144
|
+
"direct_tmux",
|
|
1145
|
+
))
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
fn app_server_receiver_value(
|
|
1149
|
+
binding: &crate::codex_app_server::AppServerBinding,
|
|
1150
|
+
epoch: OwnerEpoch,
|
|
1151
|
+
) -> Value {
|
|
1152
|
+
write_leader_receiver_transport(
|
|
1153
|
+
json!({
|
|
1154
|
+
"status": "attached",
|
|
1155
|
+
"provider": "codex",
|
|
1156
|
+
"owner_epoch": epoch.0,
|
|
1157
|
+
"app_server": {
|
|
1158
|
+
"socket": binding.socket,
|
|
1159
|
+
"thread_id": binding.thread_id,
|
|
1160
|
+
"session_id": binding.session_id,
|
|
1161
|
+
"cwd": binding.cwd,
|
|
1162
|
+
"cli_version": binding.cli_version,
|
|
1163
|
+
"bound_at": binding.bound_at,
|
|
1164
|
+
"source": "app-server"
|
|
1165
|
+
}
|
|
1166
|
+
}),
|
|
1167
|
+
"codex_app_server",
|
|
1168
|
+
)
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
fn app_server_owner_value(epoch: OwnerEpoch) -> Value {
|
|
1172
|
+
json!({
|
|
1173
|
+
"provider": "codex",
|
|
1174
|
+
"transport_kind": "codex_app_server",
|
|
1175
|
+
"owner_epoch": epoch.0,
|
|
1176
|
+
"claimed_at": now_ts(),
|
|
1177
|
+
"claimed_via": "attach-app-server-leader",
|
|
1178
|
+
"os_user": std::env::var("USER")
|
|
1179
|
+
.or_else(|_| std::env::var("USERNAME"))
|
|
1180
|
+
.unwrap_or_default(),
|
|
1181
|
+
})
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
fn write_leader_receiver_transport(mut receiver: Value, transport_kind: &str) -> Value {
|
|
1185
|
+
if let Some(obj) = receiver.as_object_mut() {
|
|
1186
|
+
obj.insert("mode".to_string(), json!(transport_kind));
|
|
1187
|
+
match transport_kind {
|
|
1188
|
+
"codex_app_server" => {
|
|
1189
|
+
obj.insert("transport_kind".to_string(), json!(transport_kind));
|
|
1190
|
+
for legacy in [
|
|
1191
|
+
"pane_id",
|
|
1192
|
+
"tmux_socket",
|
|
1193
|
+
"session_name",
|
|
1194
|
+
"window_index",
|
|
1195
|
+
"window_name",
|
|
1196
|
+
"pane_index",
|
|
1197
|
+
"pane_tty",
|
|
1198
|
+
"pane_current_command",
|
|
1199
|
+
"fingerprint",
|
|
1200
|
+
"leader_session_uuid",
|
|
1201
|
+
"attached_at",
|
|
1202
|
+
"discovery",
|
|
1203
|
+
"requested_provider",
|
|
1204
|
+
"warning",
|
|
1205
|
+
] {
|
|
1206
|
+
obj.remove(legacy);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
"direct_tmux" => {
|
|
1210
|
+
obj.remove("app_server");
|
|
1211
|
+
obj.remove("transport_kind");
|
|
1212
|
+
}
|
|
1213
|
+
_ => {}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
receiver
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1082
1219
|
/// Stage 3 owner persist fix (architect direction 2026-06-24): determine
|
|
1083
1220
|
/// the canonical team_key for an in-memory ownership write. When the state
|
|
1084
1221
|
/// carries an `active_team_key` (set by `project_top_level_view` whenever
|
|
@@ -262,7 +262,9 @@ fn family_a_identity(
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
fn tmux_pane_current_command(workspace: &Path, pane: &str) -> Result<String, LeaderError> {
|
|
265
|
-
|
|
265
|
+
// Phase 1d Batch 6: factory tmux workspace helper for grep-visibility.
|
|
266
|
+
// Tmux-only owner-bind (caller pane = tmux pane).
|
|
267
|
+
crate::transport_factory::tmux_workspace_transport(workspace)
|
|
266
268
|
.query(
|
|
267
269
|
&Target::Pane(PaneId::new(pane)),
|
|
268
270
|
PaneField::PaneCurrentCommand,
|
|
@@ -273,7 +275,8 @@ fn tmux_pane_current_command(workspace: &Path, pane: &str) -> Result<String, Lea
|
|
|
273
275
|
|
|
274
276
|
fn tmux_pane_info(workspace: &Path, pane: &str) -> Option<PaneInfo> {
|
|
275
277
|
let target = PaneId::new(pane);
|
|
276
|
-
|
|
278
|
+
// Phase 1d Batch 6: same as above.
|
|
279
|
+
crate::transport_factory::tmux_workspace_transport(workspace)
|
|
277
280
|
.list_targets()
|
|
278
281
|
.ok()?
|
|
279
282
|
.into_iter()
|
|
@@ -434,7 +434,12 @@ fn execute_managed_leader_plan(
|
|
|
434
434
|
let Some(window) = plan.leader_window.as_ref() else {
|
|
435
435
|
return Err(LeaderError::Start("managed leader window missing".to_string()));
|
|
436
436
|
};
|
|
437
|
-
|
|
437
|
+
// 0.5.x Phase 1d Batch 6: use the factory tmux channel helper
|
|
438
|
+
// (thin wrapper over `TmuxBackend::for_workspace`) for
|
|
439
|
+
// grep-visibility of every intentional tmux-only leader-launcher
|
|
440
|
+
// site. Semantics unchanged; managed leader hosting stays
|
|
441
|
+
// tmux-only per design §Batch 6.
|
|
442
|
+
let transport = crate::transport_factory::tmux_workspace_transport(workspace);
|
|
438
443
|
let spawned = ensure_managed_leader_pane(&transport, session, window, plan, workspace)?;
|
|
439
444
|
persist_managed_leader_binding(plan, workspace, &spawned)?;
|
|
440
445
|
spawn_managed_provider_startup_prompt_handler(
|
|
@@ -1029,7 +1034,9 @@ fn spawn_managed_provider_startup_prompt_handler(
|
|
|
1029
1034
|
pane_id: String,
|
|
1030
1035
|
) {
|
|
1031
1036
|
std::thread::spawn(move || {
|
|
1032
|
-
|
|
1037
|
+
// Phase 1d Batch 6: factory tmux workspace helper for
|
|
1038
|
+
// grep-visibility. Semantics unchanged.
|
|
1039
|
+
let transport = crate::transport_factory::tmux_workspace_transport(&workspace);
|
|
1033
1040
|
let _ = handle_exec_provider_startup_prompts(
|
|
1034
1041
|
provider,
|
|
1035
1042
|
&workspace,
|
|
@@ -1042,9 +1049,13 @@ fn spawn_managed_provider_startup_prompt_handler(
|
|
|
1042
1049
|
}
|
|
1043
1050
|
|
|
1044
1051
|
fn tmux_transport_for_current_pane() -> TmuxBackend {
|
|
1052
|
+
// Phase 1d Batch 6: factory tmux channel helpers for
|
|
1053
|
+
// grep-visibility. Semantics unchanged; this is intentionally
|
|
1054
|
+
// tmux-only (caller pane = tmux `$TMUX` endpoint or default socket,
|
|
1055
|
+
// MUST-12 anchor).
|
|
1045
1056
|
crate::tmux_backend::socket_name_from_tmux_env()
|
|
1046
|
-
.map(|endpoint|
|
|
1047
|
-
.unwrap_or_else(
|
|
1057
|
+
.map(|endpoint| crate::transport_factory::tmux_endpoint_transport(&endpoint))
|
|
1058
|
+
.unwrap_or_else(crate::transport_factory::tmux_default_transport)
|
|
1048
1059
|
}
|
|
1049
1060
|
|
|
1050
1061
|
pub fn handle_exec_provider_startup_prompts(
|
|
@@ -1113,7 +1124,9 @@ fn provider_command_name(provider: Provider) -> &'static str {
|
|
|
1113
1124
|
}
|
|
1114
1125
|
|
|
1115
1126
|
fn tmux_session_exists(workspace: &Path, session: &SessionName) -> Result<bool, LeaderError> {
|
|
1116
|
-
|
|
1127
|
+
// Phase 1d Batch 6: factory tmux workspace helper for
|
|
1128
|
+
// grep-visibility. Tmux-only anchor (managed leader session lookup).
|
|
1129
|
+
crate::transport_factory::tmux_workspace_transport(workspace)
|
|
1117
1130
|
.has_session(session)
|
|
1118
1131
|
.map_err(|e| LeaderError::Start(format!("tmux has-session failed: {e}")))
|
|
1119
1132
|
}
|
|
@@ -302,3 +302,175 @@ fn r8_requeued_exhausted_watchers_event_payload_golden_shape() {
|
|
|
302
302
|
assert_eq!(payload.get("count").and_then(|v| v.as_u64()), Some(1), "count == number of requeued watchers");
|
|
303
303
|
assert_eq!(payload.get("trigger").and_then(|v| v.as_str()), Some("attach_leader"));
|
|
304
304
|
}
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn app_server_attach_writes_transport_kind_tuple_and_advances_epoch() {
|
|
308
|
+
let ws = std::env::temp_dir().join(format!(
|
|
309
|
+
"ta_rs_app_attach_{}_{}",
|
|
310
|
+
std::process::id(),
|
|
311
|
+
chrono::Utc::now().timestamp_millis()
|
|
312
|
+
));
|
|
313
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
314
|
+
crate::state::persist::save_runtime_state(
|
|
315
|
+
&ws,
|
|
316
|
+
&serde_json::json!({
|
|
317
|
+
"active_team_key": "team-a",
|
|
318
|
+
"teams": {"team-a": {"team_key": "team-a", "agents": {}}},
|
|
319
|
+
}),
|
|
320
|
+
)
|
|
321
|
+
.unwrap();
|
|
322
|
+
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
323
|
+
"attach-ok",
|
|
324
|
+
crate::app_server_test_support::FakeAppServerScript::happy(
|
|
325
|
+
"thread-live",
|
|
326
|
+
"session-live",
|
|
327
|
+
ws.to_str().unwrap(),
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
let out = attach_app_server_leader(&ws, Some("team-a"), fake.endpoint(), "thread-live")
|
|
332
|
+
.expect("app-server attach should succeed");
|
|
333
|
+
|
|
334
|
+
assert_eq!(out["ok"], serde_json::json!(true));
|
|
335
|
+
assert_eq!(out["leader_receiver"]["transport_kind"], serde_json::json!("codex_app_server"));
|
|
336
|
+
assert_eq!(out["leader_receiver"]["mode"], serde_json::json!("codex_app_server"));
|
|
337
|
+
assert_eq!(
|
|
338
|
+
out["leader_receiver"]["app_server"]["thread_id"],
|
|
339
|
+
serde_json::json!("thread-live")
|
|
340
|
+
);
|
|
341
|
+
assert_eq!(
|
|
342
|
+
out["leader_receiver"]["app_server"]["session_id"],
|
|
343
|
+
serde_json::json!("session-live")
|
|
344
|
+
);
|
|
345
|
+
assert_eq!(
|
|
346
|
+
out["leader_receiver"]["app_server"]["cwd"],
|
|
347
|
+
serde_json::json!(ws.to_str().unwrap())
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
let saved = crate::state::projection::select_runtime_state(&ws, Some("team-a")).unwrap();
|
|
351
|
+
assert_eq!(
|
|
352
|
+
saved["leader_receiver"]["app_server"]["thread_id"],
|
|
353
|
+
serde_json::json!("thread-live")
|
|
354
|
+
);
|
|
355
|
+
assert_eq!(saved["leader_receiver"].get("pane_id"), None);
|
|
356
|
+
assert_eq!(saved["team_owner"]["transport_kind"], serde_json::json!("codex_app_server"));
|
|
357
|
+
assert_eq!(saved["owner_epoch"], serde_json::json!(1));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#[test]
|
|
361
|
+
fn app_server_attach_rejects_world_writable_socket_without_state_write() {
|
|
362
|
+
let ws = std::env::temp_dir().join(format!(
|
|
363
|
+
"ta_rs_app_attach_badmode_{}_{}",
|
|
364
|
+
std::process::id(),
|
|
365
|
+
chrono::Utc::now().timestamp_millis()
|
|
366
|
+
));
|
|
367
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
368
|
+
crate::state::persist::save_runtime_state(
|
|
369
|
+
&ws,
|
|
370
|
+
&serde_json::json!({"active_team_key": "team-a", "teams": {"team-a": {"agents": {}}}}),
|
|
371
|
+
)
|
|
372
|
+
.unwrap();
|
|
373
|
+
let fake = crate::app_server_test_support::FakeAppServer::start(
|
|
374
|
+
"attach-world",
|
|
375
|
+
crate::app_server_test_support::FakeAppServerScript::happy(
|
|
376
|
+
"thread-live",
|
|
377
|
+
"session-live",
|
|
378
|
+
ws.to_str().unwrap(),
|
|
379
|
+
),
|
|
380
|
+
);
|
|
381
|
+
let mut perms = std::fs::metadata(fake.path()).unwrap().permissions();
|
|
382
|
+
use std::os::unix::fs::PermissionsExt;
|
|
383
|
+
perms.set_mode(0o777);
|
|
384
|
+
std::fs::set_permissions(fake.path(), perms).unwrap();
|
|
385
|
+
|
|
386
|
+
let err = attach_app_server_leader(&ws, Some("team-a"), fake.endpoint(), "thread-live")
|
|
387
|
+
.expect_err("world-writable app-server socket must be rejected");
|
|
388
|
+
assert!(
|
|
389
|
+
err.to_string().contains("socket_ownership_invalid"),
|
|
390
|
+
"unexpected error: {err}"
|
|
391
|
+
);
|
|
392
|
+
let saved = crate::state::projection::select_runtime_state(&ws, Some("team-a")).unwrap();
|
|
393
|
+
assert!(
|
|
394
|
+
saved.get("leader_receiver").is_none(),
|
|
395
|
+
"failed attach must not write leader_receiver: {saved}"
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[test]
|
|
400
|
+
fn app_server_attach_rejects_missing_user_agent_without_state_write() {
|
|
401
|
+
let ws = std::env::temp_dir().join(format!(
|
|
402
|
+
"ta_rs_app_attach_no_ua_{}_{}",
|
|
403
|
+
std::process::id(),
|
|
404
|
+
chrono::Utc::now().timestamp_millis()
|
|
405
|
+
));
|
|
406
|
+
std::fs::create_dir_all(&ws).unwrap();
|
|
407
|
+
crate::state::persist::save_runtime_state(
|
|
408
|
+
&ws,
|
|
409
|
+
&serde_json::json!({"active_team_key": "team-a", "teams": {"team-a": {"agents": {}}}}),
|
|
410
|
+
)
|
|
411
|
+
.unwrap();
|
|
412
|
+
let mut script = crate::app_server_test_support::FakeAppServerScript::happy(
|
|
413
|
+
"thread-live",
|
|
414
|
+
"session-live",
|
|
415
|
+
ws.to_str().unwrap(),
|
|
416
|
+
);
|
|
417
|
+
script.user_agent = None;
|
|
418
|
+
let fake = crate::app_server_test_support::FakeAppServer::start("attach-no-ua", script);
|
|
419
|
+
|
|
420
|
+
let err = attach_app_server_leader(&ws, Some("team-a"), fake.endpoint(), "thread-live")
|
|
421
|
+
.expect_err("missing initialize.userAgent must fail closed");
|
|
422
|
+
assert!(
|
|
423
|
+
err.to_string()
|
|
424
|
+
.contains("protocol_mismatch_missing_user_agent"),
|
|
425
|
+
"unexpected error: {err}"
|
|
426
|
+
);
|
|
427
|
+
let saved = crate::state::projection::select_runtime_state(&ws, Some("team-a")).unwrap();
|
|
428
|
+
assert!(
|
|
429
|
+
saved.get("leader_receiver").is_none(),
|
|
430
|
+
"failed attach must not write leader_receiver: {saved}"
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
#[test]
|
|
435
|
+
fn app_server_delivery_paths_are_read_only_and_binding_entry_is_explicit() {
|
|
436
|
+
let delivery = include_str!("../../messaging/delivery.rs");
|
|
437
|
+
assert!(
|
|
438
|
+
!delivery.contains("write_owner(")
|
|
439
|
+
&& !delivery.contains("with_leader_receiver(")
|
|
440
|
+
&& !delivery.contains("with_team_owner("),
|
|
441
|
+
"MUST-12/I-RN-1: delivery may read typed leader_receiver fields but must not write ownership"
|
|
442
|
+
);
|
|
443
|
+
let lease = include_str!("../lease.rs");
|
|
444
|
+
assert!(
|
|
445
|
+
lease.contains("fn write_leader_receiver_transport("),
|
|
446
|
+
"C-5: mode and transport_kind must be stamped by a single receiver transport helper"
|
|
447
|
+
);
|
|
448
|
+
let cli = include_str!("../../cli/attach_app_server_leader.rs");
|
|
449
|
+
assert!(
|
|
450
|
+
cli.contains("attach_app_server_leader("),
|
|
451
|
+
"I-RN-2: app-server ownership mutation must be reachable through the explicit CLI entry"
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
#[test]
|
|
456
|
+
fn messaging_leader_receiver_module_exports_no_ownership_writer() {
|
|
457
|
+
let leader_receiver = include_str!("../../messaging/leader_receiver.rs");
|
|
458
|
+
for forbidden in [
|
|
459
|
+
"pub fn claim_leader_receiver",
|
|
460
|
+
"write_owner(",
|
|
461
|
+
"with_leader_receiver(",
|
|
462
|
+
"with_team_owner(",
|
|
463
|
+
"save_runtime_state",
|
|
464
|
+
] {
|
|
465
|
+
assert!(
|
|
466
|
+
!leader_receiver.contains(forbidden),
|
|
467
|
+
"MUST-12/I-RN-1: messaging/leader_receiver.rs may read receiver fields but must not expose ownership writes; forbidden={forbidden}"
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let messaging_mod = include_str!("../../messaging/mod.rs");
|
|
472
|
+
assert!(
|
|
473
|
+
!messaging_mod.contains("claim_leader_receiver"),
|
|
474
|
+
"MUST-12/I-RN-2: messaging/mod.rs must not re-export ownership writer APIs"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
@@ -92,4 +92,27 @@ pub mod fake_worker;
|
|
|
92
92
|
// tmux_backend — concrete tmux Transport executor (Command::new tmux via a CommandRunner seam);
|
|
93
93
|
// the real spawn/capture/inject/has_session backend the daemon + launch use (step 9 shipped only
|
|
94
94
|
// the trait + argv-builders). Real subprocess execution is the #[ignore] real-machine boundary.
|
|
95
|
+
pub mod codex_app_server;
|
|
95
96
|
pub mod tmux_backend;
|
|
97
|
+
|
|
98
|
+
// 0.5.x Windows-native transport Phase 1: ConPTY backend + named-pipe
|
|
99
|
+
// protocol. `protocol` is portable (pure logic, tested on all hosts);
|
|
100
|
+
// `backend` compiles on all hosts and returns typed MuxUnavailable when
|
|
101
|
+
// no pipe client is wired (honest degradation, not silent success).
|
|
102
|
+
//
|
|
103
|
+
// Truth source:
|
|
104
|
+
// - Design: `.team/artifacts/0.5.x-windows-native-transport-design.md`
|
|
105
|
+
// §Transport Boundary + §Named Pipe Control Protocol + §Phase 1
|
|
106
|
+
// - CR verdict: `.team/artifacts/0.5.x-windows-transport-cr-verdict.md`
|
|
107
|
+
// (7 constraints; C-1/C-2/C-3/C-5 anchor into this module)
|
|
108
|
+
pub mod conpty;
|
|
109
|
+
|
|
110
|
+
// 0.5.x Phase 1d Batch 0: backend assembly factory.
|
|
111
|
+
// Truth sources:
|
|
112
|
+
// - Design: `.team/artifacts/0.5.x-backend-assembly-factory-design.md`
|
|
113
|
+
// - CR verdict: `.team/artifacts/0.5.x-backend-factory-cr-verdict.md`
|
|
114
|
+
// (6 constraints anchored inside the module doc)
|
|
115
|
+
pub mod transport_factory;
|
|
116
|
+
|
|
117
|
+
#[cfg(test)]
|
|
118
|
+
pub(crate) mod app_server_test_support;
|