@team-agent/installer 0.5.86 → 0.5.89
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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +2 -2
- package/crates/team-agent/src/cli/emit.rs +56 -12
- package/crates/team-agent/src/cli/status_port/brief.rs +187 -7
- package/crates/team-agent/src/compiler.rs +4 -7
- package/crates/team-agent/src/lifecycle/launch/identity.rs +39 -21
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +3 -3
- package/crates/team-agent/src/model/enums.rs +17 -3
- package/crates/team-agent/src/model/spec.rs +8 -7
- package/crates/team-agent/src/provider/wire.rs +1 -1
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -314,7 +314,7 @@ fn warn_ignored_owner_team_id(team_dir: &std::path::Path) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/// `cmd_status`(`commands.py:90`)。CLI status 统一为只读七字段 brief;
|
|
317
|
-
/// `--json` 与人读路径共享
|
|
317
|
+
/// `--json` 与人读路径共享 native tmux/process projection;`--summary`/`--detail` 保留参数兼容性但不暴露诊断。
|
|
318
318
|
#[cfg(test)]
|
|
319
319
|
pub(crate) fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
|
|
320
320
|
cmd_status_for_team(args, args.team.as_deref())
|
|
@@ -357,7 +357,7 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
// Status brief is deliberately independent of the legacy RuntimeSnapshot:
|
|
360
|
-
// it performs one
|
|
360
|
+
// it performs one native tmux/process sample and exposes exactly seven
|
|
361
361
|
// fields. `--summary` and `--detail` remain parser-compatible but do not
|
|
362
362
|
// re-enable history, runtime diagnostics, or reminder text.
|
|
363
363
|
if args.json {
|
|
@@ -417,7 +417,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
417
417
|
)
|
|
418
418
|
.to_string(),
|
|
419
419
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
420
|
-
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出七字段:name/provider/runtime_status/activity/health/session_name/tmux_command;人读与 --json
|
|
420
|
+
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出七字段:name/provider/runtime_status/activity/health/session_name/tmux_command;人读与 --json 使用同一投影。缺少可靠定位或原生 tmux/process 采样时显示 unknown;tmux_command 可复制到对应目标。--summary/--detail 仅保留兼容性,不增加诊断字段。".to_string(),
|
|
421
421
|
Some("models") => "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, and display name. Cursor uses the local `agent --list-models` catalog.".to_string(),
|
|
422
422
|
Some("leaders") => "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]\n\nLists LIVE leaders by default. Use --all to include retained STALE entries, --stale to inspect only STALE entries, QUERY or --search TEXT to match workspace/team/name fields, and --prune to remove only entries proven terminal by canonical state. --dry-run is valid only with --prune.".to_string(),
|
|
423
423
|
Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
|
|
@@ -1069,17 +1069,37 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
|
|
|
1069
1069
|
));
|
|
1070
1070
|
}
|
|
1071
1071
|
let parsed = parse_args(args);
|
|
1072
|
-
let
|
|
1073
|
-
|
|
1074
|
-
.
|
|
1075
|
-
.
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1072
|
+
let explicit_workspace = parsed
|
|
1073
|
+
.workspace
|
|
1074
|
+
.as_deref()
|
|
1075
|
+
.map(|path| resolve_cli_path(cwd, path));
|
|
1076
|
+
let positional_agents_dir = parsed.positionals.first().map(PathBuf::from).map(|path| {
|
|
1077
|
+
if path.is_absolute() {
|
|
1078
|
+
path
|
|
1079
|
+
} else if let Some(workspace) = explicit_workspace.as_ref() {
|
|
1080
|
+
workspace.join(path)
|
|
1081
|
+
} else {
|
|
1082
|
+
cwd.join(path)
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
let workspace = explicit_workspace
|
|
1086
|
+
.or_else(|| {
|
|
1087
|
+
let path = positional_agents_dir.as_ref()?;
|
|
1088
|
+
if let Some(run_workspace) = path
|
|
1089
|
+
.ancestors()
|
|
1090
|
+
.find(|ancestor| ancestor.file_name().and_then(|name| name.to_str()) == Some(".team"))
|
|
1091
|
+
.and_then(Path::parent)
|
|
1092
|
+
{
|
|
1093
|
+
return Some(run_workspace.to_path_buf());
|
|
1094
|
+
}
|
|
1095
|
+
path.join("TEAM.md").is_file().then(|| path.to_path_buf())
|
|
1096
|
+
})
|
|
1097
|
+
.unwrap_or_else(|| cwd.to_path_buf());
|
|
1098
|
+
// A positional team directory containing TEAM.md is a complete standalone
|
|
1099
|
+
// workspace. Use it as the runtime workspace unless --workspace explicitly
|
|
1100
|
+
// selects another root; this prevents host owner context/state leaking into
|
|
1101
|
+
// an independent quick-start target.
|
|
1102
|
+
let agents_dir = positional_agents_dir.unwrap_or_else(|| workspace.clone());
|
|
1083
1103
|
// 0.5.x Phase 1d Batch 2: validate the `--backend` literal up-front
|
|
1084
1104
|
// so users get a fast, actionable error instead of a downstream
|
|
1085
1105
|
// factory refusal. Accept the same literals as the factory
|
|
@@ -2565,6 +2585,30 @@ mod tests {
|
|
|
2565
2585
|
let _ = std::fs::remove_dir_all(&ws);
|
|
2566
2586
|
}
|
|
2567
2587
|
|
|
2588
|
+
#[test]
|
|
2589
|
+
fn ux_quick_start_positional_team_dir_is_standalone_workspace() {
|
|
2590
|
+
let cwd = tmp_workspace();
|
|
2591
|
+
let ws = tmp_workspace();
|
|
2592
|
+
std::fs::write(ws.join("TEAM.md"), "# team\n").unwrap();
|
|
2593
|
+
let args = quick_start_args(&cli_argv(&[&ws.to_string_lossy()]), &cwd).unwrap();
|
|
2594
|
+
assert_eq!(args.workspace, ws);
|
|
2595
|
+
assert_eq!(args.agents_dir, ws);
|
|
2596
|
+
let _ = std::fs::remove_dir_all(&cwd);
|
|
2597
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
#[test]
|
|
2601
|
+
fn ux_quick_start_team_dir_under_dot_team_uses_project_workspace() {
|
|
2602
|
+
let cwd = tmp_workspace();
|
|
2603
|
+
let team_dir = cwd.join(".team/current");
|
|
2604
|
+
std::fs::create_dir_all(&team_dir).unwrap();
|
|
2605
|
+
std::fs::write(team_dir.join("TEAM.md"), "# team\n").unwrap();
|
|
2606
|
+
let args = quick_start_args(&cli_argv(&[".team/current"]), &cwd).unwrap();
|
|
2607
|
+
assert_eq!(args.workspace, cwd);
|
|
2608
|
+
assert_eq!(args.agents_dir, team_dir);
|
|
2609
|
+
let _ = std::fs::remove_dir_all(&cwd);
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2568
2612
|
// ── E8 (N38): 未知子命令 → 最近似建议(additive,不破坏 golden invalid-choice 行) ──
|
|
2569
2613
|
#[test]
|
|
2570
2614
|
fn e8_unknown_subcommand_suggests_nearest_known_command() {
|
|
@@ -1,38 +1,66 @@
|
|
|
1
|
-
//! Seven-field, read-only status projection backed by
|
|
1
|
+
//! Seven-field, read-only status projection backed by native tmux and process samples.
|
|
2
2
|
//!
|
|
3
3
|
//! This intentionally does not reuse RuntimeSnapshot: the legacy snapshot reads
|
|
4
4
|
//! coordinator/db/history and carries diagnostic fields that are outside the
|
|
5
5
|
//! status brief contract.
|
|
6
6
|
|
|
7
|
-
use serde::Deserialize;
|
|
8
7
|
use serde_json::{json, Map, Value};
|
|
9
|
-
use sha2::{Digest, Sha256};
|
|
10
8
|
use std::collections::BTreeMap;
|
|
9
|
+
use std::path::Path;
|
|
10
|
+
use std::process::Command;
|
|
11
|
+
|
|
12
|
+
#[cfg(test)]
|
|
13
|
+
use serde::Deserialize;
|
|
14
|
+
#[cfg(test)]
|
|
15
|
+
use sha2::{Digest, Sha256};
|
|
16
|
+
#[cfg(test)]
|
|
11
17
|
use std::fs::{File, OpenOptions};
|
|
18
|
+
#[cfg(test)]
|
|
12
19
|
use std::io::Read;
|
|
13
|
-
|
|
14
|
-
use std::
|
|
20
|
+
#[cfg(test)]
|
|
21
|
+
use std::path::PathBuf;
|
|
22
|
+
#[cfg(test)]
|
|
23
|
+
use std::process::{Child, ExitStatus, Stdio};
|
|
24
|
+
#[cfg(test)]
|
|
15
25
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
26
|
+
#[cfg(test)]
|
|
16
27
|
use std::sync::mpsc;
|
|
17
28
|
#[cfg(test)]
|
|
18
29
|
use std::sync::Arc;
|
|
30
|
+
#[cfg(test)]
|
|
19
31
|
use std::thread;
|
|
32
|
+
#[cfg(test)]
|
|
20
33
|
use std::time::{Duration, Instant};
|
|
21
34
|
|
|
35
|
+
#[cfg(test)]
|
|
22
36
|
const NODEPROBE_TIMEOUT: Duration = Duration::from_secs(2);
|
|
37
|
+
#[cfg(test)]
|
|
23
38
|
const NODEPROBE_OUTPUT_LIMIT: u64 = 1024 * 1024;
|
|
39
|
+
#[cfg(test)]
|
|
24
40
|
const NODEPROBE_KILL_GRACE: Duration = Duration::from_millis(100);
|
|
41
|
+
#[cfg(test)]
|
|
25
42
|
const NODEPROBE_RECEIPT_LIMIT: u64 = 64 * 1024;
|
|
43
|
+
#[cfg(test)]
|
|
26
44
|
const NODEPROBE_BINARY_LIMIT: u64 = 64 * 1024 * 1024;
|
|
45
|
+
#[cfg(test)]
|
|
27
46
|
const NODEPROBE_READ_CHUNK: usize = 64 * 1024;
|
|
47
|
+
#[cfg(test)]
|
|
28
48
|
const NODEPROBE_NAME: &str = "nodeprobe";
|
|
49
|
+
#[cfg(test)]
|
|
29
50
|
const NODEPROBE_RECEIPT_SUFFIX: &str = ".capability.json";
|
|
51
|
+
#[cfg(test)]
|
|
30
52
|
const NODEPROBE_RECEIPT_SCHEMA: &str = "nodeprobe-capability-v1";
|
|
53
|
+
#[cfg(test)]
|
|
31
54
|
const NODEPROBE_SOURCE_REPO: &str = "Florious95/team-agent-scratch/nodeprobe";
|
|
55
|
+
#[cfg(test)]
|
|
32
56
|
const NODEPROBE_SOURCE_COMMIT: &str = "ff316dc0afe8ab280e61d30934e7624579be6224";
|
|
57
|
+
#[cfg(test)]
|
|
33
58
|
const NODEPROBE_SOURCE_TREE: &str = "5217a41aa914ddcb72c27f39f1b4af9ead68b1b6";
|
|
59
|
+
#[cfg(test)]
|
|
34
60
|
const NODEPROBE_REPORT_SCHEMA: u64 = 1;
|
|
61
|
+
#[cfg(test)]
|
|
35
62
|
const NODEPROBE_CAPABILITIES: &[&str] = &["tmux.list-panes", "ps.pid_ppid_stat_comm"];
|
|
63
|
+
#[cfg(test)]
|
|
36
64
|
const NODEPROBE_FORBIDDEN: &[&str] = &[
|
|
37
65
|
"tmux.capture-pane",
|
|
38
66
|
"tmux.attach",
|
|
@@ -47,6 +75,7 @@ thread_local! {
|
|
|
47
75
|
static TEST_RESOLVER_DELAY: std::cell::RefCell<Option<TestResolverDelay>> = const { std::cell::RefCell::new(None) };
|
|
48
76
|
}
|
|
49
77
|
|
|
78
|
+
#[cfg(test)]
|
|
50
79
|
static NODEPROBE_RESOLVER_ACTIVE: AtomicBool = AtomicBool::new(false);
|
|
51
80
|
|
|
52
81
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
@@ -90,6 +119,7 @@ struct ProbeResult {
|
|
|
90
119
|
}
|
|
91
120
|
|
|
92
121
|
#[derive(Debug, Deserialize)]
|
|
122
|
+
#[cfg(test)]
|
|
93
123
|
struct NodeprobeCapabilityReceipt {
|
|
94
124
|
schema: String,
|
|
95
125
|
binary: String,
|
|
@@ -383,6 +413,126 @@ fn first_string(value: &Value, keys: &[&str]) -> Option<String> {
|
|
|
383
413
|
}
|
|
384
414
|
|
|
385
415
|
fn probe_registered_endpoints(nodes: &[RegisteredNode]) -> BTreeMap<String, Option<ProbeResult>> {
|
|
416
|
+
#[cfg(test)]
|
|
417
|
+
{
|
|
418
|
+
// Keep the legacy fixture seam for existing hermetic tests; production
|
|
419
|
+
// status never searches for or executes nodeprobe.
|
|
420
|
+
if TEST_NODEPROBE.with(|slot| slot.borrow().is_some()) {
|
|
421
|
+
return probe_registered_endpoints_with_test_nodeprobe(nodes);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let mut endpoints = BTreeMap::new();
|
|
425
|
+
for endpoint in nodes.iter().filter_map(|node| node.endpoint.as_deref()) {
|
|
426
|
+
if endpoints.contains_key(endpoint) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
endpoints.insert(
|
|
430
|
+
endpoint.to_string(),
|
|
431
|
+
sample_native_endpoint(endpoint, nodes),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
endpoints
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
fn sample_native_endpoint(
|
|
438
|
+
endpoint: &str,
|
|
439
|
+
registered: &[RegisteredNode],
|
|
440
|
+
) -> Option<ProbeResult> {
|
|
441
|
+
let output = run_native_tmux_list_panes(endpoint)?;
|
|
442
|
+
let nodes = output
|
|
443
|
+
.lines()
|
|
444
|
+
.filter_map(|line| parse_native_pane(line, endpoint, registered))
|
|
445
|
+
.collect::<Vec<_>>();
|
|
446
|
+
Some(ProbeResult { nodes })
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fn run_native_tmux_list_panes(endpoint: &str) -> Option<String> {
|
|
450
|
+
const FORMAT: &str = "#{session_name}\t#{window_name}\t#{pane_id}\t#{pane_pid}\t#{pane_current_command}";
|
|
451
|
+
let flag = if Path::new(endpoint).is_absolute() { "-S" } else { "-L" };
|
|
452
|
+
let output = Command::new("tmux")
|
|
453
|
+
.arg(flag)
|
|
454
|
+
.arg(endpoint)
|
|
455
|
+
.args(["list-panes", "-a", "-F", FORMAT])
|
|
456
|
+
.output()
|
|
457
|
+
.ok()?;
|
|
458
|
+
output.status.success().then(|| String::from_utf8(output.stdout).ok())?
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
fn parse_native_pane(
|
|
462
|
+
line: &str,
|
|
463
|
+
endpoint: &str,
|
|
464
|
+
registered: &[RegisteredNode],
|
|
465
|
+
) -> Option<ProbeNode> {
|
|
466
|
+
let mut fields = line.split('\t');
|
|
467
|
+
let session = fields.next()?.to_string();
|
|
468
|
+
let window = fields.next()?.to_string();
|
|
469
|
+
let pane = fields.next()?.to_string();
|
|
470
|
+
let pid = fields.next().filter(|value| !value.is_empty());
|
|
471
|
+
let command = fields.next().unwrap_or_default();
|
|
472
|
+
if session.is_empty() || window.is_empty() || pane.is_empty() {
|
|
473
|
+
return None;
|
|
474
|
+
}
|
|
475
|
+
let matched = registered.iter().find(|node| {
|
|
476
|
+
node.endpoint.as_deref() == Some(endpoint)
|
|
477
|
+
&& node.session.as_deref() == Some(session.as_str())
|
|
478
|
+
&& node.window.as_deref().is_none_or(|value| value == window)
|
|
479
|
+
&& node.pane.as_deref() == Some(pane.as_str())
|
|
480
|
+
});
|
|
481
|
+
if let Some(pid) = pid {
|
|
482
|
+
let snapshot = process_snapshot(pid)?;
|
|
483
|
+
if snapshot.zombie {
|
|
484
|
+
return None;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
let provider = matched
|
|
488
|
+
.map(|node| node.provider.clone())
|
|
489
|
+
.unwrap_or_else(|| "unknown".to_string());
|
|
490
|
+
let activity = if command.is_empty() {
|
|
491
|
+
"unknown"
|
|
492
|
+
} else if matches!(command, "bash" | "fish" | "sh" | "zsh" | "-bash" | "-zsh") {
|
|
493
|
+
"idle"
|
|
494
|
+
} else {
|
|
495
|
+
"working"
|
|
496
|
+
};
|
|
497
|
+
Some(ProbeNode {
|
|
498
|
+
socket: endpoint.to_string(),
|
|
499
|
+
session: session.clone(),
|
|
500
|
+
window: window.clone(),
|
|
501
|
+
pane,
|
|
502
|
+
provider,
|
|
503
|
+
activity: activity.to_string(),
|
|
504
|
+
health: "normal".to_string(),
|
|
505
|
+
session_name: Some(session),
|
|
506
|
+
evidence_method: Some("native_tmux".to_string()),
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
struct ProcessSnapshot {
|
|
511
|
+
zombie: bool,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
fn process_snapshot(pid: &str) -> Option<ProcessSnapshot> {
|
|
515
|
+
let output = Command::new("ps")
|
|
516
|
+
.args(["-o", "pid=,ppid=,stat=,comm=", "-p", pid])
|
|
517
|
+
.output()
|
|
518
|
+
.ok()?;
|
|
519
|
+
if !output.status.success() || String::from_utf8_lossy(&output.stdout).trim().is_empty() {
|
|
520
|
+
return None;
|
|
521
|
+
}
|
|
522
|
+
let output_text = String::from_utf8_lossy(&output.stdout);
|
|
523
|
+
let stat = output_text
|
|
524
|
+
.split_whitespace()
|
|
525
|
+
.nth(2)
|
|
526
|
+
.unwrap_or_default();
|
|
527
|
+
Some(ProcessSnapshot {
|
|
528
|
+
zombie: stat.contains('Z'),
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
#[cfg(test)]
|
|
533
|
+
fn probe_registered_endpoints_with_test_nodeprobe(
|
|
534
|
+
nodes: &[RegisteredNode],
|
|
535
|
+
) -> BTreeMap<String, Option<ProbeResult>> {
|
|
386
536
|
let deadline = Instant::now() + NODEPROBE_TIMEOUT;
|
|
387
537
|
let candidates = nodeprobe_candidates();
|
|
388
538
|
let resolver_options = resolver_options();
|
|
@@ -410,6 +560,7 @@ fn probe_registered_endpoints(nodes: &[RegisteredNode]) -> BTreeMap<String, Opti
|
|
|
410
560
|
endpoints
|
|
411
561
|
}
|
|
412
562
|
|
|
563
|
+
#[cfg(test)]
|
|
413
564
|
fn run_nodeprobe(binary: &Path, endpoint: &str, deadline: Instant) -> Option<Vec<u8>> {
|
|
414
565
|
if Instant::now() >= deadline {
|
|
415
566
|
return None;
|
|
@@ -476,6 +627,7 @@ fn run_nodeprobe(binary: &Path, endpoint: &str, deadline: Instant) -> Option<Vec
|
|
|
476
627
|
}
|
|
477
628
|
}
|
|
478
629
|
|
|
630
|
+
#[cfg(test)]
|
|
479
631
|
fn wait_child_bounded(child: &mut Child, deadline: Instant) -> Option<ExitStatus> {
|
|
480
632
|
loop {
|
|
481
633
|
match child.try_wait() {
|
|
@@ -493,6 +645,7 @@ fn wait_child_bounded(child: &mut Child, deadline: Instant) -> Option<ExitStatus
|
|
|
493
645
|
}
|
|
494
646
|
}
|
|
495
647
|
|
|
648
|
+
#[cfg(test)]
|
|
496
649
|
fn terminate_nodeprobe(child: &mut Child) {
|
|
497
650
|
#[cfg(unix)]
|
|
498
651
|
{
|
|
@@ -509,6 +662,7 @@ fn terminate_nodeprobe(child: &mut Child) {
|
|
|
509
662
|
}
|
|
510
663
|
}
|
|
511
664
|
|
|
665
|
+
#[cfg(test)]
|
|
512
666
|
fn spawn_nodeprobe(binary: &Path, endpoint: &str) -> Option<Child> {
|
|
513
667
|
let flag = if Path::new(endpoint).is_absolute() {
|
|
514
668
|
"-S"
|
|
@@ -522,11 +676,13 @@ fn spawn_nodeprobe(binary: &Path, endpoint: &str) -> Option<Child> {
|
|
|
522
676
|
}
|
|
523
677
|
|
|
524
678
|
#[derive(Clone, Default)]
|
|
679
|
+
#[cfg(test)]
|
|
525
680
|
struct ResolverOptions {
|
|
526
681
|
#[cfg(test)]
|
|
527
682
|
delay_after_selection: Option<TestResolverDelay>,
|
|
528
683
|
}
|
|
529
684
|
|
|
685
|
+
#[cfg(test)]
|
|
530
686
|
fn resolver_options() -> ResolverOptions {
|
|
531
687
|
#[cfg(test)]
|
|
532
688
|
{
|
|
@@ -543,11 +699,13 @@ fn resolver_options() -> ResolverOptions {
|
|
|
543
699
|
// A resolver that is stuck in an uninterruptible filesystem syscall may outlive
|
|
544
700
|
// the caller deadline. It owns only its candidate work; the active gate prevents
|
|
545
701
|
// repeated status calls from accumulating more detached resolver threads.
|
|
702
|
+
#[cfg(test)]
|
|
546
703
|
struct ResolverActiveGuard {
|
|
547
704
|
#[cfg(test)]
|
|
548
705
|
finished_signal: Option<ResolverDelaySignal>,
|
|
549
706
|
}
|
|
550
707
|
|
|
708
|
+
#[cfg(test)]
|
|
551
709
|
impl Drop for ResolverActiveGuard {
|
|
552
710
|
fn drop(&mut self) {
|
|
553
711
|
NODEPROBE_RESOLVER_ACTIVE.store(false, Ordering::Release);
|
|
@@ -558,6 +716,7 @@ impl Drop for ResolverActiveGuard {
|
|
|
558
716
|
}
|
|
559
717
|
}
|
|
560
718
|
|
|
719
|
+
#[cfg(test)]
|
|
561
720
|
fn resolve_nodeprobe_binary(
|
|
562
721
|
candidates: Vec<PathBuf>,
|
|
563
722
|
deadline: Instant,
|
|
@@ -606,6 +765,7 @@ fn resolve_nodeprobe_binary(
|
|
|
606
765
|
}
|
|
607
766
|
}
|
|
608
767
|
|
|
768
|
+
#[cfg(test)]
|
|
609
769
|
fn select_nodeprobe_binary(
|
|
610
770
|
candidates: Vec<PathBuf>,
|
|
611
771
|
deadline: Instant,
|
|
@@ -621,6 +781,7 @@ fn select_nodeprobe_binary(
|
|
|
621
781
|
None
|
|
622
782
|
}
|
|
623
783
|
|
|
784
|
+
#[cfg(test)]
|
|
624
785
|
fn nodeprobe_candidates() -> Vec<PathBuf> {
|
|
625
786
|
#[cfg(test)]
|
|
626
787
|
{
|
|
@@ -642,6 +803,7 @@ fn nodeprobe_candidates() -> Vec<PathBuf> {
|
|
|
642
803
|
candidates
|
|
643
804
|
}
|
|
644
805
|
|
|
806
|
+
#[cfg(test)]
|
|
645
807
|
fn validate_nodeprobe_candidate(path: &Path, deadline: Instant) -> Option<PathBuf> {
|
|
646
808
|
if Instant::now() >= deadline {
|
|
647
809
|
return None;
|
|
@@ -669,6 +831,7 @@ fn validate_nodeprobe_candidate(path: &Path, deadline: Instant) -> Option<PathBu
|
|
|
669
831
|
(digest == receipt.binary_sha256).then_some(path.to_path_buf())
|
|
670
832
|
}
|
|
671
833
|
|
|
834
|
+
#[cfg(test)]
|
|
672
835
|
fn valid_nodeprobe_receipt(receipt: &NodeprobeCapabilityReceipt, binary: &Path) -> bool {
|
|
673
836
|
receipt.schema == NODEPROBE_RECEIPT_SCHEMA
|
|
674
837
|
&& binary.file_name().and_then(|name| name.to_str()) == Some(NODEPROBE_NAME)
|
|
@@ -683,10 +846,12 @@ fn valid_nodeprobe_receipt(receipt: &NodeprobeCapabilityReceipt, binary: &Path)
|
|
|
683
846
|
&& same_string_set(&receipt.forbidden, NODEPROBE_FORBIDDEN)
|
|
684
847
|
}
|
|
685
848
|
|
|
849
|
+
#[cfg(test)]
|
|
686
850
|
fn valid_sha256(value: &str) -> bool {
|
|
687
851
|
value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
|
|
688
852
|
}
|
|
689
853
|
|
|
854
|
+
#[cfg(test)]
|
|
690
855
|
fn same_string_set(actual: &[String], expected: &[&str]) -> bool {
|
|
691
856
|
let mut actual = actual.to_vec();
|
|
692
857
|
actual.sort_unstable();
|
|
@@ -698,6 +863,7 @@ fn same_string_set(actual: &[String], expected: &[&str]) -> bool {
|
|
|
698
863
|
actual == expected
|
|
699
864
|
}
|
|
700
865
|
|
|
866
|
+
#[cfg(test)]
|
|
701
867
|
fn nodeprobe_receipt_path(binary: &Path) -> PathBuf {
|
|
702
868
|
let name = binary
|
|
703
869
|
.file_name()
|
|
@@ -706,6 +872,7 @@ fn nodeprobe_receipt_path(binary: &Path) -> PathBuf {
|
|
|
706
872
|
binary.with_file_name(format!("{name}{NODEPROBE_RECEIPT_SUFFIX}"))
|
|
707
873
|
}
|
|
708
874
|
|
|
875
|
+
#[cfg(test)]
|
|
709
876
|
fn read_bounded_file(path: &Path, limit: u64, deadline: Instant) -> Option<Vec<u8>> {
|
|
710
877
|
let metadata = std::fs::metadata(path).ok()?;
|
|
711
878
|
if !metadata.is_file() || metadata.len() > limit {
|
|
@@ -729,6 +896,7 @@ fn read_bounded_file(path: &Path, limit: u64, deadline: Instant) -> Option<Vec<u
|
|
|
729
896
|
}
|
|
730
897
|
}
|
|
731
898
|
|
|
899
|
+
#[cfg(test)]
|
|
732
900
|
fn sha256_file(path: &Path, deadline: Instant) -> Option<String> {
|
|
733
901
|
let metadata = std::fs::metadata(path).ok()?;
|
|
734
902
|
if !metadata.is_file() || metadata.len() > NODEPROBE_BINARY_LIMIT {
|
|
@@ -754,6 +922,7 @@ fn sha256_file(path: &Path, deadline: Instant) -> Option<String> {
|
|
|
754
922
|
}
|
|
755
923
|
}
|
|
756
924
|
|
|
925
|
+
#[cfg(test)]
|
|
757
926
|
fn open_readonly_nonblocking(path: &Path) -> Option<File> {
|
|
758
927
|
let mut options = OpenOptions::new();
|
|
759
928
|
options.read(true);
|
|
@@ -765,6 +934,7 @@ fn open_readonly_nonblocking(path: &Path) -> Option<File> {
|
|
|
765
934
|
options.open(path).ok()
|
|
766
935
|
}
|
|
767
936
|
|
|
937
|
+
#[cfg(test)]
|
|
768
938
|
fn is_executable(metadata: &std::fs::Metadata) -> bool {
|
|
769
939
|
#[cfg(unix)]
|
|
770
940
|
{
|
|
@@ -778,6 +948,7 @@ fn is_executable(metadata: &std::fs::Metadata) -> bool {
|
|
|
778
948
|
}
|
|
779
949
|
}
|
|
780
950
|
|
|
951
|
+
#[cfg(test)]
|
|
781
952
|
fn current_nodeprobe_target() -> Option<&'static str> {
|
|
782
953
|
match (std::env::consts::OS, std::env::consts::ARCH) {
|
|
783
954
|
("macos", "aarch64") => Some("aarch64-apple-darwin"),
|
|
@@ -789,6 +960,7 @@ fn current_nodeprobe_target() -> Option<&'static str> {
|
|
|
789
960
|
}
|
|
790
961
|
}
|
|
791
962
|
|
|
963
|
+
#[cfg(test)]
|
|
792
964
|
fn configure_nodeprobe_command(command: &mut Command) {
|
|
793
965
|
command
|
|
794
966
|
.stdin(Stdio::null())
|
|
@@ -801,6 +973,7 @@ fn configure_nodeprobe_command(command: &mut Command) {
|
|
|
801
973
|
}
|
|
802
974
|
}
|
|
803
975
|
|
|
976
|
+
#[cfg(test)]
|
|
804
977
|
fn parse_probe(bytes: &[u8], requested_endpoint: &str) -> Result<ProbeResult, ()> {
|
|
805
978
|
let value: Value = serde_json::from_slice(bytes).map_err(|_| ())?;
|
|
806
979
|
if value.get("schema_version").and_then(Value::as_u64) != Some(1) {
|
|
@@ -823,6 +996,7 @@ fn parse_probe(bytes: &[u8], requested_endpoint: &str) -> Result<ProbeResult, ()
|
|
|
823
996
|
Ok(ProbeResult { nodes })
|
|
824
997
|
}
|
|
825
998
|
|
|
999
|
+
#[cfg(test)]
|
|
826
1000
|
fn probe_has_error(value: &Value) -> bool {
|
|
827
1001
|
match value.get("error") {
|
|
828
1002
|
None | Some(Value::Null) => false,
|
|
@@ -833,6 +1007,7 @@ fn probe_has_error(value: &Value) -> bool {
|
|
|
833
1007
|
}
|
|
834
1008
|
}
|
|
835
1009
|
|
|
1010
|
+
#[cfg(test)]
|
|
836
1011
|
fn parse_probe_node(value: &Value, report_socket: &str) -> Option<ProbeNode> {
|
|
837
1012
|
Some(ProbeNode {
|
|
838
1013
|
// The accepted producer binds the socket once at the report envelope
|
|
@@ -857,6 +1032,7 @@ fn parse_probe_node(value: &Value, report_socket: &str) -> Option<ProbeNode> {
|
|
|
857
1032
|
})
|
|
858
1033
|
}
|
|
859
1034
|
|
|
1035
|
+
#[cfg(test)]
|
|
860
1036
|
fn non_empty(value: &Value, key: &str) -> Option<String> {
|
|
861
1037
|
value
|
|
862
1038
|
.get(key)
|
|
@@ -895,12 +1071,16 @@ fn project_node(node: &RegisteredNode, probe: Option<&Option<ProbeResult>>) -> B
|
|
|
895
1071
|
}
|
|
896
1072
|
let pi_channel = observed.provider.eq_ignore_ascii_case("pi")
|
|
897
1073
|
&& observed.evidence_method.as_deref() == Some("pi_activity_channel");
|
|
898
|
-
let
|
|
1074
|
+
let native_tmux = observed.evidence_method.as_deref() == Some("native_tmux");
|
|
1075
|
+
let activity = if observed.provider.eq_ignore_ascii_case("pi")
|
|
1076
|
+
&& !pi_channel
|
|
1077
|
+
&& !native_tmux
|
|
1078
|
+
{
|
|
899
1079
|
"unknown"
|
|
900
1080
|
} else {
|
|
901
1081
|
valid_activity(&observed.activity)
|
|
902
1082
|
};
|
|
903
|
-
let health = if observed.provider.eq_ignore_ascii_case("pi") && !pi_channel {
|
|
1083
|
+
let health = if observed.provider.eq_ignore_ascii_case("pi") && !pi_channel && !native_tmux {
|
|
904
1084
|
"unknown"
|
|
905
1085
|
} else {
|
|
906
1086
|
valid_health(&observed.health)
|
|
@@ -32,8 +32,8 @@ use crate::model::enums::{Provider, ProviderEffort};
|
|
|
32
32
|
use crate::model::yaml::Value;
|
|
33
33
|
use crate::model::{paths, spec, yaml, ModelError};
|
|
34
34
|
use crate::provider::wire::{
|
|
35
|
-
builtin_provider_model as wire_builtin_provider_model,
|
|
36
|
-
|
|
35
|
+
builtin_provider_model as wire_builtin_provider_model, parse_canonical_provider,
|
|
36
|
+
provider_model_keys,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
pub const IGNORED_OWNER_TEAM_ID_FIELD: &str = "owner_team_id";
|
|
@@ -602,12 +602,9 @@ fn compile_role_agent_with_mode(
|
|
|
602
602
|
})
|
|
603
603
|
.unwrap_or("");
|
|
604
604
|
let provider_enum = parse_canonical_provider(provider_str).unwrap_or(Provider::Codex);
|
|
605
|
-
if effort.
|
|
606
|
-
&& !is_claude_family(provider_enum)
|
|
607
|
-
&& provider_enum != Provider::Pi
|
|
608
|
-
{
|
|
605
|
+
if let Err(reason) = effort.resolve_for_provider(provider_enum) {
|
|
609
606
|
return Err(ModelError::Validation(format!(
|
|
610
|
-
"{}: effort
|
|
607
|
+
"{}: {reason} (effort: {}; provider: {provider_str})",
|
|
611
608
|
role_path.display(),
|
|
612
609
|
effort.as_str()
|
|
613
610
|
)));
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//! - name: transport_has_session
|
|
10
10
|
//! what: 探测 tmux session 是否存在,探测本身出错一律当不存在
|
|
11
11
|
//! - name: provider_effort_from_raw
|
|
12
|
-
//! what: 解析 effort
|
|
12
|
+
//! what: 解析 effort,保留兼容忽略项并拒绝非法 provider 组合
|
|
13
13
|
//! - name: quick_start_depth_guard
|
|
14
14
|
//! what: 定出本次 quick-start 的父团队与层级,推不出父团队时拒绝
|
|
15
15
|
//! - name: annotate_persisted_team_depth
|
|
@@ -135,24 +135,24 @@ pub(super) fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
|
135
135
|
/// purpose: 从原始字符串解析 effort,并要求当前 provider 支持它
|
|
136
136
|
/// params:
|
|
137
137
|
/// raw: effort 原文,None 或空白返回 None
|
|
138
|
-
/// returns:
|
|
138
|
+
/// returns: 支持的档位或兼容忽略的 None;已知非法 provider/effort 组合返回错误
|
|
139
139
|
/// ---
|
|
140
140
|
/// 0.4.x provider effort MVP step 4: low-level from a raw string. Returns
|
|
141
|
-
/// `Some(effort)`
|
|
141
|
+
/// `Some(effort)` for supported levels, None for legacy ignored values,
|
|
142
|
+
/// and an error for provider/effort combinations rejected by configuration.
|
|
142
143
|
pub(crate) fn provider_effort_from_raw(
|
|
143
144
|
raw: Option<&str>,
|
|
144
145
|
provider: Provider,
|
|
145
|
-
) -> Option<ProviderEffort> {
|
|
146
|
-
let
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
146
|
+
) -> Result<Option<ProviderEffort>, LifecycleError> {
|
|
147
|
+
let Some(effort) = raw.and_then(ProviderEffort::parse) else {
|
|
148
|
+
return Ok(None);
|
|
149
|
+
};
|
|
150
|
+
effort.resolve_for_provider(provider).map_err(|reason| {
|
|
151
|
+
LifecycleError::RequirementUnmet(format!(
|
|
152
|
+
"{reason} (effort: {}; provider: {provider:?})",
|
|
153
|
+
effort.as_str()
|
|
154
|
+
))
|
|
155
|
+
})
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/// ---
|
|
@@ -171,7 +171,7 @@ pub(crate) fn provider_effort_event_payload(
|
|
|
171
171
|
return None;
|
|
172
172
|
}
|
|
173
173
|
let effort = ProviderEffort::parse(raw)?;
|
|
174
|
-
if effort.
|
|
174
|
+
if !matches!(effort.resolve_for_provider(provider), Ok(None)) {
|
|
175
175
|
return None;
|
|
176
176
|
}
|
|
177
177
|
Some(serde_json::json!({
|
|
@@ -221,7 +221,7 @@ pub(crate) fn extend_worker_env_unset_for_effort(
|
|
|
221
221
|
pub(crate) fn provider_effort_for_spawn(
|
|
222
222
|
agent: &crate::model::yaml::Value,
|
|
223
223
|
provider: Provider,
|
|
224
|
-
) -> Option<ProviderEffort> {
|
|
224
|
+
) -> Result<Option<ProviderEffort>, LifecycleError> {
|
|
225
225
|
provider_effort_from_raw(agent.get("effort").and_then(|v| v.as_str()), provider)
|
|
226
226
|
}
|
|
227
227
|
|
|
@@ -252,7 +252,7 @@ pub(crate) fn provider_effort_event_if_dropped(
|
|
|
252
252
|
pub(crate) fn provider_effort_for_spawn_json(
|
|
253
253
|
agent: &serde_json::Value,
|
|
254
254
|
provider: Provider,
|
|
255
|
-
) -> Option<ProviderEffort> {
|
|
255
|
+
) -> Result<Option<ProviderEffort>, LifecycleError> {
|
|
256
256
|
provider_effort_from_raw(
|
|
257
257
|
agent.get("effort").and_then(serde_json::Value::as_str),
|
|
258
258
|
provider,
|
|
@@ -307,10 +307,16 @@ pub(super) fn quick_start_depth_guard(
|
|
|
307
307
|
requested_team: Option<&str>,
|
|
308
308
|
_strict_real_runtime: bool,
|
|
309
309
|
) -> Result<QuickStartDepth, LifecycleError> {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
310
|
+
// OWNER_TEAM_ID is injected into every worker process. It is parent
|
|
311
|
+
// context only when the target quick-start workspace is the same workspace
|
|
312
|
+
// that produced the environment; otherwise a worker launching a fresh
|
|
313
|
+
// external workspace would inherit a false nesting parent.
|
|
314
|
+
let env_parent = owner_context_matches_workspace(workspace).then(|| {
|
|
315
|
+
std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
|
|
316
|
+
.ok()
|
|
317
|
+
.map(|value| value.trim().to_string())
|
|
318
|
+
.filter(|value| !value.is_empty())
|
|
319
|
+
}).flatten();
|
|
314
320
|
let parent = env_parent;
|
|
315
321
|
let Some(parent) = parent else {
|
|
316
322
|
let state = crate::state::persist::load_runtime_state(workspace)
|
|
@@ -365,6 +371,18 @@ pub(super) fn quick_start_depth_guard(
|
|
|
365
371
|
/// purpose: 从 runtime state 推断父团队键
|
|
366
372
|
/// returns: 活跃团队键,且该团队确有 running 席位时返回它,否则 None
|
|
367
373
|
/// ---
|
|
374
|
+
fn owner_context_matches_workspace(workspace: &Path) -> bool {
|
|
375
|
+
let Some(env_workspace) = std::env::var_os("TEAM_AGENT_WORKSPACE") else {
|
|
376
|
+
return false;
|
|
377
|
+
};
|
|
378
|
+
let env_workspace = PathBuf::from(env_workspace);
|
|
379
|
+
env_workspace == workspace
|
|
380
|
+
|| std::fs::canonicalize(&env_workspace)
|
|
381
|
+
.ok()
|
|
382
|
+
.zip(std::fs::canonicalize(workspace).ok())
|
|
383
|
+
.is_some_and(|(env_path, target_path)| env_path == target_path)
|
|
384
|
+
}
|
|
385
|
+
|
|
368
386
|
pub(super) fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
|
|
369
387
|
let active = explicit_active_team_key(state)?;
|
|
370
388
|
let team = state
|
|
@@ -152,7 +152,7 @@ pub(super) fn spawn_agents(
|
|
|
152
152
|
// 0.4.x provider effort MVP step 4 + 7: resolve effort and emit
|
|
153
153
|
// unsupported warning event when the spec asked for an effort the
|
|
154
154
|
// provider can't satisfy.
|
|
155
|
-
let agent_effort = provider_effort_for_spawn(agent, provider)
|
|
155
|
+
let agent_effort = provider_effort_for_spawn(agent, provider)?;
|
|
156
156
|
if let Some(event_value) = provider_effort_event_if_dropped(agent, provider, agent_id_raw) {
|
|
157
157
|
let _ = crate::event_log::EventLog::new(workspace)
|
|
158
158
|
.write("provider.effort_unsupported", event_value);
|
|
@@ -355,7 +355,7 @@ pub(super) fn spawn_agent_window(
|
|
|
355
355
|
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
356
356
|
// 0.4.x provider effort MVP: restart/resume preserves effort from the
|
|
357
357
|
// persisted agent JSON (state's agent["effort"] field, set by launch).
|
|
358
|
-
let restart_effort = crate::lifecycle::launch::provider_effort_for_spawn_json(agent, provider)
|
|
358
|
+
let restart_effort = crate::lifecycle::launch::provider_effort_for_spawn_json(agent, provider)?;
|
|
359
359
|
if let Some(event_value) = crate::lifecycle::launch::provider_effort_event_if_dropped_json(
|
|
360
360
|
agent,
|
|
361
361
|
provider,
|
|
@@ -99,17 +99,17 @@ fn stale_snapshot_cannot_flip_status_or_diagnose_ok_readiness() {
|
|
|
99
99
|
let case = B0HideOneCase::new("status-diagnose-hide-one");
|
|
100
100
|
let root = case.current_state();
|
|
101
101
|
save_runtime_state(&case.workspace, &root).expect("seed current root state");
|
|
102
|
-
let
|
|
102
|
+
let tmux = brief_probe::install_native_tmux(
|
|
103
103
|
&case.workspace.join("probe"),
|
|
104
104
|
NEW_ENDPOINT,
|
|
105
105
|
SESSION,
|
|
106
106
|
WORKER,
|
|
107
107
|
"%new",
|
|
108
|
-
"
|
|
108
|
+
"bash",
|
|
109
109
|
);
|
|
110
110
|
let _path = case.env.with_env(
|
|
111
111
|
"PATH",
|
|
112
|
-
&brief_probe::path_with_probe(&
|
|
112
|
+
&brief_probe::path_with_probe(&tmux, std::env::var_os("PATH").as_deref()),
|
|
113
113
|
);
|
|
114
114
|
|
|
115
115
|
let status_before = case.run_json(&[
|
|
@@ -86,9 +86,8 @@ impl ProviderEffort {
|
|
|
86
86
|
matches!(self, Self::Max)
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
///
|
|
90
|
-
///
|
|
91
|
-
/// Copilot/Gemini/CursorAgent/Fake ignore the field (warning at runtime).
|
|
89
|
+
/// Native command support. Claude/Pi accept max; Codex/Grok accept
|
|
90
|
+
/// the other levels. Use resolve_for_provider for admission/ignore policy.
|
|
92
91
|
pub fn is_supported_by(self, provider: Provider) -> bool {
|
|
93
92
|
match provider {
|
|
94
93
|
Provider::Claude | Provider::ClaudeCode | Provider::Pi => true,
|
|
@@ -98,6 +97,21 @@ impl ProviderEffort {
|
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
}
|
|
100
|
+
|
|
101
|
+
/// One admission policy for compiler, direct specs and runtime commands.
|
|
102
|
+
/// Ok(None) preserves the legacy warning-and-ignore providers.
|
|
103
|
+
pub fn resolve_for_provider(self, provider: Provider) -> Result<Option<Self>, &'static str> {
|
|
104
|
+
if provider == Provider::CursorAgent {
|
|
105
|
+
return Err("cursor_agent does not support effort; the Cursor CLI has no --effort flag");
|
|
106
|
+
}
|
|
107
|
+
if self.is_supported_by(provider) {
|
|
108
|
+
Ok(Some(self))
|
|
109
|
+
} else if self.is_claude_only() {
|
|
110
|
+
Err("effort 'max' is only supported by claude/claude_code/pi")
|
|
111
|
+
} else {
|
|
112
|
+
Ok(None)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
/// auth 模式(`AUTH_MODES` `profiles/constants.py:6`)。
|
|
@@ -18,7 +18,7 @@ use crate::model::ids::TaskId;
|
|
|
18
18
|
use crate::model::task_graph::{find_dependency_cycle, TaskNode};
|
|
19
19
|
use crate::model::yaml::Value as Yaml;
|
|
20
20
|
use crate::model::yaml;
|
|
21
|
-
use crate::provider::wire::
|
|
21
|
+
use crate::provider::wire::parse_canonical_provider;
|
|
22
22
|
|
|
23
23
|
/// result_envelope_v1 顶层 required(= allowed)。
|
|
24
24
|
const RESULT_REQUIRED: &[&str] = &[
|
|
@@ -463,15 +463,16 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
|
|
|
463
463
|
"{path}/effort: unknown effort '{raw}' (allowed: low|medium|high|xhigh|max)"
|
|
464
464
|
));
|
|
465
465
|
}
|
|
466
|
-
Some(effort)
|
|
466
|
+
Some(effort) => {
|
|
467
467
|
let provider = agent.get("provider").and_then(Yaml::as_str).unwrap_or("");
|
|
468
|
-
if
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
468
|
+
if let Some(provider_enum) = parse_canonical_provider(provider) {
|
|
469
|
+
if let Err(reason) = effort.resolve_for_provider(provider_enum) {
|
|
470
|
+
errors.push(format!(
|
|
471
|
+
"{path}/effort: {reason} (effort: {raw}; provider: {provider})"
|
|
472
|
+
));
|
|
473
|
+
}
|
|
472
474
|
}
|
|
473
475
|
}
|
|
474
|
-
Some(_) => {}
|
|
475
476
|
}
|
|
476
477
|
}
|
|
477
478
|
check_keys_y(
|
|
@@ -231,7 +231,7 @@ mod tests {
|
|
|
231
231
|
);
|
|
232
232
|
assert!(
|
|
233
233
|
model_spec_rs.contains("parse_canonical_provider")
|
|
234
|
-
&& model_spec_rs.contains("
|
|
234
|
+
&& model_spec_rs.contains("resolve_for_provider"),
|
|
235
235
|
"model spec must validate providers through typed helpers"
|
|
236
236
|
);
|
|
237
237
|
assert!(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.89",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.89",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.89",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.89"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|