@team-agent/installer 0.3.9 → 0.3.11
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/send.rs +9 -2
- package/crates/team-agent/src/coordinator/backoff.rs +83 -2
- package/crates/team-agent/src/coordinator/tests/spine.rs +6 -0
- package/crates/team-agent/src/coordinator/tick.rs +410 -168
- package/crates/team-agent/src/leader/lease.rs +19 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +12 -0
- package/crates/team-agent/src/leader/rediscover.rs +2 -0
- package/crates/team-agent/src/lifecycle/launch.rs +35 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +17 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +75 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +201 -3
- package/crates/team-agent/src/lifecycle/restart/selection.rs +51 -14
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/core.rs +89 -15
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +68 -3
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +3 -1
- package/crates/team-agent/src/mcp_server/helpers.rs +24 -5
- package/crates/team-agent/src/mcp_server/normalize.rs +13 -6
- package/crates/team-agent/src/mcp_server/tests/send.rs +310 -212
- package/crates/team-agent/src/messaging/delivery.rs +83 -2
- package/crates/team-agent/src/messaging/helpers.rs +30 -10
- package/crates/team-agent/src/messaging/send.rs +71 -14
- package/crates/team-agent/src/messaging/tests/basic.rs +25 -7
- package/crates/team-agent/src/messaging/tests/runtime.rs +565 -111
- package/crates/team-agent/src/messaging/types.rs +19 -4
- package/crates/team-agent/src/provider/approvals/parsing.rs +43 -14
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +12 -9
- package/crates/team-agent/src/transport/test_support.rs +12 -1
- package/package.json +4 -4
|
@@ -342,13 +342,12 @@ fn start_mode_serde_names_match_python_start_mode_strings() {
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
// ───────────────────────────────────────────────────────────────────────
|
|
345
|
-
// decide_start_mode — bug-085 四象限
|
|
346
|
-
// golden 实跑(PYTHONPATH=… python3 /tmp/x.py,_resume_rollout_missing + start_mode 逻辑):
|
|
345
|
+
// decide_start_mode — bug-085 四象限 + E20 #264 gap closure.
|
|
347
346
|
// codex sess rollout-present any-fresh -> resumed
|
|
348
|
-
// codex sess
|
|
349
|
-
// codex sess
|
|
347
|
+
// codex sess backing-MISSING !allow_fresh -> noop/refuse (绝不静默 resume 死 session)
|
|
348
|
+
// codex sess backing-MISSING allow_fresh -> fresh_after_missing_rollout
|
|
350
349
|
// codex no-sess any -> fresh
|
|
351
|
-
// claude
|
|
350
|
+
// claude/copilot sess backing-missing -> fresh_after_missing_rollout 或 noop/refuse
|
|
352
351
|
// claude no-sess -> fresh
|
|
353
352
|
// 这是 bug-085 把 start_mode 分类从 start_agent 的 lock+spawn 全路径剥离出来的命门。
|
|
354
353
|
// ───────────────────────────────────────────────────────────────────────
|
|
@@ -375,11 +374,11 @@ fn decide_start_mode_codex_missing_rollout_with_allow_fresh_is_fresh_after_missi
|
|
|
375
374
|
}
|
|
376
375
|
|
|
377
376
|
#[test]
|
|
378
|
-
fn
|
|
379
|
-
//
|
|
377
|
+
fn decide_start_mode_codex_missing_rollout_without_allow_fresh_refuses() {
|
|
378
|
+
// E20 C①:backing 缺且 !allow_fresh → 诚实拒绝,绝不 resume 进死 session。
|
|
380
379
|
assert_eq!(
|
|
381
380
|
decide_start_mode("codex", Some(&sid("s1")), None, false, false),
|
|
382
|
-
StartMode::
|
|
381
|
+
StartMode::Noop
|
|
383
382
|
);
|
|
384
383
|
}
|
|
385
384
|
|
|
@@ -408,12 +407,24 @@ fn decide_start_mode_no_session_is_fresh() {
|
|
|
408
407
|
}
|
|
409
408
|
|
|
410
409
|
#[test]
|
|
411
|
-
fn
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
410
|
+
fn decide_start_mode_checks_backing_for_all_resumable_providers() {
|
|
411
|
+
for provider in ["claude", "claude_code", "copilot"] {
|
|
412
|
+
assert_eq!(
|
|
413
|
+
decide_start_mode(provider, Some(&sid("s1")), None, false, true),
|
|
414
|
+
StartMode::FreshAfterMissingRollout,
|
|
415
|
+
"{provider} missing backing + allow_fresh must not resume"
|
|
416
|
+
);
|
|
417
|
+
assert_eq!(
|
|
418
|
+
decide_start_mode(provider, Some(&sid("s1")), None, false, false),
|
|
419
|
+
StartMode::Noop,
|
|
420
|
+
"{provider} missing backing + !allow_fresh must refuse"
|
|
421
|
+
);
|
|
422
|
+
assert_eq!(
|
|
423
|
+
decide_start_mode(provider, Some(&sid("s1")), Some(&rp("/r")), true, false),
|
|
424
|
+
StartMode::Resumed,
|
|
425
|
+
"{provider} existing backing remains resumable"
|
|
426
|
+
);
|
|
427
|
+
}
|
|
417
428
|
assert_eq!(
|
|
418
429
|
decide_start_mode("claude", None, None, false, true),
|
|
419
430
|
StartMode::Fresh
|
|
@@ -533,8 +544,11 @@ fn classify_restart_plan_never_interacted_null_session_with_allow_fresh_marks_fo
|
|
|
533
544
|
fn classify_restart_plan_codex_with_session_still_resumes() {
|
|
534
545
|
// E6 层2 回归锁(不误伤): codex worker first_send_at=null 但 session_id 已捕 →
|
|
535
546
|
// 仍走 Resume(分流轴是 session_id 有无,不是 interacted)。防层2 修法把 has_session 也误判。
|
|
547
|
+
let ws = temp_ws();
|
|
548
|
+
let rollout = ws.join("codex-rollout.jsonl");
|
|
549
|
+
std::fs::write(&rollout, "{}\n").unwrap();
|
|
536
550
|
let state = json!({
|
|
537
|
-
"agents": { "w1": { "provider": "codex", "session_id": "sess-codex-abc" } }
|
|
551
|
+
"agents": { "w1": { "provider": "codex", "session_id": "sess-codex-abc", "rollout_path": rollout.to_string_lossy() } }
|
|
538
552
|
});
|
|
539
553
|
let plan = classify_restart_plan(&state, false).expect("纯验证不应 Err");
|
|
540
554
|
assert_eq!(plan.decisions.len(), 1);
|
|
@@ -978,6 +992,66 @@ fn leader_pane_env_cross_socket_all_probe_errors_stays_unknown() {
|
|
|
978
992
|
assert_eq!(state, LeaderPaneEnvState::Unknown);
|
|
979
993
|
}
|
|
980
994
|
|
|
995
|
+
#[test]
|
|
996
|
+
fn mcp_auto_approval_env_marks_leader_bypass_namespace_only() {
|
|
997
|
+
let mut env = std::collections::BTreeMap::new();
|
|
998
|
+
let safety = DangerousApproval {
|
|
999
|
+
enabled: true,
|
|
1000
|
+
source: DangerousApprovalSource::LeaderProcess,
|
|
1001
|
+
inherited: true,
|
|
1002
|
+
provider: Some("codex".to_string()),
|
|
1003
|
+
flag: Some("--dangerously-bypass-approvals-and-sandbox".to_string()),
|
|
1004
|
+
worker_capability_above_leader: false,
|
|
1005
|
+
ancestry_binary_name: Some("codex".to_string()),
|
|
1006
|
+
unexpected_binary: false,
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
1010
|
+
|
|
1011
|
+
assert_eq!(env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str), Some("1"));
|
|
1012
|
+
assert_eq!(
|
|
1013
|
+
env.get("TEAM_AGENT_MCP_AUTO_APPROVE").map(String::as_str),
|
|
1014
|
+
Some("team_orchestrator")
|
|
1015
|
+
);
|
|
1016
|
+
assert_eq!(
|
|
1017
|
+
env.get("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE").map(String::as_str),
|
|
1018
|
+
Some("leader_bypass")
|
|
1019
|
+
);
|
|
1020
|
+
assert_eq!(
|
|
1021
|
+
env.get("TEAM_AGENT_LEADER_BYPASS_FLAG").map(String::as_str),
|
|
1022
|
+
Some("--dangerously-bypass-approvals-and-sandbox")
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
#[test]
|
|
1027
|
+
fn mcp_auto_approval_env_clears_when_leader_is_restricted() {
|
|
1028
|
+
let mut env = std::collections::BTreeMap::from([
|
|
1029
|
+
(
|
|
1030
|
+
"TEAM_AGENT_MCP_AUTO_APPROVE".to_string(),
|
|
1031
|
+
"team_orchestrator".to_string(),
|
|
1032
|
+
),
|
|
1033
|
+
("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(), "leader_bypass".to_string()),
|
|
1034
|
+
]);
|
|
1035
|
+
let safety = DangerousApproval {
|
|
1036
|
+
enabled: false,
|
|
1037
|
+
source: DangerousApprovalSource::Disabled,
|
|
1038
|
+
inherited: false,
|
|
1039
|
+
provider: None,
|
|
1040
|
+
flag: None,
|
|
1041
|
+
worker_capability_above_leader: false,
|
|
1042
|
+
ancestry_binary_name: None,
|
|
1043
|
+
unexpected_binary: false,
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
apply_mcp_auto_approval_env(&mut env, &safety);
|
|
1047
|
+
|
|
1048
|
+
assert_eq!(env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str), Some("0"));
|
|
1049
|
+
assert!(
|
|
1050
|
+
!env.contains_key("TEAM_AGENT_MCP_AUTO_APPROVE"),
|
|
1051
|
+
"restricted leader must not leave MCP auto-approval env behind: {env:?}"
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
981
1055
|
struct EnvVarGuard {
|
|
982
1056
|
key: &'static str,
|
|
983
1057
|
previous: Option<String>,
|
|
@@ -945,6 +945,10 @@ const DELEG_ROLE_WORKER2: &str = "---\nname: worker2\nrole: Second Worker\nprovi
|
|
|
945
945
|
pub(super) fn restart_ws_two_resumable_workers() -> PathBuf {
|
|
946
946
|
let ws = temp_ws().join("restartteam");
|
|
947
947
|
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
948
|
+
let alpha_rollout = ws.join("alpha-rollout.jsonl");
|
|
949
|
+
let bravo_rollout = ws.join("bravo-rollout.jsonl");
|
|
950
|
+
std::fs::write(&alpha_rollout, "{}\n").unwrap();
|
|
951
|
+
std::fs::write(&bravo_rollout, "{}\n").unwrap();
|
|
948
952
|
std::fs::write(ws.join("TEAM.md"), "---\nname: restartteam\nobjective: Restart probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
|
|
949
953
|
std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
950
954
|
std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
|
|
@@ -955,8 +959,8 @@ pub(super) fn restart_ws_two_resumable_workers() -> PathBuf {
|
|
|
955
959
|
&json!({
|
|
956
960
|
"session_name": "team-restartteam",
|
|
957
961
|
"agents": {
|
|
958
|
-
"alpha": {"status": "running", "provider": "codex", "session_id": "sess-a", "first_send_at": "2026-05-27T10:00:00+00:00"},
|
|
959
|
-
"bravo": {"status": "running", "provider": "codex", "session_id": "sess-b", "first_send_at": "2026-05-27T10:00:00+00:00"}
|
|
962
|
+
"alpha": {"status": "running", "provider": "codex", "session_id": "sess-a", "rollout_path": alpha_rollout.to_string_lossy(), "first_send_at": "2026-05-27T10:00:00+00:00"},
|
|
963
|
+
"bravo": {"status": "running", "provider": "codex", "session_id": "sess-b", "rollout_path": bravo_rollout.to_string_lossy(), "first_send_at": "2026-05-27T10:00:00+00:00"}
|
|
960
964
|
}
|
|
961
965
|
}),
|
|
962
966
|
)
|
|
@@ -965,6 +969,33 @@ pub(super) fn restart_ws_two_resumable_workers() -> PathBuf {
|
|
|
965
969
|
ws
|
|
966
970
|
}
|
|
967
971
|
|
|
972
|
+
fn restart_ws_one_resumable_worker() -> PathBuf {
|
|
973
|
+
let ws = temp_ws().join("restartone");
|
|
974
|
+
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
975
|
+
let rollout = ws.join("alpha-rollout.jsonl");
|
|
976
|
+
std::fs::write(&rollout, "{}\n").unwrap();
|
|
977
|
+
std::fs::write(
|
|
978
|
+
ws.join("TEAM.md"),
|
|
979
|
+
"---\nname: restartone\nobjective: Restart readiness probe.\nprovider: codex\n---\n\nteam.\n",
|
|
980
|
+
)
|
|
981
|
+
.unwrap();
|
|
982
|
+
std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
983
|
+
let spec = crate::compiler::compile_team(&ws).expect("compile 1-agent team");
|
|
984
|
+
std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
|
|
985
|
+
crate::state::persist::save_runtime_state(
|
|
986
|
+
&ws,
|
|
987
|
+
&json!({
|
|
988
|
+
"session_name": "team-restartone",
|
|
989
|
+
"agents": {
|
|
990
|
+
"alpha": {"status": "running", "provider": "codex", "session_id": "sess-a", "rollout_path": rollout.to_string_lossy(), "first_send_at": "2026-05-27T10:00:00+00:00"}
|
|
991
|
+
}
|
|
992
|
+
}),
|
|
993
|
+
)
|
|
994
|
+
.unwrap();
|
|
995
|
+
seed_healthy_coordinator(&ws);
|
|
996
|
+
ws
|
|
997
|
+
}
|
|
998
|
+
|
|
968
999
|
// 2 [P0] — restart_with_transport must drive the REAL Route-B resume spawn: one spawn per resumable
|
|
969
1000
|
// worker. The first resumed worker recreates the session with spawn_first; later workers may use
|
|
970
1001
|
// spawn_into only after a live-session check proves that recreated session still exists. Each spawn
|
|
@@ -1020,6 +1051,38 @@ fn restart_with_transport_spawns_resumable_workers_not_stub() {
|
|
|
1020
1051
|
);
|
|
1021
1052
|
}
|
|
1022
1053
|
|
|
1054
|
+
#[test]
|
|
1055
|
+
fn restart_times_out_when_spawned_worker_pane_is_not_addressable() {
|
|
1056
|
+
let ws = restart_ws_one_resumable_worker();
|
|
1057
|
+
let transport = OfflineTransport::new().with_spawned_panes_addressable(false);
|
|
1058
|
+
|
|
1059
|
+
let result =
|
|
1060
|
+
restart_with_transport_with_readiness_deadline(&ws, false, None, &transport, Some(0));
|
|
1061
|
+
|
|
1062
|
+
let text = format!("{result:?}");
|
|
1063
|
+
assert!(
|
|
1064
|
+
text.contains("restart not ready")
|
|
1065
|
+
&& text.contains("worker pane addressable: no")
|
|
1066
|
+
&& text.contains("Action:")
|
|
1067
|
+
&& text.contains("Log:"),
|
|
1068
|
+
"restart must refuse with N38 readiness timeout details, not return ok; got {text}"
|
|
1069
|
+
);
|
|
1070
|
+
assert!(
|
|
1071
|
+
!matches!(result, Ok(RestartReport::Restarted { .. })),
|
|
1072
|
+
"restart readiness timeout must not return Restarted ok"
|
|
1073
|
+
);
|
|
1074
|
+
let events = crate::event_log::EventLog::new(&ws).tail(20).unwrap();
|
|
1075
|
+
let timeout = events
|
|
1076
|
+
.iter()
|
|
1077
|
+
.find(|event| event.get("event").and_then(|v| v.as_str()) == Some("restart.readiness_timeout"))
|
|
1078
|
+
.expect("restart.readiness_timeout event");
|
|
1079
|
+
assert_eq!(
|
|
1080
|
+
timeout.get("worker_pane_addressable").and_then(|v| v.as_bool()),
|
|
1081
|
+
Some(false),
|
|
1082
|
+
"timeout event must carry the failed readiness condition: {timeout}"
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1023
1086
|
// 3 [P0] — start_agent_with_transport on a non-paused agent with a session_id must spawn EXACTLY ONE
|
|
1024
1087
|
// worker (resume) carrying the provider build_command. Today the stub returns RequirementUnmet with
|
|
1025
1088
|
// ZERO spawns -> RED at recorded.len().
|
|
@@ -1027,11 +1090,13 @@ fn restart_with_transport_spawns_resumable_workers_not_stub() {
|
|
|
1027
1090
|
fn start_agent_with_transport_spawns_resume_not_stub() {
|
|
1028
1091
|
let ws = temp_ws().join("startagentws");
|
|
1029
1092
|
std::fs::create_dir_all(&ws).unwrap();
|
|
1093
|
+
let rollout = ws.join("alpha-rollout.jsonl");
|
|
1094
|
+
std::fs::write(&rollout, "{}\n").unwrap();
|
|
1030
1095
|
crate::state::persist::save_runtime_state(
|
|
1031
1096
|
&ws,
|
|
1032
1097
|
&json!({
|
|
1033
1098
|
"session_name": "team-sa",
|
|
1034
|
-
"agents": {"alpha": {"status": "running", "provider": "codex", "session_id": "sess-a", "first_send_at": "2026-05-27T10:00:00+00:00"}}
|
|
1099
|
+
"agents": {"alpha": {"status": "running", "provider": "codex", "session_id": "sess-a", "rollout_path": rollout.to_string_lossy(), "first_send_at": "2026-05-27T10:00:00+00:00"}}
|
|
1035
1100
|
}),
|
|
1036
1101
|
)
|
|
1037
1102
|
.unwrap();
|
|
@@ -88,11 +88,13 @@ impl crate::transport::Transport for SessionProbeRecordingTransport {
|
|
|
88
88
|
fn respawn_ws_one_resumable_worker() -> PathBuf {
|
|
89
89
|
let ws = temp_ws().join("respawn_dead_session");
|
|
90
90
|
std::fs::create_dir_all(&ws).unwrap();
|
|
91
|
+
let rollout = ws.join("alpha-rollout.jsonl");
|
|
92
|
+
std::fs::write(&rollout, "{}\n").unwrap();
|
|
91
93
|
crate::state::persist::save_runtime_state(
|
|
92
94
|
&ws,
|
|
93
95
|
&json!({
|
|
94
96
|
"session_name": "team-sa",
|
|
95
|
-
"agents": {"alpha": {"status": "running", "provider": "codex", "session_id": "sess-a", "first_send_at": "2026-05-27T10:00:00+00:00"}}
|
|
97
|
+
"agents": {"alpha": {"status": "running", "provider": "codex", "session_id": "sess-a", "rollout_path": rollout.to_string_lossy(), "first_send_at": "2026-05-27T10:00:00+00:00"}}
|
|
96
98
|
}),
|
|
97
99
|
)
|
|
98
100
|
.unwrap();
|
|
@@ -146,7 +146,10 @@ impl serde_json::ser::Formatter for PythonJsonFormatter {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
fn begin_object_value<W: ?Sized + std::io::Write>(
|
|
149
|
+
fn begin_object_value<W: ?Sized + std::io::Write>(
|
|
150
|
+
&mut self,
|
|
151
|
+
writer: &mut W,
|
|
152
|
+
) -> std::io::Result<()> {
|
|
150
153
|
writer.write_all(b": ")
|
|
151
154
|
}
|
|
152
155
|
}
|
|
@@ -171,7 +174,11 @@ pub(crate) fn ensure_object(value: &mut Value) {
|
|
|
171
174
|
}
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
pub(crate) fn insert_array(
|
|
177
|
+
pub(crate) fn insert_array(
|
|
178
|
+
obj: &mut serde_json::Map<String, Value>,
|
|
179
|
+
key: &str,
|
|
180
|
+
value: Option<&[Value]>,
|
|
181
|
+
) {
|
|
175
182
|
if let Some(items) = value {
|
|
176
183
|
obj.insert(key.to_string(), Value::Array(items.to_vec()));
|
|
177
184
|
}
|
|
@@ -198,11 +205,20 @@ pub(crate) fn object_fields(value: Value) -> serde_json::Map<String, Value> {
|
|
|
198
205
|
}
|
|
199
206
|
|
|
200
207
|
pub(crate) fn delivery_outcome_value(out: &DeliveryOutcome) -> Value {
|
|
201
|
-
serde_json::json!({
|
|
208
|
+
let mut value = serde_json::json!({
|
|
202
209
|
"ok": out.ok,
|
|
203
210
|
"status": enum_value(out.status),
|
|
204
211
|
"message_id": out.message_id,
|
|
205
|
-
})
|
|
212
|
+
});
|
|
213
|
+
if let Some(obj) = value.as_object_mut() {
|
|
214
|
+
if let Some(reason) = out.reason {
|
|
215
|
+
obj.insert("reason".to_string(), enum_value(reason));
|
|
216
|
+
}
|
|
217
|
+
if let Some(warning) = out.verification.as_deref() {
|
|
218
|
+
obj.insert("warning".to_string(), Value::String(warning.to_string()));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
value
|
|
206
222
|
}
|
|
207
223
|
|
|
208
224
|
pub(crate) fn latest_task_for_assignee(workspace: &Path, agent_id: &str) -> Option<String> {
|
|
@@ -218,7 +234,10 @@ pub(crate) fn latest_task_for_assignee(workspace: &Path, agent_id: &str) -> Opti
|
|
|
218
234
|
.and_then(Value::as_str)
|
|
219
235
|
.unwrap_or("")
|
|
220
236
|
.to_ascii_lowercase();
|
|
221
|
-
if matches!(
|
|
237
|
+
if matches!(
|
|
238
|
+
status.as_str(),
|
|
239
|
+
"done" | "success" | "failed" | "blocked" | "cancelled"
|
|
240
|
+
) {
|
|
222
241
|
continue;
|
|
223
242
|
}
|
|
224
243
|
if let Some(id) = task.get("id").and_then(text_of_value) {
|
|
@@ -65,7 +65,10 @@ pub fn normalize_change_kind(value: Option<&str>, description: &str) -> ChangeKi
|
|
|
65
65
|
ChangeKind::Created
|
|
66
66
|
} else if desc.contains("removed") || desc.contains("deleted") {
|
|
67
67
|
ChangeKind::Deleted
|
|
68
|
-
} else if desc.contains("verified")
|
|
68
|
+
} else if desc.contains("verified")
|
|
69
|
+
|| desc.contains("observed")
|
|
70
|
+
|| desc.contains("inspected")
|
|
71
|
+
{
|
|
69
72
|
ChangeKind::Observed
|
|
70
73
|
} else {
|
|
71
74
|
ChangeKind::Modified
|
|
@@ -127,6 +130,7 @@ pub fn compact_tool_result(result: &Value) -> ToolResult {
|
|
|
127
130
|
"ok",
|
|
128
131
|
"status",
|
|
129
132
|
"reason",
|
|
133
|
+
"warning",
|
|
130
134
|
"error",
|
|
131
135
|
"message_id",
|
|
132
136
|
"agent_id",
|
|
@@ -195,7 +199,10 @@ pub fn compact_tool_result(result: &Value) -> ToolResult {
|
|
|
195
199
|
Ok(ToolOk { fields })
|
|
196
200
|
}
|
|
197
201
|
|
|
198
|
-
pub(crate) fn normalize_changes(
|
|
202
|
+
pub(crate) fn normalize_changes(
|
|
203
|
+
value: Option<&Value>,
|
|
204
|
+
envelope_summary: &str,
|
|
205
|
+
) -> Vec<NormalizedChange> {
|
|
199
206
|
items_from_value(value)
|
|
200
207
|
.iter()
|
|
201
208
|
.filter_map(|item| {
|
|
@@ -267,7 +274,9 @@ pub(crate) fn normalize_risks(value: Option<&Value>) -> Vec<NormalizedRisk> {
|
|
|
267
274
|
.filter_map(|item| match item {
|
|
268
275
|
Value::Object(obj) => Some(NormalizedRisk {
|
|
269
276
|
severity: normalize_risk_severity(
|
|
270
|
-
obj.get("severity")
|
|
277
|
+
obj.get("severity")
|
|
278
|
+
.or_else(|| obj.get("level"))
|
|
279
|
+
.and_then(Value::as_str),
|
|
271
280
|
),
|
|
272
281
|
description: obj
|
|
273
282
|
.get("description")
|
|
@@ -325,9 +334,7 @@ pub(crate) fn normalize_next_actions(value: Option<&Value>) -> Vec<NormalizedNex
|
|
|
325
334
|
.or_else(|| obj.get("todo"))
|
|
326
335
|
.or_else(|| obj.get("message"))
|
|
327
336
|
.and_then(text_of_value)
|
|
328
|
-
.map(|description| NormalizedNextAction {
|
|
329
|
-
description,
|
|
330
|
-
}),
|
|
337
|
+
.map(|description| NormalizedNextAction { description }),
|
|
331
338
|
scalar => text_of_value(scalar).map(|description| NormalizedNextAction { description }),
|
|
332
339
|
})
|
|
333
340
|
.collect()
|