@team-agent/installer 0.5.118 → 0.5.120
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/README.md +46 -1
- package/README.zh.md +46 -1
- package/crates/team-agent/src/cli/adapters.rs +11 -0
- package/crates/team-agent/src/cli/arguments.rs +401 -0
- package/crates/team-agent/src/cli/emit.rs +241 -142
- package/crates/team-agent/src/cli/intent/runtime.rs +1139 -0
- package/crates/team-agent/src/cli/intent/runtime_tests.rs +584 -0
- package/crates/team-agent/src/cli/intent/tests.rs +654 -0
- package/crates/team-agent/src/cli/intent.rs +642 -0
- package/crates/team-agent/src/cli/mod.rs +49 -5
- package/crates/team-agent/src/cli/named_address.rs +10 -1
- package/crates/team-agent/src/cli/spec.rs +22 -10
- package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
- package/crates/team-agent/src/cli/tests/base.rs +6 -1
- package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
- package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
- package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +10 -3
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
- package/crates/team-agent/src/lifecycle/launch.rs +28 -0
- package/crates/team-agent/src/lifecycle/restart.rs +17 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +4 -2
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +4 -3
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
- package/crates/team-agent/src/lifecycle/types.rs +10 -0
- package/crates/team-agent/src/messaging/delivery.rs +132 -39
- package/crates/team-agent/src/messaging/delivery_consumption_tests.rs +223 -0
- package/crates/team-agent/src/messaging/enter_rescue.rs +182 -154
- package/crates/team-agent/src/messaging/enter_rescue_tests.rs +344 -277
- package/crates/team-agent/src/messaging/mod.rs +1 -1
- package/crates/team-agent/src/tmux_backend/enter_rescue.rs +325 -0
- package/crates/team-agent/src/tmux_backend/enter_rescue_tests.rs +432 -0
- package/crates/team-agent/src/tmux_backend/native_input.rs +19 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
- package/crates/team-agent/src/tmux_backend.rs +35 -1
- package/crates/team-agent/src/transport/enter_rescue.rs +166 -0
- package/crates/team-agent/src/transport/native_input.rs +5 -1
- package/crates/team-agent/src/transport/test_support.rs +25 -0
- package/crates/team-agent/src/transport.rs +34 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +71 -54
- package/skills/team-agent/command-coverage.json +283 -13
- package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
- package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
- package/skills/team-agent/references/recovery-runbook.md +61 -212
- package/skills/team-agent/references/team-agent-operator.md +181 -48
- package/skills/team-agent/references/team-in-team.md +47 -80
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//! Durable Enter-only
|
|
1
|
+
//! Durable independent Enter-only watchdog. An uncertain effect is never replayed.
|
|
2
2
|
|
|
3
3
|
use std::path::Path;
|
|
4
4
|
|
|
5
5
|
use chrono::{DateTime, Duration, Utc};
|
|
6
|
-
use rusqlite::{params, OptionalExtension};
|
|
6
|
+
use rusqlite::{params, OptionalExtension, Transaction};
|
|
7
7
|
use serde::{Deserialize, Serialize};
|
|
8
8
|
use sha2::{Digest, Sha256};
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ use super::delivery::{render_message, top_level_state_matches_owner_team};
|
|
|
11
11
|
use super::MessagingError;
|
|
12
12
|
use crate::event_log::EventLog;
|
|
13
13
|
use crate::message_store::MessageStore;
|
|
14
|
-
use crate::transport::
|
|
14
|
+
use crate::transport::enter_rescue::{EnterRescueContext, EnterRescueOutcome};
|
|
15
15
|
use crate::transport::Transport;
|
|
16
16
|
|
|
17
17
|
#[cfg(test)]
|
|
@@ -23,7 +23,9 @@ const EXPIRE_SECONDS: i64 = 120;
|
|
|
23
23
|
|
|
24
24
|
#[derive(Debug, Serialize, Deserialize)]
|
|
25
25
|
struct RescueJob {
|
|
26
|
-
|
|
26
|
+
schema: u8,
|
|
27
|
+
context: EnterRescueContext,
|
|
28
|
+
registered_at: String,
|
|
27
29
|
expires_at: String,
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -31,105 +33,134 @@ fn digest(text: &str) -> String {
|
|
|
31
33
|
format!("{:x}", Sha256::digest(text.as_bytes()))
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
fn
|
|
35
|
-
DateTime::parse_from_rfc3339(
|
|
36
|
+
fn timestamp(value: &str) -> Result<DateTime<Utc>, MessagingError> {
|
|
37
|
+
DateTime::parse_from_rfc3339(value)
|
|
36
38
|
.map(|value| value.with_timezone(&Utc))
|
|
37
|
-
.map_err(|_| MessagingError::Validation("enter_rescue: invalid
|
|
39
|
+
.map_err(|_| MessagingError::Validation("enter_rescue: invalid timestamp".into()))
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
///
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
/// Called in the message-status transaction, even when no carrier/proof exists.
|
|
43
|
+
/// One original registration survives duplicates, unknown effects and restarts.
|
|
44
|
+
pub(crate) fn schedule_in_tx(
|
|
45
|
+
tx: &Transaction<'_>,
|
|
43
46
|
message_id: &str,
|
|
44
|
-
attempt: u32,
|
|
45
|
-
) -> Result<(), MessagingError> {
|
|
46
|
-
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
47
|
-
conn.execute(
|
|
48
|
-
"update messages set error='native_input_not_ready', delivery_attempts=delivery_attempts-1, updated_at=?3
|
|
49
|
-
where message_id=?1 and status='target_resolved' and error is null and delivery_attempts=?2",
|
|
50
|
-
params![message_id, attempt, Utc::now().to_rfc3339()],
|
|
51
|
-
)?;
|
|
52
|
-
Ok(())
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/// Original uncertainty and its one job are committed together under the same CAS.
|
|
56
|
-
pub(super) fn park(
|
|
57
|
-
store: &MessageStore,
|
|
58
|
-
evidence: &NativeInputEvidence,
|
|
59
|
-
attempt: u32,
|
|
60
|
-
reason: &str,
|
|
61
|
-
log: &EventLog,
|
|
62
|
-
) -> Result<(), MessagingError> {
|
|
63
|
-
park_at(store, evidence, attempt, reason, log, Utc::now())
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
fn park_at(
|
|
67
|
-
store: &MessageStore,
|
|
68
|
-
evidence: &NativeInputEvidence,
|
|
69
|
-
attempt: u32,
|
|
70
|
-
reason: &str,
|
|
71
|
-
log: &EventLog,
|
|
72
47
|
now: DateTime<Utc>,
|
|
73
|
-
) -> Result<(),
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
let
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
and status='target_resolved' and error is null and delivery_attempts=?6",
|
|
85
|
-
params![
|
|
86
|
-
evidence.request.message_id,
|
|
87
|
-
evidence.request.owner_team_id,
|
|
88
|
-
evidence.request.recipient,
|
|
89
|
-
reason,
|
|
90
|
-
now.to_rfc3339(),
|
|
91
|
-
attempt
|
|
92
|
-
],
|
|
93
|
-
)?;
|
|
94
|
-
if changed != 1 {
|
|
95
|
-
return Err(MessagingError::Validation(
|
|
96
|
-
"enter_rescue: claim changed".into(),
|
|
97
|
-
));
|
|
98
|
-
}
|
|
48
|
+
) -> Result<(), rusqlite::Error> {
|
|
49
|
+
let row = tx.query_row(
|
|
50
|
+
"select owner_team_id,coalesce(recipient,''),coalesce(sender,''),task_id,coalesce(content,'')
|
|
51
|
+
from messages where message_id=?1 and status='submitted_unverified'",
|
|
52
|
+
[message_id],
|
|
53
|
+
|row| Ok((row.get::<_, Option<String>>(0)?, row.get::<_, String>(1)?,
|
|
54
|
+
row.get::<_, String>(2)?, row.get::<_, Option<String>>(3)?, row.get::<_, String>(4)?)),
|
|
55
|
+
).optional()?;
|
|
56
|
+
let Some((owner, recipient, sender, task, content)) = row else {
|
|
57
|
+
return Ok(());
|
|
58
|
+
};
|
|
99
59
|
let job = RescueJob {
|
|
100
|
-
|
|
60
|
+
schema: 1,
|
|
61
|
+
context: EnterRescueContext {
|
|
62
|
+
message_id: message_id.into(),
|
|
63
|
+
owner_team_id: owner.clone().unwrap_or_default(),
|
|
64
|
+
recipient: recipient.clone(),
|
|
65
|
+
payload_sha256: digest(&render_message(
|
|
66
|
+
&sender,
|
|
67
|
+
task.as_deref(),
|
|
68
|
+
&content,
|
|
69
|
+
message_id,
|
|
70
|
+
)),
|
|
71
|
+
target: None,
|
|
72
|
+
proof: None,
|
|
73
|
+
},
|
|
74
|
+
registered_at: now.to_rfc3339(),
|
|
101
75
|
expires_at: (now + Duration::seconds(EXPIRE_SECONDS)).to_rfc3339(),
|
|
102
76
|
};
|
|
77
|
+
let payload = serde_json::to_string(&job)
|
|
78
|
+
.map_err(|error| rusqlite::Error::ToSqlConversionFailure(Box::new(error)))?;
|
|
103
79
|
tx.execute(
|
|
104
|
-
"insert into scheduled_events(kind,target,payload_json,due_at,status,owner_team_id,created_at)
|
|
105
|
-
|
|
106
|
-
|
|
80
|
+
"insert into scheduled_events(kind,target,payload_json,due_at,status,owner_team_id,created_at)
|
|
81
|
+
select 'enter_rescue',?1,?2,?3,'pending',?4,?5 where not exists(
|
|
82
|
+
select 1 from scheduled_events where kind='enter_rescue' and case when json_valid(payload_json)
|
|
83
|
+
then coalesce(json_extract(payload_json,'$.context.message_id'),json_extract(payload_json,'$.evidence.request.message_id')) end=?6)",
|
|
84
|
+
params![recipient, payload, (now + Duration::seconds(DELAY_SECONDS)).to_rfc3339(), owner,
|
|
85
|
+
now.to_rfc3339(), message_id],
|
|
107
86
|
)?;
|
|
108
|
-
tx.commit()?;
|
|
109
|
-
log.write("enter_rescue.scheduled", serde_json::json!({
|
|
110
|
-
"message_id": evidence.request.message_id, "recipient": evidence.request.recipient,
|
|
111
|
-
"phase": evidence.phase, "delay_seconds": DELAY_SECONDS, "expire_seconds": EXPIRE_SECONDS,
|
|
112
|
-
}))?;
|
|
113
87
|
Ok(())
|
|
114
88
|
}
|
|
115
89
|
|
|
90
|
+
/// Supplement the original job at the original delivery boundary, never at fire.
|
|
91
|
+
/// This cannot change its scope, message digest, initial clock, or a bound carrier.
|
|
92
|
+
pub(crate) fn bind_pending_context(
|
|
93
|
+
store: &MessageStore,
|
|
94
|
+
context: &EnterRescueContext,
|
|
95
|
+
) -> Result<bool, MessagingError> {
|
|
96
|
+
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
97
|
+
let row = conn.query_row(
|
|
98
|
+
"select id,payload_json from scheduled_events where kind='enter_rescue' and status='pending'
|
|
99
|
+
and case when json_valid(payload_json) then json_extract(payload_json,'$.context.message_id') end=?1",
|
|
100
|
+
[&context.message_id], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
|
|
101
|
+
).optional()?;
|
|
102
|
+
let Some((id, previous)) = row else {
|
|
103
|
+
return Ok(false);
|
|
104
|
+
};
|
|
105
|
+
let mut job: RescueJob = serde_json::from_str(&previous)?;
|
|
106
|
+
if job.schema != 1
|
|
107
|
+
|| job.context.message_id != context.message_id
|
|
108
|
+
|| job.context.owner_team_id != context.owner_team_id
|
|
109
|
+
|| job.context.recipient != context.recipient
|
|
110
|
+
|| job.context.payload_sha256 != context.payload_sha256
|
|
111
|
+
|| job.context.target.as_ref().is_some_and(|target| {
|
|
112
|
+
!context
|
|
113
|
+
.target
|
|
114
|
+
.as_ref()
|
|
115
|
+
.is_some_and(|next| target.same_selection(next))
|
|
116
|
+
|| (target.bound() && job.context != *context)
|
|
117
|
+
})
|
|
118
|
+
{
|
|
119
|
+
return Ok(false);
|
|
120
|
+
}
|
|
121
|
+
job.context = context.clone();
|
|
122
|
+
Ok(conn.execute(
|
|
123
|
+
"update scheduled_events set payload_json=?2 where id=?1 and status='pending' and payload_json=?3
|
|
124
|
+
and exists(select 1 from messages where message_id=?4 and status='submitted_unverified'
|
|
125
|
+
and owner_team_id=?5 and recipient=?6)",
|
|
126
|
+
params![id, serde_json::to_string(&job)?, previous, context.message_id,
|
|
127
|
+
context.owner_team_id, context.recipient],
|
|
128
|
+
)? == 1)
|
|
129
|
+
}
|
|
130
|
+
|
|
116
131
|
fn canonical_valid(
|
|
117
132
|
workspace: &Path,
|
|
118
133
|
store: &MessageStore,
|
|
119
134
|
job: &RescueJob,
|
|
120
135
|
) -> Result<bool, MessagingError> {
|
|
121
|
-
let
|
|
122
|
-
let
|
|
123
|
-
|
|
136
|
+
let context = &job.context;
|
|
137
|
+
let registered = timestamp(&job.registered_at)?;
|
|
138
|
+
let expires = timestamp(&job.expires_at)?;
|
|
139
|
+
let now = Utc::now();
|
|
140
|
+
if job.schema != 1
|
|
141
|
+
|| !context.valid_message()
|
|
142
|
+
|| expires != registered + Duration::seconds(EXPIRE_SECONDS)
|
|
143
|
+
|| now < registered + Duration::seconds(DELAY_SECONDS)
|
|
144
|
+
|| now > expires
|
|
145
|
+
{
|
|
124
146
|
return Ok(false);
|
|
125
147
|
}
|
|
126
148
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
127
|
-
let original = conn
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
let original = conn
|
|
150
|
+
.query_row(
|
|
151
|
+
"select sender,task_id,content,created_at from messages where message_id=?1
|
|
152
|
+
and owner_team_id=?2 and recipient=?3 and status='submitted_unverified'",
|
|
153
|
+
params![context.message_id, context.owner_team_id, context.recipient],
|
|
154
|
+
|row| {
|
|
155
|
+
Ok((
|
|
156
|
+
row.get::<_, String>(0)?,
|
|
157
|
+
row.get::<_, Option<String>>(1)?,
|
|
158
|
+
row.get::<_, String>(2)?,
|
|
159
|
+
row.get::<_, String>(3)?,
|
|
160
|
+
))
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
.optional()?;
|
|
133
164
|
let Some((sender, task, content, created_at)) = original else {
|
|
134
165
|
return Ok(false);
|
|
135
166
|
};
|
|
@@ -137,8 +168,8 @@ fn canonical_valid(
|
|
|
137
168
|
&sender,
|
|
138
169
|
task.as_deref(),
|
|
139
170
|
&content,
|
|
140
|
-
&
|
|
141
|
-
)) !=
|
|
171
|
+
&context.message_id,
|
|
172
|
+
)) != context.payload_sha256
|
|
142
173
|
{
|
|
143
174
|
return Ok(false);
|
|
144
175
|
}
|
|
@@ -146,9 +177,9 @@ fn canonical_valid(
|
|
|
146
177
|
"select exists(select 1 from messages where owner_team_id=?1 and recipient=?2
|
|
147
178
|
and message_id<>?3 and created_at>=?4 and delivery_attempts>0)",
|
|
148
179
|
params![
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
180
|
+
context.owner_team_id,
|
|
181
|
+
context.recipient,
|
|
182
|
+
context.message_id,
|
|
152
183
|
created_at
|
|
153
184
|
],
|
|
154
185
|
|row| row.get(0),
|
|
@@ -157,56 +188,59 @@ fn canonical_valid(
|
|
|
157
188
|
return Ok(false);
|
|
158
189
|
}
|
|
159
190
|
let raw = crate::state::persist::load_runtime_state(workspace)?;
|
|
160
|
-
if !matches!(crate::state::projection::resolve_owner_team_id(&raw, &
|
|
161
|
-
crate::state::projection::OwnerTeamResolution::Canonical(team) if team ==
|
|
191
|
+
if !matches!(crate::state::projection::resolve_owner_team_id(&raw, &context.owner_team_id),
|
|
192
|
+
crate::state::projection::OwnerTeamResolution::Canonical(team) if team == context.owner_team_id)
|
|
162
193
|
{
|
|
163
194
|
return Ok(false);
|
|
164
195
|
}
|
|
165
196
|
let state = if raw
|
|
166
197
|
.get("teams")
|
|
167
198
|
.and_then(serde_json::Value::as_object)
|
|
168
|
-
.is_some_and(|teams| teams.contains_key(&
|
|
199
|
+
.is_some_and(|teams| teams.contains_key(&context.owner_team_id))
|
|
169
200
|
{
|
|
170
|
-
crate::state::projection::project_top_level_view(&raw, &
|
|
171
|
-
} else if top_level_state_matches_owner_team(&raw, &
|
|
201
|
+
crate::state::projection::project_top_level_view(&raw, &context.owner_team_id)
|
|
202
|
+
} else if top_level_state_matches_owner_team(&raw, &context.owner_team_id) {
|
|
172
203
|
raw
|
|
173
204
|
} else {
|
|
174
205
|
return Ok(false);
|
|
175
206
|
};
|
|
176
207
|
let Some(agent) = state
|
|
177
208
|
.get("agents")
|
|
178
|
-
.and_then(|agents| agents.get(&
|
|
209
|
+
.and_then(|agents| agents.get(&context.recipient))
|
|
179
210
|
else {
|
|
180
211
|
return Ok(false);
|
|
181
212
|
};
|
|
213
|
+
if agent.get("status").and_then(serde_json::Value::as_str) != Some("running")
|
|
214
|
+
|| agent
|
|
215
|
+
.get("explicit_stop")
|
|
216
|
+
.and_then(serde_json::Value::as_bool)
|
|
217
|
+
== Some(true)
|
|
218
|
+
{
|
|
219
|
+
return Ok(false);
|
|
220
|
+
}
|
|
221
|
+
// An unbound job is still an observable scheduled job, but never key authority.
|
|
222
|
+
let Some(target) = context.target.as_ref() else {
|
|
223
|
+
return Ok(true);
|
|
224
|
+
};
|
|
182
225
|
Ok(
|
|
183
|
-
agent.get("
|
|
184
|
-
&& agent
|
|
185
|
-
.get("explicit_stop")
|
|
186
|
-
.and_then(serde_json::Value::as_bool)
|
|
187
|
-
!= Some(true)
|
|
188
|
-
&& agent.get("pane_id").and_then(serde_json::Value::as_str)
|
|
189
|
-
== Some(request.pane_id.as_str())
|
|
226
|
+
agent.get("pane_id").and_then(serde_json::Value::as_str) == Some(target.pane_id.as_str())
|
|
190
227
|
&& agent.get("pane_pid").and_then(serde_json::Value::as_u64)
|
|
191
|
-
== Some(u64::from(
|
|
192
|
-
&& request.pane_pid == evidence.binding.pane_pid
|
|
228
|
+
== Some(u64::from(target.pane_pid))
|
|
193
229
|
&& state
|
|
194
230
|
.get("session_name")
|
|
195
231
|
.and_then(serde_json::Value::as_str)
|
|
196
|
-
== Some(
|
|
232
|
+
== Some(target.session.as_str())
|
|
197
233
|
&& agent
|
|
198
234
|
.get("window")
|
|
199
235
|
.and_then(serde_json::Value::as_str)
|
|
200
|
-
.unwrap_or(&
|
|
201
|
-
==
|
|
236
|
+
.unwrap_or(&context.recipient)
|
|
237
|
+
== target.window
|
|
202
238
|
&& agent.get("provider").cloned().and_then(|value| {
|
|
203
239
|
serde_json::from_value::<crate::model::enums::Provider>(value).ok()
|
|
204
|
-
}) == Some(
|
|
240
|
+
}) == Some(target.provider),
|
|
205
241
|
)
|
|
206
242
|
}
|
|
207
243
|
|
|
208
|
-
/// Called only for due jobs. CAS precedes every possible effect; stale `claimed`
|
|
209
|
-
/// rows intentionally remain unknown after a crash, rather than becoming runnable.
|
|
210
244
|
pub(super) fn fire(
|
|
211
245
|
workspace: &Path,
|
|
212
246
|
store: &MessageStore,
|
|
@@ -215,56 +249,39 @@ pub(super) fn fire(
|
|
|
215
249
|
id: i64,
|
|
216
250
|
payload: &str,
|
|
217
251
|
) -> Result<bool, MessagingError> {
|
|
218
|
-
fire_with(
|
|
219
|
-
workspace,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
super::delivery::with_rescue_submit_observer(
|
|
226
|
-
workspace,
|
|
227
|
-
store,
|
|
228
|
-
evidence,
|
|
229
|
-
log,
|
|
230
|
-
|observer| {
|
|
231
|
-
transport
|
|
232
|
-
.rescue_enter(evidence, authorize, Some(observer))
|
|
233
|
-
.map_err(Into::into)
|
|
234
|
-
},
|
|
235
|
-
)
|
|
236
|
-
},
|
|
237
|
-
)
|
|
252
|
+
fire_with(workspace, store, log, id, payload, &|context, authorize| {
|
|
253
|
+
super::delivery::with_rescue_submit_observer(workspace, store, context, log, |observer| {
|
|
254
|
+
transport
|
|
255
|
+
.rescue_enter_guarded(context, authorize, Some(observer))
|
|
256
|
+
.map_err(Into::into)
|
|
257
|
+
})
|
|
258
|
+
})
|
|
238
259
|
}
|
|
239
260
|
|
|
261
|
+
type Executor<'a> = dyn Fn(&EnterRescueContext, &dyn Fn() -> bool) -> Result<EnterRescueOutcome, MessagingError>
|
|
262
|
+
+ 'a;
|
|
263
|
+
|
|
240
264
|
fn fire_with(
|
|
241
265
|
workspace: &Path,
|
|
242
266
|
store: &MessageStore,
|
|
243
267
|
log: &EventLog,
|
|
244
268
|
id: i64,
|
|
245
269
|
payload: &str,
|
|
246
|
-
execute: &
|
|
247
|
-
&NativeInputEvidence,
|
|
248
|
-
&dyn Fn() -> bool,
|
|
249
|
-
) -> Result<EnterRescueOutcome, MessagingError>,
|
|
270
|
+
execute: &Executor<'_>,
|
|
250
271
|
) -> Result<bool, MessagingError> {
|
|
251
272
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
252
273
|
if conn.execute(
|
|
253
|
-
"update scheduled_events set status='claimed',
|
|
254
|
-
|
|
274
|
+
"update scheduled_events set status='claimed',fired_at=?2 where id=?1 and kind='enter_rescue'
|
|
275
|
+
and status='pending' and due_at<=?2 and payload_json=?3",
|
|
255
276
|
params![id, Utc::now().to_rfc3339(), payload],
|
|
256
277
|
)? != 1 { return Ok(false); }
|
|
257
|
-
log.write("enter_rescue.claimed", serde_json::json!({"event_id":
|
|
278
|
+
log.write("enter_rescue.claimed", serde_json::json!({"event_id":id}))?;
|
|
258
279
|
let result = (|| -> Result<(String, serde_json::Value, Option<String>), MessagingError> {
|
|
259
280
|
let job: RescueJob = serde_json::from_str(payload)?;
|
|
281
|
+
let context = &job.context;
|
|
260
282
|
let scope_matches: bool = conn.query_row(
|
|
261
|
-
"select owner_team_id=?2 and target=?3 from scheduled_events where id=?1",
|
|
262
|
-
params![
|
|
263
|
-
id,
|
|
264
|
-
job.evidence.request.owner_team_id,
|
|
265
|
-
job.evidence.request.recipient
|
|
266
|
-
],
|
|
267
|
-
|row| row.get(0),
|
|
283
|
+
"select coalesce(owner_team_id,'')=?2 and target=?3 and created_at=?4 from scheduled_events where id=?1",
|
|
284
|
+
params![id, context.owner_team_id, context.recipient, job.registered_at], |row| row.get(0),
|
|
268
285
|
)?;
|
|
269
286
|
if !scope_matches {
|
|
270
287
|
return Ok((
|
|
@@ -280,12 +297,26 @@ fn fire_with(
|
|
|
280
297
|
None,
|
|
281
298
|
));
|
|
282
299
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
300
|
+
if !context.target.as_ref().is_some_and(|target| target.bound()) || !context.valid_proof() {
|
|
301
|
+
return Ok((
|
|
302
|
+
"refused".into(),
|
|
303
|
+
serde_json::json!({"reason":"proof_missing","effect":"none"}),
|
|
304
|
+
None,
|
|
305
|
+
));
|
|
306
|
+
}
|
|
307
|
+
let authorize = || {
|
|
308
|
+
let still_claimed: bool = conn.query_row(
|
|
309
|
+
"select exists(select 1 from scheduled_events where id=?1 and kind='enter_rescue'
|
|
310
|
+
and status='claimed' and payload_json=?2 and owner_team_id=?3 and target=?4)",
|
|
311
|
+
params![id, payload, context.owner_team_id, context.recipient], |row| row.get(0),
|
|
312
|
+
).unwrap_or(false);
|
|
313
|
+
still_claimed && canonical_valid(workspace, store, &job).unwrap_or(false)
|
|
314
|
+
};
|
|
315
|
+
match execute(context, &authorize)? {
|
|
286
316
|
EnterRescueOutcome::NoEffect { reason, reobserve } => {
|
|
287
317
|
let next = Utc::now() + Duration::seconds(5);
|
|
288
|
-
let due =
|
|
318
|
+
let due =
|
|
319
|
+
(reobserve && next <= timestamp(&job.expires_at)?).then(|| next.to_rfc3339());
|
|
289
320
|
Ok((
|
|
290
321
|
if due.is_some() { "pending" } else { "refused" }.into(),
|
|
291
322
|
serde_json::json!({"reason":reason,"effect":"none"}),
|
|
@@ -293,12 +324,10 @@ fn fire_with(
|
|
|
293
324
|
))
|
|
294
325
|
}
|
|
295
326
|
EnterRescueOutcome::Consumed | EnterRescueOutcome::Entered { accepted: true } => {
|
|
296
|
-
// Row-level CAS cannot downgrade an acknowledgement/other terminal owner.
|
|
297
327
|
let changed = conn.execute(
|
|
298
328
|
"update messages set status='delivered',delivered_at=?2,updated_at=?2,error=null
|
|
299
329
|
where message_id=?1 and status='submitted_unverified' and owner_team_id=?3 and recipient=?4",
|
|
300
|
-
params![
|
|
301
|
-
job.evidence.request.owner_team_id, job.evidence.request.recipient],
|
|
330
|
+
params![context.message_id, Utc::now().to_rfc3339(), context.owner_team_id, context.recipient],
|
|
302
331
|
)?;
|
|
303
332
|
Ok((
|
|
304
333
|
"done".into(),
|
|
@@ -313,14 +342,13 @@ fn fire_with(
|
|
|
313
342
|
)),
|
|
314
343
|
}
|
|
315
344
|
})();
|
|
316
|
-
let (status, result, due) =
|
|
317
|
-
|
|
318
|
-
Err(error) => (
|
|
345
|
+
let (status, result, due) = result.unwrap_or_else(|error| {
|
|
346
|
+
(
|
|
319
347
|
"unknown".into(),
|
|
320
348
|
serde_json::json!({"error":error.to_string(),"retry":false}),
|
|
321
349
|
None,
|
|
322
|
-
)
|
|
323
|
-
};
|
|
350
|
+
)
|
|
351
|
+
});
|
|
324
352
|
conn.execute(
|
|
325
353
|
"update scheduled_events set status=?2,result_json=?3,due_at=coalesce(?4,due_at)
|
|
326
354
|
where id=?1 and kind='enter_rescue' and status='claimed'",
|