@team-agent/installer 0.5.2 → 0.5.4
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 +3 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +20 -0
- package/crates/team-agent/src/cli/diagnose.rs +191 -31
- package/crates/team-agent/src/cli/emit.rs +5 -0
- package/crates/team-agent/src/cli/mod.rs +213 -89
- package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
- package/crates/team-agent/src/codex_app_server.rs +62 -1
- package/crates/team-agent/src/conpty/backend.rs +120 -8
- package/crates/team-agent/src/coordinator/backoff.rs +88 -2
- package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
- package/crates/team-agent/src/coordinator/health.rs +97 -11
- package/crates/team-agent/src/coordinator/mod.rs +8 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
- package/crates/team-agent/src/coordinator/tick.rs +13 -0
- package/crates/team-agent/src/diagnose/orphans.rs +18 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
- package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
- package/crates/team-agent/src/lib.rs +14 -1
- package/crates/team-agent/src/lifecycle/launch.rs +89 -92
- package/crates/team-agent/src/lifecycle/lock.rs +40 -33
- package/crates/team-agent/src/lifecycle/restart/agent.rs +25 -19
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -6
- package/crates/team-agent/src/lifecycle/restart/selection.rs +47 -18
- package/crates/team-agent/src/lifecycle/restart.rs +16 -6
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +287 -53
- package/crates/team-agent/src/lifecycle/tests/restart.rs +144 -5
- package/crates/team-agent/src/lifecycle/types.rs +1 -0
- package/crates/team-agent/src/mcp_server/wire.rs +6 -7
- package/crates/team-agent/src/messaging/tests/runtime.rs +5 -0
- package/crates/team-agent/src/packaging/tests.rs +41 -6
- package/crates/team-agent/src/packaging/types.rs +31 -3
- package/crates/team-agent/src/platform/argv.rs +324 -0
- package/crates/team-agent/src/platform/errors.rs +95 -0
- package/crates/team-agent/src/platform/file_lock.rs +418 -0
- package/crates/team-agent/src/platform/mod.rs +66 -0
- package/crates/team-agent/src/platform/process.rs +555 -0
- package/crates/team-agent/src/provider/adapter.rs +103 -41
- package/crates/team-agent/src/provider/session/capture.rs +328 -66
- package/crates/team-agent/src/provider/session/resume.rs +30 -28
- package/crates/team-agent/src/provider/session_scan/common.rs +117 -1
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +222 -25
- package/crates/team-agent/src/state/projection.rs +59 -4
- package/crates/team-agent/src/tmux_backend.rs +63 -13
- package/crates/team-agent/src/transport_factory.rs +124 -5
- package/package.json +4 -4
|
@@ -101,11 +101,8 @@ fn layer2_backing_missing_refusal_carries_checked_paths_and_recovery_hint() {
|
|
|
101
101
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
102
102
|
static N: AtomicU32 = AtomicU32::new(0);
|
|
103
103
|
let n = N.fetch_add(1, Ordering::Relaxed);
|
|
104
|
-
let ws =
|
|
105
|
-
"ta_rs_l2_backingmiss_{}_{}",
|
|
106
|
-
std::process::id(),
|
|
107
|
-
n
|
|
108
|
-
));
|
|
104
|
+
let ws =
|
|
105
|
+
std::env::temp_dir().join(format!("ta_rs_l2_backingmiss_{}_{}", std::process::id(), n));
|
|
109
106
|
std::fs::create_dir_all(&ws).unwrap();
|
|
110
107
|
|
|
111
108
|
let missing_rollout = ws.join(".missing-rollout.jsonl");
|
|
@@ -168,6 +165,148 @@ fn layer2_backing_missing_refusal_carries_checked_paths_and_recovery_hint() {
|
|
|
168
165
|
}
|
|
169
166
|
}
|
|
170
167
|
|
|
168
|
+
fn write_codex_identity_rollout(
|
|
169
|
+
workspace: &std::path::Path,
|
|
170
|
+
file_name: &str,
|
|
171
|
+
session_id: &str,
|
|
172
|
+
embedded_agent_id: &str,
|
|
173
|
+
) -> std::path::PathBuf {
|
|
174
|
+
let path = workspace.join(file_name);
|
|
175
|
+
let text = format!(
|
|
176
|
+
"{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}}}}\n\
|
|
177
|
+
{{\"type\":\"turn_context\",\"payload\":{{}}}}\n\
|
|
178
|
+
{{\"type\":\"response_item\",\"payload\":{{\"content\":[{{\"type\":\"input_text\",\"text\":\"You are Team Agent worker `{embedded_agent_id}` with role `fixture`.\"}}]}}}}\n",
|
|
179
|
+
workspace.to_string_lossy()
|
|
180
|
+
);
|
|
181
|
+
std::fs::write(&path, text).unwrap();
|
|
182
|
+
path
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[test]
|
|
186
|
+
fn restart_refuses_codex_session_identity_mismatch_without_allow_fresh() {
|
|
187
|
+
let ws = temp_ws();
|
|
188
|
+
let rollout = write_codex_identity_rollout(
|
|
189
|
+
&ws,
|
|
190
|
+
"rollout-frontend-poison.jsonl",
|
|
191
|
+
"019f3327-c35a-7023-b3cd-1bea93a7a157",
|
|
192
|
+
"ios-dev",
|
|
193
|
+
);
|
|
194
|
+
let state = json!({
|
|
195
|
+
"agents": {
|
|
196
|
+
"frontend": {
|
|
197
|
+
"provider": "codex",
|
|
198
|
+
"status": "running",
|
|
199
|
+
"session_id": "019f3327-c35a-7023-b3cd-1bea93a7a157",
|
|
200
|
+
"rollout_path": rollout.to_string_lossy(),
|
|
201
|
+
"captured_at": "2026-07-05T17:04:04Z",
|
|
202
|
+
"captured_via": "fs_watch",
|
|
203
|
+
"spawn_cwd": ws.to_string_lossy(),
|
|
204
|
+
"first_send_at": "2026-07-05T17:10:00Z"
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
let plan = crate::lifecycle::restart::classify_restart_plan_with_resume_validation(
|
|
210
|
+
Some(&ws),
|
|
211
|
+
&state,
|
|
212
|
+
false,
|
|
213
|
+
)
|
|
214
|
+
.unwrap();
|
|
215
|
+
|
|
216
|
+
assert_eq!(plan.decisions.len(), 1);
|
|
217
|
+
assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
|
|
218
|
+
assert_eq!(plan.unresumable.len(), 1);
|
|
219
|
+
let entry = &plan.unresumable[0];
|
|
220
|
+
assert_eq!(entry.agent_id.as_str(), "frontend");
|
|
221
|
+
assert_eq!(entry.reason, "session_identity_mismatch");
|
|
222
|
+
assert_eq!(
|
|
223
|
+
entry
|
|
224
|
+
.refusal_reason
|
|
225
|
+
.as_ref()
|
|
226
|
+
.map(crate::provider::session::ResumeRefusalReason::wire),
|
|
227
|
+
Some("session_identity_mismatch")
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[test]
|
|
232
|
+
fn restart_identity_probe_reports_real_frontend_ios_dev_mismatch_shape() {
|
|
233
|
+
let ws = temp_ws();
|
|
234
|
+
let rollout = write_codex_identity_rollout(
|
|
235
|
+
&ws,
|
|
236
|
+
"rollout-frontend-poison.jsonl",
|
|
237
|
+
"019f3327-c35a-7023-b3cd-1bea93a7a157",
|
|
238
|
+
"ios-dev",
|
|
239
|
+
);
|
|
240
|
+
let rollout_path = crate::provider::RolloutPath::new(rollout.clone());
|
|
241
|
+
let probe = crate::lifecycle::restart::session_identity_probe_for_agent(
|
|
242
|
+
&aid("frontend"),
|
|
243
|
+
crate::provider::Provider::Codex,
|
|
244
|
+
Some(&rollout_path),
|
|
245
|
+
);
|
|
246
|
+
assert_eq!(probe.identity_ok, Some(false));
|
|
247
|
+
assert_eq!(probe.embedded_agent_id.as_deref(), Some("ios-dev"));
|
|
248
|
+
assert_eq!(probe.rollout_path.as_deref(), Some(rollout.as_path()));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#[test]
|
|
252
|
+
fn restart_allow_fresh_only_fresh_starts_mismatched_codex_worker() {
|
|
253
|
+
let ws = temp_ws();
|
|
254
|
+
let frontend_rollout = write_codex_identity_rollout(
|
|
255
|
+
&ws,
|
|
256
|
+
"rollout-frontend-poison.jsonl",
|
|
257
|
+
"019f3327-c35a-7023-b3cd-1bea93a7a157",
|
|
258
|
+
"ios-dev",
|
|
259
|
+
);
|
|
260
|
+
let backend_rollout = write_codex_identity_rollout(
|
|
261
|
+
&ws,
|
|
262
|
+
"rollout-backend-valid.jsonl",
|
|
263
|
+
"019f3327-backend-valid",
|
|
264
|
+
"backend",
|
|
265
|
+
);
|
|
266
|
+
let state = json!({
|
|
267
|
+
"agents": {
|
|
268
|
+
"frontend": {
|
|
269
|
+
"provider": "codex",
|
|
270
|
+
"status": "running",
|
|
271
|
+
"session_id": "019f3327-c35a-7023-b3cd-1bea93a7a157",
|
|
272
|
+
"rollout_path": frontend_rollout.to_string_lossy(),
|
|
273
|
+
"captured_at": "2026-07-05T17:04:04Z",
|
|
274
|
+
"captured_via": "fs_watch",
|
|
275
|
+
"spawn_cwd": ws.to_string_lossy()
|
|
276
|
+
},
|
|
277
|
+
"backend": {
|
|
278
|
+
"provider": "codex",
|
|
279
|
+
"status": "running",
|
|
280
|
+
"session_id": "019f3327-backend-valid",
|
|
281
|
+
"rollout_path": backend_rollout.to_string_lossy(),
|
|
282
|
+
"captured_at": "2026-07-05T17:04:05Z",
|
|
283
|
+
"captured_via": "fs_watch",
|
|
284
|
+
"spawn_cwd": ws.to_string_lossy()
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
let plan = crate::lifecycle::restart::classify_restart_plan_with_resume_validation(
|
|
290
|
+
Some(&ws),
|
|
291
|
+
&state,
|
|
292
|
+
true,
|
|
293
|
+
)
|
|
294
|
+
.unwrap();
|
|
295
|
+
|
|
296
|
+
assert!(
|
|
297
|
+
plan.unresumable.is_empty(),
|
|
298
|
+
"allow_fresh should convert the poisoned worker to fresh without refusing; got {:?}",
|
|
299
|
+
plan.unresumable
|
|
300
|
+
);
|
|
301
|
+
let decisions = plan
|
|
302
|
+
.decisions
|
|
303
|
+
.iter()
|
|
304
|
+
.map(|decision| (decision.agent_id.as_str(), decision.decision))
|
|
305
|
+
.collect::<std::collections::BTreeMap<_, _>>();
|
|
306
|
+
assert_eq!(decisions.get("frontend"), Some(&ResumeDecision::FreshStart));
|
|
307
|
+
assert_eq!(decisions.get("backend"), Some(&ResumeDecision::Resume));
|
|
308
|
+
}
|
|
309
|
+
|
|
171
310
|
#[test]
|
|
172
311
|
fn unit0_restart_corrupt_first_send_at_blocks_before_resume_classification() {
|
|
173
312
|
// The corrupt-first_send_at branch is the hard-refuse gate that fires
|
|
@@ -440,6 +440,7 @@ pub struct StartedAgent {
|
|
|
440
440
|
pub agent_id: AgentId,
|
|
441
441
|
pub start_mode: StartMode,
|
|
442
442
|
pub target: String,
|
|
443
|
+
pub spawned_at: String,
|
|
443
444
|
pub session_id: Option<SessionId>,
|
|
444
445
|
pub rollout_path: Option<RolloutPath>,
|
|
445
446
|
pub pending_session_id: Option<SessionId>,
|
|
@@ -314,14 +314,13 @@ fn non_empty_env(key: &str) -> Option<String> {
|
|
|
314
314
|
.filter(|value| !value.is_empty())
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
|
|
317
|
+
// 0.5.x Windows portability Batch 3: parent-pid probe routes through
|
|
318
|
+
// `crate::platform::process::current_parent_pid`. Unix uses
|
|
319
|
+
// `libc::getppid`; Windows uses Toolhelp32Snapshot. Both return
|
|
320
|
+
// `Option<u32>`; the legacy `0` fallback (which would have silently
|
|
321
|
+
// degraded MCP metadata + orphan detection on Windows) is gone.
|
|
318
322
|
fn parent_pid() -> u32 {
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
#[cfg(not(unix))]
|
|
323
|
-
fn parent_pid() -> u32 {
|
|
324
|
-
0
|
|
323
|
+
crate::platform::process::current_parent_pid().unwrap_or(0)
|
|
325
324
|
}
|
|
326
325
|
|
|
327
326
|
fn error_response_value(id: RpcId, code: i64, message: String) -> Value {
|
|
@@ -1932,6 +1932,9 @@ fn app_server_state(
|
|
|
1932
1932
|
})
|
|
1933
1933
|
}
|
|
1934
1934
|
|
|
1935
|
+
// 0.5.x Windows portability Batch 5: FakeAppServer uses Unix domain
|
|
1936
|
+
// sockets — Unix-only.
|
|
1937
|
+
#[cfg(unix)]
|
|
1935
1938
|
#[test]
|
|
1936
1939
|
fn app_server_leader_delivery_marks_delivered_without_tmux_transport() {
|
|
1937
1940
|
let ws = tmp_ws("appdeliver");
|
|
@@ -1984,6 +1987,7 @@ fn app_server_leader_delivery_marks_delivered_without_tmux_transport() {
|
|
|
1984
1987
|
);
|
|
1985
1988
|
}
|
|
1986
1989
|
|
|
1990
|
+
#[cfg(unix)]
|
|
1987
1991
|
#[test]
|
|
1988
1992
|
fn app_server_leader_delivery_stale_tuple_fails_closed_with_event() {
|
|
1989
1993
|
let ws = tmp_ws("appstale");
|
|
@@ -2076,6 +2080,7 @@ fn app_server_leader_delivery_fails_closed_on_mode_transport_conflict() {
|
|
|
2076
2080
|
);
|
|
2077
2081
|
}
|
|
2078
2082
|
|
|
2083
|
+
#[cfg(unix)]
|
|
2079
2084
|
#[test]
|
|
2080
2085
|
fn app_server_leader_busy_is_retryable_and_does_not_steer() {
|
|
2081
2086
|
let ws = tmp_ws("appbusy");
|
|
@@ -222,12 +222,47 @@ fn linux_aarch64_is_native() {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
#[test]
|
|
225
|
-
fn
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
225
|
+
fn windows_x8664_is_preview_compile_only_during_batch_0_to_5() {
|
|
226
|
+
// 0.5.x Windows portability CR C-1 (P0):
|
|
227
|
+
// Windows previously claimed `PlatformSupport::Native` but
|
|
228
|
+
// `cargo check --target x86_64-pc-windows-msvc` was RED — the
|
|
229
|
+
// Unix-only `std::os::unix` / `libc::pid_t` calls in
|
|
230
|
+
// tmux_backend/coordinator/lifecycle/packaging never compiled on
|
|
231
|
+
// Windows. Claiming Native was a MUST-NOT-13 假绿承诺 (user
|
|
232
|
+
// installs and immediately hits build failures).
|
|
233
|
+
//
|
|
234
|
+
// This test locks the honest downgrade in place until Batch 6/7
|
|
235
|
+
// real-machine gates pass. Promoting Windows back to `Native`
|
|
236
|
+
// requires:
|
|
237
|
+
// 1. Batch 6 fake-provider smoke on the SSH host (design §Batch 6)
|
|
238
|
+
// 2. Batch 7 real-machine subscription serial (CR §C-5)
|
|
239
|
+
// Both flip this test to `assert PlatformSupport::Native` in a
|
|
240
|
+
// deliberate PR — no silent regen.
|
|
241
|
+
//
|
|
242
|
+
// Truth source: `.team/artifacts/0.5.x-windows-portability-cr-verdict.md` §C-1.
|
|
243
|
+
match platform_support(ReleaseTarget::WindowsX8664) {
|
|
244
|
+
PlatformSupport::PreviewCompileOnly { preview_gate, note } => {
|
|
245
|
+
assert_eq!(preview_gate, "compile_gate");
|
|
246
|
+
assert!(note.contains("compile_gate") || note.contains("cargo check"));
|
|
247
|
+
}
|
|
248
|
+
other => panic!(
|
|
249
|
+
"Windows must stay `PreviewCompileOnly` while the compile gate is RED; \
|
|
250
|
+
promoting back to `Native` requires Batch 6 + Batch 7 real-machine gates. \
|
|
251
|
+
Got {other:?}"
|
|
252
|
+
),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#[test]
|
|
257
|
+
fn windows_x8664_is_not_native_yet_c1_hard_lock() {
|
|
258
|
+
// Belt-and-braces companion for the above — a distinct assertion
|
|
259
|
+
// that fires clearly if a future refactor of `PlatformSupport`
|
|
260
|
+
// changes variant names.
|
|
261
|
+
let support = platform_support(ReleaseTarget::WindowsX8664);
|
|
262
|
+
assert_ne!(
|
|
263
|
+
support,
|
|
264
|
+
PlatformSupport::Native,
|
|
265
|
+
"CR C-1: Windows must NOT be Native until Batch 6/7 gates pass"
|
|
231
266
|
);
|
|
232
267
|
}
|
|
233
268
|
|
|
@@ -162,13 +162,27 @@ pub enum ReleaseTarget {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
/// 平台支持等级(§8:如实声明,不假装兼容)。
|
|
165
|
+
///
|
|
166
|
+
/// 0.5.x Windows portability CR C-1 (P0) 引入 `PreviewCompileOnly`:
|
|
167
|
+
/// Windows 目前 `cargo check --target x86_64-pc-windows-msvc` **仍红**
|
|
168
|
+
/// (三处 non-Unix fallback + platform 层未接入 + Batch 1-4 未完成);
|
|
169
|
+
/// 保持 `Native` 会构成 MUST-NOT-13 假绿承诺 → 用户装 Windows 二进制
|
|
170
|
+
/// 会真报错。降级为 `PreviewCompileOnly` 明示"当前 Windows 尚未通过
|
|
171
|
+
/// 编译门,Batch 6/7 真机订阅测通过前不承诺 Native"。
|
|
172
|
+
/// Truth source: `.team/artifacts/0.5.x-windows-portability-cr-verdict.md` §C-1。
|
|
165
173
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
166
174
|
#[serde(rename_all = "snake_case", tag = "level")]
|
|
167
175
|
pub enum PlatformSupport {
|
|
168
|
-
/// 原生一等(macOS/Linux tmux
|
|
176
|
+
/// 原生一等(macOS/Linux tmux)。
|
|
169
177
|
Native,
|
|
170
178
|
/// 需 WSL + tmux backend(显式标要求,不假装原生)。
|
|
171
179
|
RequiresWslTmux { note: String },
|
|
180
|
+
/// Windows 移植进行中,`cargo check` 仍红或 Batch 6/7 真机订阅测未过。
|
|
181
|
+
/// 用户不应期待可运行的 `.exe`;`preview_gate` 字段说明当前挡位
|
|
182
|
+
/// (`compile_gate` = Batch 0-4 编译门,`fake_provider_smoke` = Batch 6,
|
|
183
|
+
/// `subscription_realmachine` = Batch 7)。
|
|
184
|
+
/// 该档位不承诺可用,仅承诺"正在移植 + 每 Batch 有 CI 编译门追踪"。
|
|
185
|
+
PreviewCompileOnly { preview_gate: String, note: String },
|
|
172
186
|
/// 当前 release 矩阵不覆盖。
|
|
173
187
|
Unsupported { reason: String },
|
|
174
188
|
}
|
|
@@ -179,8 +193,22 @@ pub fn platform_support(target: ReleaseTarget) -> PlatformSupport {
|
|
|
179
193
|
ReleaseTarget::MacosAarch64
|
|
180
194
|
| ReleaseTarget::MacosX8664
|
|
181
195
|
| ReleaseTarget::LinuxX8664
|
|
182
|
-
| ReleaseTarget::LinuxAarch64
|
|
183
|
-
|
|
196
|
+
| ReleaseTarget::LinuxAarch64 => PlatformSupport::Native,
|
|
197
|
+
// 0.5.x Windows portability CR C-1 (P0):
|
|
198
|
+
// `cargo check --target x86_64-pc-windows-msvc` 是当前挡位;
|
|
199
|
+
// Batch 6 fake-provider smoke + Batch 7 真机订阅测通过后
|
|
200
|
+
// 才升回 `Native`。历史 tag 期间 Windows 曾声明为 Native 但
|
|
201
|
+
// 从未通过编译门,构成 MUST-NOT-13 假绿承诺 → 本 batch 降级。
|
|
202
|
+
ReleaseTarget::WindowsX8664 => PlatformSupport::PreviewCompileOnly {
|
|
203
|
+
preview_gate: "compile_gate".to_string(),
|
|
204
|
+
note: "Windows native port in progress; `cargo check --target x86_64-pc-windows-msvc` \
|
|
205
|
+
is the current gate. See \
|
|
206
|
+
`.team/artifacts/0.5.x-windows-portability-survey-design.md` §Batch 0-6 \
|
|
207
|
+
and CR verdict §C-1 for burn-down status. Promote back to `Native` only \
|
|
208
|
+
after Batch 6 (fake-provider smoke) + Batch 7 (subscription real-machine) \
|
|
209
|
+
pass on the SSH host."
|
|
210
|
+
.to_string(),
|
|
211
|
+
},
|
|
184
212
|
}
|
|
185
213
|
}
|
|
186
214
|
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
//! Process argv / environ probe.
|
|
2
|
+
//!
|
|
3
|
+
//! ## Batch 4 real implementation (leader msg_0689a63a9e40)
|
|
4
|
+
//!
|
|
5
|
+
//! Batch 4 promotes this module from a Batch 0 signature-only
|
|
6
|
+
//! scaffold to a **byte-preserving migration** of the process ancestry
|
|
7
|
+
//! + argv/env probes used by:
|
|
8
|
+
//!
|
|
9
|
+
//! - `lifecycle/launch.rs::process_ancestry_argv` +
|
|
10
|
+
//! `process_argv_tokens` + `process_parent_pid` — the
|
|
11
|
+
//! `dangerous_auto_approve` inheritance chain (0.5.0 caller-identity
|
|
12
|
+
//! 钉输入 is exactly this test surface).
|
|
13
|
+
//! - `leader/provider_attribution.rs::process_command_line` +
|
|
14
|
+
//! `process_environment` — leader provider attribution.
|
|
15
|
+
//!
|
|
16
|
+
//! ## Platform matrix
|
|
17
|
+
//!
|
|
18
|
+
//! - **Linux**: `/proc/<pid>/cmdline` (NUL-separated argv) and
|
|
19
|
+
//! `/proc/<pid>/environ` (NUL-separated KEY=VALUE).
|
|
20
|
+
//! - **macOS**: `sysctl(KERN_PROCARGS2)` for argv; environ not
|
|
21
|
+
//! currently probed (matches the pre-batch behavior).
|
|
22
|
+
//! - **non-Linux non-macOS Unix**: `ps -p <pid> -o command=` for argv.
|
|
23
|
+
//! - **Windows**: `NtQueryInformationProcess` + PEB is intrusive; for
|
|
24
|
+
//! Batch 4 we return `None` for both `argv_tokens` and
|
|
25
|
+
//! `environ_text` (design §Batch 4 Verification anchor: "unknown
|
|
26
|
+
//! argv must never infer elevated approval; keep worker permission
|
|
27
|
+
//! at provider default or require explicit user consent"). This is
|
|
28
|
+
//! the honest "we don't know" branch — callers already treat
|
|
29
|
+
//! `None` as "no elevation inherited", so Windows leaders default
|
|
30
|
+
//! to safe (non-dangerous) approval mode.
|
|
31
|
+
//!
|
|
32
|
+
//! ## CR C-3 anchor
|
|
33
|
+
//!
|
|
34
|
+
//! The `Option::None` return on Windows deliberately keeps worker
|
|
35
|
+
//! permission at provider default. Design §Batch 4 Verification: "the
|
|
36
|
+
//! safe direction is to avoid elevation, not to assume bypass" —
|
|
37
|
+
//! `detect_dangerous_approval` in `lifecycle/launch.rs` already
|
|
38
|
+
//! iterates `process_ancestry_argv(...)` and defaults to
|
|
39
|
+
//! `disabled_dangerous_approval()` when it finds no matching flag,
|
|
40
|
+
//! so a Windows probe returning `None` on every step lands on the
|
|
41
|
+
//! same disabled default. **This is intentional**: leaking a
|
|
42
|
+
//! bypass via unknown argv would be a MUST-NOT-13 假绿 failure.
|
|
43
|
+
|
|
44
|
+
/// Return the argv tokens for `pid`, or `None` if unavailable on
|
|
45
|
+
/// this platform.
|
|
46
|
+
///
|
|
47
|
+
/// - Linux: `/proc/<pid>/cmdline` NUL-separated (byte-preserving
|
|
48
|
+
/// migration of `lifecycle/launch.rs:3648-3656`).
|
|
49
|
+
/// - macOS: `sysctl(KERN_PROCARGS2)` (migration of
|
|
50
|
+
/// `lifecycle/launch.rs:3659-3711`).
|
|
51
|
+
/// - Other Unix: `ps -p <pid> -o command=` split on whitespace
|
|
52
|
+
/// (migration of `lifecycle/launch.rs:3714-3729`).
|
|
53
|
+
/// - Windows: `None` (design §Batch 4 conservative fallback — never
|
|
54
|
+
/// infer elevated approval from unknown argv).
|
|
55
|
+
pub fn argv_tokens(pid: u32) -> Option<Vec<String>> {
|
|
56
|
+
#[cfg(target_os = "linux")]
|
|
57
|
+
{
|
|
58
|
+
argv_tokens_linux(pid)
|
|
59
|
+
}
|
|
60
|
+
#[cfg(target_os = "macos")]
|
|
61
|
+
{
|
|
62
|
+
argv_tokens_macos(pid)
|
|
63
|
+
}
|
|
64
|
+
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
|
|
65
|
+
{
|
|
66
|
+
argv_tokens_ps_fallback(pid)
|
|
67
|
+
}
|
|
68
|
+
#[cfg(windows)]
|
|
69
|
+
{
|
|
70
|
+
let _ = pid;
|
|
71
|
+
None
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Return the environ text (NUL-joined) for `pid`, or `None`.
|
|
76
|
+
///
|
|
77
|
+
/// - Linux: `/proc/<pid>/environ` verbatim (migration of
|
|
78
|
+
/// `leader/provider_attribution.rs:113-115`).
|
|
79
|
+
/// - Other platforms: `None` (matches the pre-batch
|
|
80
|
+
/// `#[cfg(not(target_os="linux"))]` stub in
|
|
81
|
+
/// `leader/provider_attribution.rs:135-139`).
|
|
82
|
+
pub fn environ_text(pid: u32) -> Option<String> {
|
|
83
|
+
#[cfg(target_os = "linux")]
|
|
84
|
+
{
|
|
85
|
+
environ_text_linux(pid)
|
|
86
|
+
}
|
|
87
|
+
#[cfg(not(target_os = "linux"))]
|
|
88
|
+
{
|
|
89
|
+
let _ = pid;
|
|
90
|
+
None
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Return the parent PID of `pid`, or `None` if unavailable.
|
|
95
|
+
///
|
|
96
|
+
/// Byte-preserving migration of `lifecycle/launch.rs::process_parent_pid`:
|
|
97
|
+
/// - Unix: `ps -p <pid> -o ppid=` parsed as u32.
|
|
98
|
+
/// - Windows: Toolhelp32 snapshot walked for the entry matching
|
|
99
|
+
/// `pid`. Same technique as `platform::process::current_parent_pid`.
|
|
100
|
+
pub fn parent_pid(pid: u32) -> Option<u32> {
|
|
101
|
+
#[cfg(unix)]
|
|
102
|
+
{
|
|
103
|
+
parent_pid_unix(pid)
|
|
104
|
+
}
|
|
105
|
+
#[cfg(windows)]
|
|
106
|
+
{
|
|
107
|
+
parent_pid_windows(pid)
|
|
108
|
+
}
|
|
109
|
+
#[cfg(not(any(unix, windows)))]
|
|
110
|
+
{
|
|
111
|
+
let _ = pid;
|
|
112
|
+
None
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
117
|
+
// Linux implementation.
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
#[cfg(target_os = "linux")]
|
|
121
|
+
fn argv_tokens_linux(pid: u32) -> Option<Vec<String>> {
|
|
122
|
+
let bytes = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
|
|
123
|
+
let argv_tokens = String::from_utf8_lossy(&bytes)
|
|
124
|
+
.split('\0')
|
|
125
|
+
.filter(|token| !token.is_empty())
|
|
126
|
+
.map(str::to_string)
|
|
127
|
+
.collect::<Vec<_>>();
|
|
128
|
+
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[cfg(target_os = "linux")]
|
|
132
|
+
fn environ_text_linux(pid: u32) -> Option<String> {
|
|
133
|
+
String::from_utf8(std::fs::read(format!("/proc/{pid}/environ")).ok()?).ok()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
137
|
+
// macOS implementation.
|
|
138
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
#[cfg(target_os = "macos")]
|
|
141
|
+
fn argv_tokens_macos(pid: u32) -> Option<Vec<String>> {
|
|
142
|
+
use std::mem::size_of;
|
|
143
|
+
let mut mib = [
|
|
144
|
+
libc::CTL_KERN,
|
|
145
|
+
libc::KERN_PROCARGS2,
|
|
146
|
+
i32::try_from(pid).ok()?,
|
|
147
|
+
];
|
|
148
|
+
let mut size = 0usize;
|
|
149
|
+
let rc = unsafe {
|
|
150
|
+
libc::sysctl(
|
|
151
|
+
mib.as_mut_ptr(),
|
|
152
|
+
mib.len() as u32,
|
|
153
|
+
std::ptr::null_mut(),
|
|
154
|
+
&mut size,
|
|
155
|
+
std::ptr::null_mut(),
|
|
156
|
+
0,
|
|
157
|
+
)
|
|
158
|
+
};
|
|
159
|
+
if rc != 0 || size <= size_of::<libc::c_int>() {
|
|
160
|
+
return None;
|
|
161
|
+
}
|
|
162
|
+
let mut buf = vec![0u8; size];
|
|
163
|
+
let rc = unsafe {
|
|
164
|
+
libc::sysctl(
|
|
165
|
+
mib.as_mut_ptr(),
|
|
166
|
+
mib.len() as u32,
|
|
167
|
+
buf.as_mut_ptr().cast(),
|
|
168
|
+
&mut size,
|
|
169
|
+
std::ptr::null_mut(),
|
|
170
|
+
0,
|
|
171
|
+
)
|
|
172
|
+
};
|
|
173
|
+
if rc != 0 || size <= size_of::<libc::c_int>() {
|
|
174
|
+
return None;
|
|
175
|
+
}
|
|
176
|
+
let argc = i32::from_ne_bytes(buf.get(..size_of::<libc::c_int>())?.try_into().ok()?) as usize;
|
|
177
|
+
let mut offset = size_of::<libc::c_int>();
|
|
178
|
+
while offset < size && buf[offset] != 0 {
|
|
179
|
+
offset += 1;
|
|
180
|
+
}
|
|
181
|
+
while offset < size && buf[offset] == 0 {
|
|
182
|
+
offset += 1;
|
|
183
|
+
}
|
|
184
|
+
let raw = String::from_utf8_lossy(&buf[offset..size]);
|
|
185
|
+
let argv_tokens = raw
|
|
186
|
+
.split('\0')
|
|
187
|
+
.filter(|token| !token.is_empty())
|
|
188
|
+
.take(argc)
|
|
189
|
+
.map(str::to_string)
|
|
190
|
+
.collect::<Vec<_>>();
|
|
191
|
+
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
195
|
+
// Generic Unix fallback (`ps -p <pid> -o ...`).
|
|
196
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
|
|
199
|
+
fn argv_tokens_ps_fallback(pid: u32) -> Option<Vec<String>> {
|
|
200
|
+
let output = std::process::Command::new("ps")
|
|
201
|
+
.args(["-p", &pid.to_string(), "-o", "command="])
|
|
202
|
+
.output()
|
|
203
|
+
.ok()?;
|
|
204
|
+
if !output.status.success() {
|
|
205
|
+
return None;
|
|
206
|
+
}
|
|
207
|
+
let text = String::from_utf8_lossy(&output.stdout);
|
|
208
|
+
let argv_tokens = text
|
|
209
|
+
.split_whitespace()
|
|
210
|
+
.filter(|token| !token.is_empty())
|
|
211
|
+
.map(str::to_string)
|
|
212
|
+
.collect::<Vec<_>>();
|
|
213
|
+
(!argv_tokens.is_empty()).then_some(argv_tokens)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[cfg(unix)]
|
|
217
|
+
fn parent_pid_unix(pid: u32) -> Option<u32> {
|
|
218
|
+
let output = std::process::Command::new("ps")
|
|
219
|
+
.args(["-p", &pid.to_string(), "-o", "ppid="])
|
|
220
|
+
.output()
|
|
221
|
+
.ok()?;
|
|
222
|
+
if !output.status.success() {
|
|
223
|
+
return None;
|
|
224
|
+
}
|
|
225
|
+
String::from_utf8_lossy(&output.stdout)
|
|
226
|
+
.trim()
|
|
227
|
+
.parse::<u32>()
|
|
228
|
+
.ok()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
232
|
+
// Windows implementation.
|
|
233
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
#[cfg(windows)]
|
|
236
|
+
fn parent_pid_windows(pid: u32) -> Option<u32> {
|
|
237
|
+
use windows::Win32::System::Diagnostics::ToolHelp::{
|
|
238
|
+
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
|
239
|
+
TH32CS_SNAPPROCESS,
|
|
240
|
+
};
|
|
241
|
+
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }.ok()?;
|
|
242
|
+
let mut entry = PROCESSENTRY32W {
|
|
243
|
+
dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
|
|
244
|
+
..Default::default()
|
|
245
|
+
};
|
|
246
|
+
let mut result = unsafe { Process32FirstW(snapshot, &mut entry) };
|
|
247
|
+
while result.is_ok() {
|
|
248
|
+
if entry.th32ProcessID == pid {
|
|
249
|
+
let ppid = entry.th32ParentProcessID;
|
|
250
|
+
unsafe {
|
|
251
|
+
let _ = windows::Win32::Foundation::CloseHandle(snapshot);
|
|
252
|
+
}
|
|
253
|
+
return Some(ppid);
|
|
254
|
+
}
|
|
255
|
+
result = unsafe { Process32NextW(snapshot, &mut entry) };
|
|
256
|
+
}
|
|
257
|
+
unsafe {
|
|
258
|
+
let _ = windows::Win32::Foundation::CloseHandle(snapshot);
|
|
259
|
+
}
|
|
260
|
+
None
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
264
|
+
// Tests.
|
|
265
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
#[cfg(test)]
|
|
268
|
+
mod tests {
|
|
269
|
+
use super::*;
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn argv_tokens_returns_some_for_own_pid_on_unix_or_none_on_windows() {
|
|
273
|
+
// Byte-preserving migration of the ancestry probe. On Unix
|
|
274
|
+
// this must return a non-empty argv for our own process (test
|
|
275
|
+
// binary). On Windows the honest `None` return is intentional:
|
|
276
|
+
// callers see the "no elevation inherited" default branch,
|
|
277
|
+
// which is safer than reading unknown argv (CR C-3 anchor).
|
|
278
|
+
let my = std::process::id();
|
|
279
|
+
let argv = argv_tokens(my);
|
|
280
|
+
#[cfg(unix)]
|
|
281
|
+
{
|
|
282
|
+
let argv = argv.expect("unix must return Some argv for own pid");
|
|
283
|
+
assert!(!argv.is_empty());
|
|
284
|
+
// At least argv[0] should look like a test binary path.
|
|
285
|
+
let argv0 = &argv[0];
|
|
286
|
+
assert!(!argv0.is_empty());
|
|
287
|
+
}
|
|
288
|
+
#[cfg(windows)]
|
|
289
|
+
{
|
|
290
|
+
// Windows conservative fallback: None, so
|
|
291
|
+
// `detect_dangerous_approval` defaults to disabled.
|
|
292
|
+
assert!(argv.is_none());
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn parent_pid_returns_some_on_both_platforms() {
|
|
298
|
+
// Batch 4: parent_pid via `ps -o ppid=` (unix) or Toolhelp32
|
|
299
|
+
// (windows) — both must resolve our own ppid.
|
|
300
|
+
let my = std::process::id();
|
|
301
|
+
let ppid = parent_pid(my);
|
|
302
|
+
assert!(ppid.is_some(), "parent_pid must resolve own pid on both unix and windows");
|
|
303
|
+
assert!(ppid.unwrap() > 0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#[test]
|
|
307
|
+
fn environ_text_linux_only_returns_something_for_own_pid() {
|
|
308
|
+
// The environ probe is Linux-only by design (matches
|
|
309
|
+
// `leader/provider_attribution.rs:113-115`). Other platforms
|
|
310
|
+
// honestly return None; callers already treat that as
|
|
311
|
+
// "no env-based attribution".
|
|
312
|
+
let my = std::process::id();
|
|
313
|
+
let env = environ_text(my);
|
|
314
|
+
#[cfg(target_os = "linux")]
|
|
315
|
+
{
|
|
316
|
+
let env = env.expect("linux must return Some environ");
|
|
317
|
+
assert!(env.contains('='));
|
|
318
|
+
}
|
|
319
|
+
#[cfg(not(target_os = "linux"))]
|
|
320
|
+
{
|
|
321
|
+
assert!(env.is_none());
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|