@team-agent/installer 0.5.49 → 0.5.51
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 +6 -4
- package/crates/team-agent/src/cli/emit.rs +91 -39
- package/crates/team-agent/src/cli/mod.rs +33 -15
- package/crates/team-agent/src/cli/named_address.rs +82 -53
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +103 -1308
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +2 -2
- package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +26 -15
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +11 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +133 -58
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -18,12 +18,11 @@ fn pane_info(pane_id: &str, session: &str, window: &str) -> PaneInfo {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// ════════════════════════════════════════════════════════════════════════
|
|
21
|
-
// GROUP E —
|
|
22
|
-
// status=FallbackLog (NOT a real submit). leader.py:394-436.
|
|
21
|
+
// GROUP E — legacy diagnostics may only advance an existing durable row.
|
|
23
22
|
// ════════════════════════════════════════════════════════════════════════
|
|
24
23
|
|
|
25
24
|
#[test]
|
|
26
|
-
fn
|
|
25
|
+
fn fail_leader_delivery_without_message_id_does_not_invent_a_row() {
|
|
27
26
|
let ws = tmp_ws("faillead");
|
|
28
27
|
let payload = json(serde_json::json!({
|
|
29
28
|
"to": "leader", "content": "hi", "sender": "coordinator"
|
|
@@ -35,12 +34,17 @@ fn fail_leader_delivery_returns_fallback_log_ok_true_not_submitted() {
|
|
|
35
34
|
Some("No direct leader tmux pane is attached. Run team-agent attach-leader."),
|
|
36
35
|
)
|
|
37
36
|
.unwrap();
|
|
38
|
-
|
|
39
|
-
assert!(out.ok);
|
|
37
|
+
assert!(!out.ok);
|
|
40
38
|
assert_eq!(out.status, DeliveryStatus::FallbackLog);
|
|
41
39
|
assert_eq!(out.reason, Some(DeliveryRefusal::LeaderNotAttached));
|
|
42
|
-
|
|
40
|
+
assert_eq!(out.message_id, None);
|
|
43
41
|
assert_ne!(out.status, DeliveryStatus::Delivered);
|
|
42
|
+
let store = MessageStore::open(&ws).unwrap();
|
|
43
|
+
let connection = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
44
|
+
let rows: i64 = connection
|
|
45
|
+
.query_row("select count(*) from messages", [], |row| row.get(0))
|
|
46
|
+
.unwrap();
|
|
47
|
+
assert_eq!(rows, 0, "diagnostics cannot create a fallback message row");
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -439,28 +443,30 @@ fn message_not_silently_stuck_accepted_when_coordinator_dead() {
|
|
|
439
443
|
)
|
|
440
444
|
.unwrap();
|
|
441
445
|
|
|
442
|
-
assert!(
|
|
443
|
-
assert_eq!(out.status, DeliveryStatus::
|
|
444
|
-
assert_eq!(out.message_status.0, "
|
|
446
|
+
assert!(out.ok, "durable persistence is the send success boundary");
|
|
447
|
+
assert_eq!(out.status, DeliveryStatus::Blocked);
|
|
448
|
+
assert_eq!(out.message_status.0, "queued_coordinator_unavailable");
|
|
449
|
+
assert!(out.message_id.as_deref().is_some_and(|id| id.starts_with("msg_")));
|
|
445
450
|
assert_eq!(out.reason, Some(DeliveryRefusal::CoordinatorUnavailable));
|
|
446
451
|
assert!(
|
|
447
452
|
out.verification
|
|
448
453
|
.as_deref()
|
|
449
454
|
.is_some_and(|warning| warning.contains("coordinator is not running")),
|
|
450
|
-
"N38 warning must explain
|
|
455
|
+
"N38 warning must explain the durable retry blocker; out={out:?}"
|
|
451
456
|
);
|
|
452
457
|
let store = MessageStore::open(&ws).unwrap();
|
|
453
458
|
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
454
|
-
let
|
|
459
|
+
let blocked: i64 = conn
|
|
455
460
|
.query_row(
|
|
456
|
-
"select count(*) from messages where status = '
|
|
461
|
+
"select count(*) from messages where status = 'queued_coordinator_unavailable' \
|
|
462
|
+
and error = 'coordinator_unavailable' and delivered_at is null",
|
|
457
463
|
[],
|
|
458
464
|
|row| row.get(0),
|
|
459
465
|
)
|
|
460
466
|
.unwrap();
|
|
461
467
|
assert_eq!(
|
|
462
|
-
|
|
463
|
-
"dead coordinator send must
|
|
468
|
+
blocked, 1,
|
|
469
|
+
"dead coordinator send must park exactly one durable, non-delivered row"
|
|
464
470
|
);
|
|
465
471
|
let events = EventLog::new(&ws).tail(20).unwrap();
|
|
466
472
|
assert!(
|
|
@@ -470,7 +476,7 @@ fn message_not_silently_stuck_accepted_when_coordinator_dead() {
|
|
|
470
476
|
&& event
|
|
471
477
|
.get("message_queued")
|
|
472
478
|
.and_then(serde_json::Value::as_bool)
|
|
473
|
-
== Some(
|
|
479
|
+
== Some(true)
|
|
474
480
|
}),
|
|
475
481
|
"send.coordinator_unavailable event must be durable; events={events:?}"
|
|
476
482
|
);
|
|
@@ -2914,9 +2920,15 @@ fn gate054_rebind_required_status_refuses_leader_delivery_without_cross_server_p
|
|
|
2914
2920
|
}
|
|
2915
2921
|
|
|
2916
2922
|
#[test]
|
|
2917
|
-
fn
|
|
2923
|
+
fn gate054_attached_status_still_accepts_when_pane_is_live() {
|
|
2918
2924
|
// Companion negative: prove the new status gate does NOT over-refuse when
|
|
2919
2925
|
// `status="attached"` and the workspace-canonical socket has the pane.
|
|
2926
|
+
// Injection-case revision: transport submit success is NOT a provider
|
|
2927
|
+
// receipt, so the old `delivered` pin was a false-positive lock
|
|
2928
|
+
// (MUST-10: physical submit is the framework's fact; acceptance is the
|
|
2929
|
+
// provider's). The un-refused path now parks the SAME row in
|
|
2930
|
+
// submitted_pending_acceptance until a provider receipt or retry
|
|
2931
|
+
// schedule advances it.
|
|
2920
2932
|
let ws = tmp_ws("gate054ok");
|
|
2921
2933
|
let store = store_for(&ws);
|
|
2922
2934
|
let log = EventLog::new(&ws);
|
|
@@ -2945,7 +2957,16 @@ fn gate054_attached_status_still_delivers_when_pane_is_live() {
|
|
|
2945
2957
|
out.ok,
|
|
2946
2958
|
"0.5.4 gate054: status='attached' with a live pane must not be over-refused: {out:?}"
|
|
2947
2959
|
);
|
|
2948
|
-
assert_eq!(
|
|
2960
|
+
assert_eq!(
|
|
2961
|
+
transport.inject_targets().len(),
|
|
2962
|
+
1,
|
|
2963
|
+
"gate054: exactly one physical inject must have happened (MUST-10 anchor)"
|
|
2964
|
+
);
|
|
2965
|
+
assert_eq!(
|
|
2966
|
+
out.message_status.0, "submitted_pending_acceptance",
|
|
2967
|
+
"gate054 (injection-case revision): a successful physical submit without a provider \
|
|
2968
|
+
receipt must park as submitted_pending_acceptance, never claim delivered: {out:?}"
|
|
2969
|
+
);
|
|
2949
2970
|
}
|
|
2950
2971
|
|
|
2951
2972
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -49,6 +49,7 @@ pub fn notify_result_watchers(
|
|
|
49
49
|
if idx == 0 {
|
|
50
50
|
primary_watcher_id = Some(watcher_id.to_string());
|
|
51
51
|
notices.push(deliver_primary_watcher(
|
|
52
|
+
workspace,
|
|
52
53
|
&conn,
|
|
53
54
|
&store,
|
|
54
55
|
event_log,
|
|
@@ -112,6 +113,7 @@ fn watcher_matches(
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
fn deliver_primary_watcher(
|
|
116
|
+
workspace: &Path,
|
|
115
117
|
conn: &rusqlite::Connection,
|
|
116
118
|
store: &MessageStore,
|
|
117
119
|
event_log: &EventLog,
|
|
@@ -178,7 +180,10 @@ fn deliver_primary_watcher(
|
|
|
178
180
|
);
|
|
179
181
|
}
|
|
180
182
|
let content = format_result_watcher_notification(result);
|
|
181
|
-
let
|
|
183
|
+
let super::PersistResolution::Persisted(persisted) = super::persist::persist_internal_send(
|
|
184
|
+
workspace,
|
|
185
|
+
super::InternalSendKind::Watcher,
|
|
186
|
+
watcher.get("owner_team_id").and_then(|v| v.as_str()),
|
|
182
187
|
result_task,
|
|
183
188
|
watcher
|
|
184
189
|
.get("leader_id")
|
|
@@ -188,8 +193,13 @@ fn deliver_primary_watcher(
|
|
|
188
193
|
&content,
|
|
189
194
|
None,
|
|
190
195
|
false,
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
None,
|
|
197
|
+
super::InitialDisposition::Accepted,
|
|
198
|
+
)?
|
|
199
|
+
else {
|
|
200
|
+
unreachable!("watcher notifications do not accept caller-supplied ids")
|
|
201
|
+
};
|
|
202
|
+
let message_id = persisted.message_id;
|
|
193
203
|
let claim = store.claim_leader_notification_delivery(NotificationClaimParams {
|
|
194
204
|
result_id,
|
|
195
205
|
owner_team_id: watcher.get("owner_team_id").and_then(|v| v.as_str()),
|
|
@@ -13,6 +13,7 @@ const SENSITIVE_FAMILIES: [&str; 7] = [
|
|
|
13
13
|
"SECRET",
|
|
14
14
|
"CREDENTIAL",
|
|
15
15
|
];
|
|
16
|
+
const SENSITIVE_KEYS: [&str; 2] = ["authorization_header", "credential_blob"];
|
|
16
17
|
const STRUCTURAL_KEYS: [&str; 13] = [
|
|
17
18
|
"active_team_key",
|
|
18
19
|
"cohort_key",
|
|
@@ -31,7 +32,7 @@ const STRUCTURAL_KEYS: [&str; 13] = [
|
|
|
31
32
|
|
|
32
33
|
static SHELL_ASSIGNMENT: LazyLock<Regex> = LazyLock::new(|| {
|
|
33
34
|
Regex::new(
|
|
34
|
-
r#"(?i)(?P<prefix>^|[\s\[,;('"])(?P<key>[a-z_][a-z0-9_]*)=(?P<value>'[^']*'|"[^"]*"|[^\s,\]\[};)]+)"#,
|
|
35
|
+
r#"(?i)(?P<prefix>^|[\s\[,;('"])(?P<key>[a-z_][a-z0-9_.]*)=(?P<value>'[^']*'|"[^"]*"|[^\s,\]\[};)]+)"#,
|
|
35
36
|
)
|
|
36
37
|
.expect("shell assignment redaction regex")
|
|
37
38
|
});
|
|
@@ -41,6 +42,13 @@ static URL_USERINFO: LazyLock<Regex> = LazyLock::new(|| {
|
|
|
41
42
|
.expect("URL userinfo redaction regex")
|
|
42
43
|
});
|
|
43
44
|
|
|
45
|
+
static BEARER_TOKEN: LazyLock<Regex> = LazyLock::new(|| {
|
|
46
|
+
Regex::new(
|
|
47
|
+
r"(?i)(?P<prefix>\bBearer[ \t]+)(?P<token>[A-Za-z0-9][-A-Za-z0-9._~+/=]{14,}[-A-Za-z0-9._~+/=])",
|
|
48
|
+
)
|
|
49
|
+
.expect("Bearer token redaction regex")
|
|
50
|
+
});
|
|
51
|
+
|
|
44
52
|
pub(crate) fn redact_external_value(value: &serde_json::Value) -> serde_json::Value {
|
|
45
53
|
match value {
|
|
46
54
|
Value::Object(object) => Value::Object(
|
|
@@ -49,6 +57,8 @@ pub(crate) fn redact_external_value(value: &serde_json::Value) -> serde_json::Va
|
|
|
49
57
|
.map(|(key, value)| {
|
|
50
58
|
let value = if is_sensitive_env_key(key) {
|
|
51
59
|
Value::String(REDACTED.to_string())
|
|
60
|
+
} else if is_argument_array_key(key) {
|
|
61
|
+
redact_argument_array(value)
|
|
52
62
|
} else {
|
|
53
63
|
redact_external_value(value)
|
|
54
64
|
};
|
|
@@ -62,6 +72,33 @@ pub(crate) fn redact_external_value(value: &serde_json::Value) -> serde_json::Va
|
|
|
62
72
|
}
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
fn is_argument_array_key(key: &str) -> bool {
|
|
76
|
+
key.eq_ignore_ascii_case("argv") || key.eq_ignore_ascii_case("command")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn redact_argument_array(value: &Value) -> Value {
|
|
80
|
+
let Value::Array(values) = value else {
|
|
81
|
+
return redact_external_value(value);
|
|
82
|
+
};
|
|
83
|
+
let mut redact_next = false;
|
|
84
|
+
Value::Array(
|
|
85
|
+
values
|
|
86
|
+
.iter()
|
|
87
|
+
.map(|value| {
|
|
88
|
+
if redact_next {
|
|
89
|
+
redact_next = false;
|
|
90
|
+
return Value::String(REDACTED.to_string());
|
|
91
|
+
}
|
|
92
|
+
if let Some(flag) = value.as_str().filter(|flag| flag.starts_with('-')) {
|
|
93
|
+
let normalized = flag.trim_start_matches('-').replace('-', "_");
|
|
94
|
+
redact_next = is_sensitive_env_key(&normalized);
|
|
95
|
+
}
|
|
96
|
+
redact_external_value(value)
|
|
97
|
+
})
|
|
98
|
+
.collect(),
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
65
102
|
pub(crate) fn redact_external_text(text: &str) -> String {
|
|
66
103
|
let assignments = SHELL_ASSIGNMENT.replace_all(text, |captures: &Captures<'_>| {
|
|
67
104
|
let key = captures.name("key").map_or("", |value| value.as_str());
|
|
@@ -90,18 +127,28 @@ pub(crate) fn redact_external_text(text: &str) -> String {
|
|
|
90
127
|
};
|
|
91
128
|
format!("{prefix}{key}={quote}{safe_value}{quote}")
|
|
92
129
|
});
|
|
93
|
-
URL_USERINFO
|
|
130
|
+
let urls = URL_USERINFO
|
|
94
131
|
.replace_all(&assignments, "${scheme}[REDACTED]@")
|
|
132
|
+
.into_owned();
|
|
133
|
+
BEARER_TOKEN
|
|
134
|
+
.replace_all(&urls, "${prefix}[REDACTED]")
|
|
95
135
|
.into_owned()
|
|
96
136
|
}
|
|
97
137
|
|
|
98
138
|
fn is_sensitive_env_key(key: &str) -> bool {
|
|
139
|
+
let key = key.rsplit('.').next().unwrap_or(key);
|
|
99
140
|
if STRUCTURAL_KEYS
|
|
100
141
|
.iter()
|
|
101
142
|
.any(|structural| key.eq_ignore_ascii_case(structural))
|
|
102
143
|
{
|
|
103
144
|
return false;
|
|
104
145
|
}
|
|
146
|
+
if SENSITIVE_KEYS
|
|
147
|
+
.iter()
|
|
148
|
+
.any(|sensitive| key.eq_ignore_ascii_case(sensitive))
|
|
149
|
+
{
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
105
152
|
let key = key.to_ascii_uppercase();
|
|
106
153
|
SENSITIVE_FAMILIES
|
|
107
154
|
.iter()
|
|
@@ -182,4 +229,27 @@ mod tests {
|
|
|
182
229
|
assert!(text.contains("https://[REDACTED]@proxy.invalid:8443/path"));
|
|
183
230
|
assert_eq!(twice, once);
|
|
184
231
|
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn event_log_shapes_are_masked_without_hiding_structural_arguments() {
|
|
235
|
+
let marker = "synthetic-event-log-marker";
|
|
236
|
+
let input = json!({
|
|
237
|
+
"authorization_header": format!("Bearer {marker}"),
|
|
238
|
+
"credential_blob": marker,
|
|
239
|
+
"config": format!("mcp_servers.demo.env.OPENAI_API_KEY=\"{marker}\""),
|
|
240
|
+
"argv": ["provider", "--api-key", marker, "--team-key", "current"],
|
|
241
|
+
"command": ["provider", "--authorization-header", marker, "--auth-mode", "subscription"],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
let redacted = redact_external_value(&input);
|
|
245
|
+
|
|
246
|
+
assert!(!redacted.to_string().contains(marker));
|
|
247
|
+
assert_eq!(redacted["authorization_header"], REDACTED);
|
|
248
|
+
assert_eq!(redacted["credential_blob"], REDACTED);
|
|
249
|
+
assert_eq!(redacted["argv"][2], REDACTED);
|
|
250
|
+
assert_eq!(redacted["argv"][4], "current");
|
|
251
|
+
assert_eq!(redacted["command"][2], REDACTED);
|
|
252
|
+
assert_eq!(redacted["command"][4], "subscription");
|
|
253
|
+
assert_eq!(redact_external_value(&redacted), redacted);
|
|
254
|
+
}
|
|
185
255
|
}
|
|
@@ -47,7 +47,7 @@ const SESSION_STATE_FIELDS: [&str; 6] = [
|
|
|
47
47
|
];
|
|
48
48
|
const LIVE_TOPOLOGY_FIELDS: [&str; 5] =
|
|
49
49
|
["pane_id", "pane_pid", "window", "spawned_at", "spawn_epoch"];
|
|
50
|
-
const ROSTER_STUB_ALLOWLIST: [&str;
|
|
50
|
+
const ROSTER_STUB_ALLOWLIST: [&str; 16] = [
|
|
51
51
|
"agent_id",
|
|
52
52
|
"provider",
|
|
53
53
|
"auth_mode",
|
|
@@ -57,6 +57,7 @@ const ROSTER_STUB_ALLOWLIST: [&str; 15] = [
|
|
|
57
57
|
"profile",
|
|
58
58
|
"_profile_dir",
|
|
59
59
|
"dynamic_role_file",
|
|
60
|
+
"role_source_ownership",
|
|
60
61
|
"effort",
|
|
61
62
|
"forked_from",
|
|
62
63
|
"managed_mcp_config",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
use super::{reapply_scope, ReapplyScope, StateWriteIntent};
|
|
2
|
+
|
|
3
|
+
#[test]
|
|
4
|
+
fn mcp_reapply_scope_matches_legacy_helpers() {
|
|
5
|
+
assert!(
|
|
6
|
+
reapply_scope(&StateWriteIntent::McpUpdateStateNote {
|
|
7
|
+
team_key: Some("team-a"),
|
|
8
|
+
}) == ReapplyScope::Team
|
|
9
|
+
);
|
|
10
|
+
assert!(
|
|
11
|
+
reapply_scope(&StateWriteIntent::McpAssignTask {
|
|
12
|
+
team_key: Some("team-a"),
|
|
13
|
+
task_id: "task-a",
|
|
14
|
+
}) == ReapplyScope::Root
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[test]
|
|
19
|
+
fn optional_unmigrated_read_preserves_missing_corrupt_and_valid_shapes(
|
|
20
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
21
|
+
let nonce = std::time::SystemTime::now()
|
|
22
|
+
.duration_since(std::time::UNIX_EPOCH)?
|
|
23
|
+
.as_nanos();
|
|
24
|
+
let workspace = std::env::temp_dir().join(format!("state-repository-read-{nonce}"));
|
|
25
|
+
let repository = super::StateRepository::new(&workspace);
|
|
26
|
+
assert!(repository
|
|
27
|
+
.load_workspace_if_exists_without_migrations()?
|
|
28
|
+
.is_none());
|
|
29
|
+
|
|
30
|
+
let path = super::helper_workspace_path(&workspace);
|
|
31
|
+
if let Some(parent) = path.parent() {
|
|
32
|
+
std::fs::create_dir_all(parent)?;
|
|
33
|
+
}
|
|
34
|
+
std::fs::write(&path, b"{not-json")?;
|
|
35
|
+
assert!(matches!(
|
|
36
|
+
repository.load_workspace_if_exists_without_migrations(),
|
|
37
|
+
Err(super::StateError::Json(_))
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
std::fs::write(&path, br#"{"legacy":true}"#)?;
|
|
41
|
+
assert_eq!(
|
|
42
|
+
repository.load_workspace_if_exists_without_migrations()?,
|
|
43
|
+
Some(serde_json::json!({"legacy": true}))
|
|
44
|
+
);
|
|
45
|
+
std::fs::remove_dir_all(workspace)?;
|
|
46
|
+
Ok(())
|
|
47
|
+
}
|
|
@@ -40,7 +40,8 @@ use super::StateError;
|
|
|
40
40
|
// `save_team_scoped_state(` tokens that the governance scanner counts.
|
|
41
41
|
#[allow(unused_imports)]
|
|
42
42
|
use super::persist::{
|
|
43
|
-
load_runtime_state as helper_load_workspace,
|
|
43
|
+
load_runtime_state as helper_load_workspace,
|
|
44
|
+
runtime_state_path as helper_workspace_path, save_runtime_state as helper_write_root,
|
|
44
45
|
save_runtime_state_reapplying_after_conflict as helper_write_root_reapply,
|
|
45
46
|
save_runtime_state_with_deleted_agents as helper_write_root_with_deleted_agents,
|
|
46
47
|
save_runtime_state_with_lifecycle_topology_authority as helper_write_root_with_lifecycle_topology_authority,
|
|
@@ -79,6 +80,19 @@ impl<'a> StateRepository<'a> {
|
|
|
79
80
|
helper_load_workspace(self.workspace)
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
/// Load the canonical workspace document without running read-time
|
|
84
|
+
/// migrations. `None` preserves the legacy raw-reader distinction between
|
|
85
|
+
/// a missing file and a present empty/default document.
|
|
86
|
+
pub fn load_workspace_if_exists_without_migrations(
|
|
87
|
+
&self,
|
|
88
|
+
) -> Result<Option<Value>, StateError> {
|
|
89
|
+
if !helper_workspace_path(self.workspace).exists() {
|
|
90
|
+
return Ok(None);
|
|
91
|
+
}
|
|
92
|
+
let text = std::fs::read_to_string(helper_workspace_path(self.workspace))?;
|
|
93
|
+
serde_json::from_str(&text).map(Some).map_err(StateError::from)
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
/// Resolve a team-scoped projection using the existing projection selector.
|
|
83
97
|
/// `team_key = None` selects the ambient team the same way as pre-S1a.
|
|
84
98
|
pub fn load_team(&self, team_key: Option<&str>) -> Result<Value, StateError> {
|
|
@@ -171,6 +185,10 @@ pub enum StateWriteIntent<'a> {
|
|
|
171
185
|
team_key: Option<&'a str>,
|
|
172
186
|
agent_id: &'a str,
|
|
173
187
|
},
|
|
188
|
+
ForceRecreateRollback {
|
|
189
|
+
team_key: &'a str,
|
|
190
|
+
agent_id: &'a str,
|
|
191
|
+
},
|
|
174
192
|
ClaimLeader {
|
|
175
193
|
team_key: &'a str,
|
|
176
194
|
},
|
|
@@ -193,7 +211,7 @@ pub enum StateWriteIntent<'a> {
|
|
|
193
211
|
/// so future S1b migration can route it deliberately.
|
|
194
212
|
CoordinatorApiErrorRecovery {
|
|
195
213
|
team_key: Option<&'a str>,
|
|
196
|
-
agent_id:
|
|
214
|
+
agent_id: Option<&'a str>,
|
|
197
215
|
},
|
|
198
216
|
McpAssignTask {
|
|
199
217
|
team_key: Option<&'a str>,
|
|
@@ -331,6 +349,16 @@ fn route_direct(
|
|
|
331
349
|
Some(_) => helper_write_team_scoped_with_deleted_agents(workspace, state, &[agent_id]),
|
|
332
350
|
None => helper_write_root_with_deleted_agents(workspace, state, &[agent_id]),
|
|
333
351
|
},
|
|
352
|
+
// Force-recreate rollback restores an existing row after the
|
|
353
|
+
// replacement spawn has advanced its lifecycle tuple. The selected
|
|
354
|
+
// row is therefore the explicit topology authority.
|
|
355
|
+
StateWriteIntent::ForceRecreateRollback { agent_id, .. } => {
|
|
356
|
+
helper_write_team_scoped_with_lifecycle_topology_authority(
|
|
357
|
+
workspace,
|
|
358
|
+
state,
|
|
359
|
+
&[agent_id],
|
|
360
|
+
)
|
|
361
|
+
}
|
|
334
362
|
// ClaimLeader -> leader/lease.rs:1625 uses the root helper, and the
|
|
335
363
|
// scoped preserve-claim-fields variant at :1702 uses the team-tombstoned
|
|
336
364
|
// agents helper.
|
|
@@ -396,16 +424,14 @@ fn route_direct(
|
|
|
396
424
|
// Route intents that historically used the `_reapplying_after_conflict`
|
|
397
425
|
// helper family. Behavior stays identical: S1a chooses the same helper the
|
|
398
426
|
// legacy caller would have chosen for a reapply.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
{
|
|
408
|
-
let use_team_scoped = matches!(
|
|
427
|
+
#[derive(Clone, Copy, Eq, PartialEq)]
|
|
428
|
+
enum ReapplyScope {
|
|
429
|
+
Root,
|
|
430
|
+
Team,
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
fn reapply_scope(intent: &StateWriteIntent<'_>) -> ReapplyScope {
|
|
434
|
+
if matches!(
|
|
409
435
|
intent,
|
|
410
436
|
StateWriteIntent::RestartSessionRepair { .. }
|
|
411
437
|
| StateWriteIntent::MessagingDeliveryState {
|
|
@@ -415,14 +441,31 @@ where
|
|
|
415
441
|
owner_team_id: Some(_),
|
|
416
442
|
}
|
|
417
443
|
| StateWriteIntent::CoordinatorTick { .. }
|
|
418
|
-
| StateWriteIntent::
|
|
444
|
+
| StateWriteIntent::McpUpdateStateNote {
|
|
419
445
|
team_key: Some(_),
|
|
420
|
-
..
|
|
421
446
|
}
|
|
422
|
-
)
|
|
423
|
-
|
|
447
|
+
) {
|
|
448
|
+
ReapplyScope::Team
|
|
449
|
+
} else {
|
|
450
|
+
ReapplyScope::Root
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fn route_reapply<F>(
|
|
455
|
+
workspace: &Path,
|
|
456
|
+
intent: StateWriteIntent<'_>,
|
|
457
|
+
state: &Value,
|
|
458
|
+
reapply: F,
|
|
459
|
+
) -> Result<(), StateError>
|
|
460
|
+
where
|
|
461
|
+
F: FnOnce(&mut Value),
|
|
462
|
+
{
|
|
463
|
+
if reapply_scope(&intent) == ReapplyScope::Team {
|
|
424
464
|
helper_write_team_scoped_reapply(workspace, state, reapply)
|
|
425
465
|
} else {
|
|
426
466
|
helper_write_root_reapply(workspace, state, reapply)
|
|
427
467
|
}
|
|
428
468
|
}
|
|
469
|
+
|
|
470
|
+
#[cfg(test)]
|
|
471
|
+
mod tests;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.51",
|
|
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.51",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.51",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.51"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|