@team-agent/installer 0.5.114 → 0.5.116
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/emit.rs +2 -2
- package/crates/team-agent/src/cli/mod.rs +155 -19
- package/crates/team-agent/src/cli/models.rs +52 -1
- package/crates/team-agent/src/cli/shutdown_socket.rs +288 -0
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +11 -4
- package/crates/team-agent/src/contract_runtime/backend.rs +490 -97
- package/crates/team-agent/src/contract_runtime/commands.rs +326 -0
- package/crates/team-agent/src/contract_runtime/config.rs +39 -8
- package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +218 -40
- package/crates/team-agent/src/contract_runtime/framework.rs +38 -5
- package/crates/team-agent/src/contract_runtime/mcp.rs +3 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +2 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +97 -33
- package/crates/team-agent/src/contract_runtime/registry.rs +248 -4
- package/crates/team-agent/src/contract_runtime/workspace.rs +228 -0
- package/crates/team-agent/src/leader/start.rs +93 -5
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +99 -1
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +3 -5
- package/crates/team-agent/src/lifecycle/launch.rs +6 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +109 -58
- package/crates/team-agent/src/lifecycle/restart/selection.rs +63 -7
- package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
- package/crates/team-agent/src/provider/session/capture.rs +24 -1
- package/crates/team-agent/src/state/persist.rs +72 -2
- package/crates/team-agent/src/state/projection.rs +29 -0
- package/crates/team-agent/src/state/repository/tests.rs +70 -0
- package/crates/team-agent/src/state/repository.rs +5 -1
- package/package.json +4 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//! implicit business retry, queue flush, or replay of uncertain messages.
|
|
3
3
|
use super::{
|
|
4
4
|
backend::{self, Backend, BackendError},
|
|
5
|
-
forward, framework,
|
|
5
|
+
forward, framework, registry,
|
|
6
6
|
};
|
|
7
7
|
use serde_json::Value;
|
|
8
8
|
use std::path::Path;
|
|
@@ -12,11 +12,10 @@ use team_agent_contract::host::{
|
|
|
12
12
|
clock::{Clock, RealClock},
|
|
13
13
|
process::verify_native_ancestry,
|
|
14
14
|
};
|
|
15
|
-
use team_agent_contract::kiro::{interaction, KIRO_DESCRIPTOR};
|
|
16
15
|
use team_agent_contract::orchestration::{physical::*, store::*, supervisor::*};
|
|
17
16
|
use team_agent_contract::runtime::probes::*;
|
|
18
17
|
|
|
19
|
-
fn protocol(
|
|
18
|
+
pub(super) fn protocol(
|
|
20
19
|
backend: &mut Backend,
|
|
21
20
|
seat: &SeatRecord,
|
|
22
21
|
clock: &RealClock,
|
|
@@ -28,6 +27,9 @@ fn protocol(
|
|
|
28
27
|
.into_iter()
|
|
29
28
|
.filter(|record| {
|
|
30
29
|
!record.closed
|
|
30
|
+
&& record.identity == seat.identity
|
|
31
|
+
&& record.server_key == seat.server_key
|
|
32
|
+
&& record.binding_key == seat.binding_key
|
|
31
33
|
&& record.native_process == target.process
|
|
32
34
|
&& verify_native_ancestry(&record.process, &record.native_process).is_ok()
|
|
33
35
|
})
|
|
@@ -75,6 +77,35 @@ fn protocol(
|
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
}
|
|
80
|
+
if registry::provider(seat.provider.as_str())?.uses_tool_roundtrips() {
|
|
81
|
+
let session = seat.session.as_ref().ok_or(BackendError::Metadata)?;
|
|
82
|
+
if target.identity() != &seat.identity || target.binding != seat.binding_key
|
|
83
|
+
|| target.provider != seat.provider || target.native != seat.native
|
|
84
|
+
|| target.native_session.as_ref() != Some(&session.native_session)
|
|
85
|
+
|| session.source != seat.identity || session.provider != seat.provider
|
|
86
|
+
|| session.native != seat.native || session.cwd != target.cwd
|
|
87
|
+
|| target.candidate_sha256 != backend.binding.candidate_sha256
|
|
88
|
+
{
|
|
89
|
+
return Err(ContractError::Mismatch("native roundtrip current binding").into());
|
|
90
|
+
}
|
|
91
|
+
// The core returns only per-tool invocation + actual successful
|
|
92
|
+
// write/flush pairs for THIS captured connection and generation.
|
|
93
|
+
// Server list writes or another connection's facts cannot qualify.
|
|
94
|
+
let tools = backend.store.completed_connection_tools(&seat.identity, &connection.connection)?;
|
|
95
|
+
if !tools.is_empty() {
|
|
96
|
+
events.push(ProtocolEvent {
|
|
97
|
+
scope: target.scope(events.len() as u64, clock.now(), Duration::from_secs(2),
|
|
98
|
+
"live-native-tool-roundtrip-ledger"),
|
|
99
|
+
fact: ProtocolFact::ClientBound {
|
|
100
|
+
server_instance: connection.connection.clone(),
|
|
101
|
+
evidence: ClientBindingEvidence {
|
|
102
|
+
server_key: seat.server_key.clone(), tools,
|
|
103
|
+
source: ClientBindingSource::NativeToolRoundtrip,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
78
109
|
Ok(collect_protocol(
|
|
79
110
|
target,
|
|
80
111
|
&connection.connection,
|
|
@@ -91,8 +122,18 @@ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendEr
|
|
|
91
122
|
{
|
|
92
123
|
return Ok(());
|
|
93
124
|
}
|
|
125
|
+
let selected = registry::provider(seat.provider.as_str())?;
|
|
126
|
+
let bootstrap_allowed = selected.first_business_bootstrap() && !seat.bootstrap_used;
|
|
127
|
+
let profiles = selected.descriptor().input.profiles.require("native input profiles")?;
|
|
128
|
+
let profile = profiles.iter().find(|profile| profile.id == seat.profile && profile.matches_native(&seat.native))
|
|
129
|
+
.ok_or(ContractError::ProfileUnavailable)?;
|
|
130
|
+
profile.policy.require("native submit policy")?;
|
|
131
|
+
let role = backend::read_role(&backend.binding, &seat.identity)?;
|
|
132
|
+
if role.provider != seat.provider {
|
|
133
|
+
return Err(BackendError::Metadata);
|
|
134
|
+
}
|
|
94
135
|
let clock = RealClock::new();
|
|
95
|
-
let adapter = backend::adapter(&backend.binding, &seat.identity)?;
|
|
136
|
+
let adapter = backend::adapter(&backend.binding, &seat.identity, &seat.provider)?;
|
|
96
137
|
let hooks = adapter.hooks();
|
|
97
138
|
let operation = OperationId::new(format!(
|
|
98
139
|
"deliver-{}",
|
|
@@ -104,15 +145,19 @@ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendEr
|
|
|
104
145
|
))?;
|
|
105
146
|
let mut physical = PhysicalRuntime::new(
|
|
106
147
|
Registration {
|
|
107
|
-
descriptor:
|
|
148
|
+
descriptor: selected.descriptor(),
|
|
108
149
|
hooks: &hooks,
|
|
109
|
-
controls:
|
|
150
|
+
controls: selected.controls(),
|
|
151
|
+
startup: selected.startup(),
|
|
152
|
+
session_panel: selected.session_panel(),
|
|
110
153
|
},
|
|
111
|
-
backend::settings(&backend.binding),
|
|
154
|
+
backend::settings(&backend.binding, selected),
|
|
112
155
|
operation.clone(),
|
|
113
156
|
&clock,
|
|
114
157
|
)?;
|
|
115
|
-
physical.evidence = backend::policy_evidence(
|
|
158
|
+
physical.evidence = backend::policy_evidence(
|
|
159
|
+
&seat.provider, &seat.native, backend.binding.candidate_sha256,
|
|
160
|
+
)?;
|
|
116
161
|
physical.protocol = Some(protocol(backend, seat, &clock)?);
|
|
117
162
|
let sample = physical.readiness(seat)?;
|
|
118
163
|
if !matches!(sample.pane.outcome, ProbeOutcome::Observed(ref pane) if pane.surface == InputSurface::ComposerReady)
|
|
@@ -120,28 +165,39 @@ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendEr
|
|
|
120
165
|
{
|
|
121
166
|
return Ok(());
|
|
122
167
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
backend.store.fail_observation(&seat.identity, &operation)?;
|
|
133
|
-
return Err(error.into());
|
|
168
|
+
// A roundtrip provider consumes only its captured protocol evidence. An
|
|
169
|
+
// Unknown/partial binding is left to the typed readiness gate; it never
|
|
170
|
+
// triggers a guessed TUI /tools command or a Kiro inspector fallback.
|
|
171
|
+
if !bootstrap_allowed && !selected.uses_tool_roundtrips() {
|
|
172
|
+
let panel = selected.tool_panel()?;
|
|
173
|
+
if panel.profile_id != seat.profile || panel.server_key != seat.server_key
|
|
174
|
+
|| panel.policy_sha256 != profile.policy_sha256
|
|
175
|
+
{
|
|
176
|
+
return Err(ContractError::Mismatch("native inspector seat binding").into());
|
|
134
177
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
|
|
178
|
+
backend.store.begin_observation(&seat.identity, &operation)?;
|
|
179
|
+
// Uncertain inspection quarantines the seat without an orphan lease.
|
|
180
|
+
// No replay or blind Escape clears an unrelated composer/approval.
|
|
181
|
+
let binding = match physical.client_binding(seat, panel) {
|
|
182
|
+
Ok(binding) => binding,
|
|
183
|
+
Err(error) => {
|
|
184
|
+
backend.store.fail_observation(&seat.identity, &operation)?;
|
|
185
|
+
return Err(error.into());
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
backend.store.finish_observation(&seat.identity, &operation)?;
|
|
189
|
+
if !matches!(binding.outcome, ProbeOutcome::Observed(_)) {
|
|
190
|
+
return Ok(());
|
|
191
|
+
}
|
|
192
|
+
let mut current_protocol = protocol(backend, seat, &clock)?;
|
|
193
|
+
current_protocol.binding = binding;
|
|
194
|
+
physical.protocol = Some(current_protocol);
|
|
195
|
+
}
|
|
196
|
+
// H3's owned prompt receipt is independently checked by PhysicalRuntime;
|
|
197
|
+
// only the one durable first-business claim can carry this instruction.
|
|
198
|
+
if bootstrap_allowed {
|
|
199
|
+
physical.initial_prompt = Some(role.prompt.text.clone());
|
|
141
200
|
}
|
|
142
|
-
let mut current_protocol = protocol(backend, seat, &clock)?;
|
|
143
|
-
current_protocol.binding = binding;
|
|
144
|
-
physical.protocol = Some(current_protocol);
|
|
145
201
|
let mut bootstrap = OutboxBootstrap::new(ContractStore::open(
|
|
146
202
|
&backend.binding.transport_root,
|
|
147
203
|
backend.binding.scope.clone(),
|
|
@@ -150,10 +206,10 @@ fn deliver_one(backend: &mut Backend, seat: &SeatRecord) -> Result<(), BackendEr
|
|
|
150
206
|
physical.bootstrap = Some(&mut bootstrap);
|
|
151
207
|
let sample = physical.readiness(seat)?;
|
|
152
208
|
let authorization = DeliveryAuthorization {
|
|
153
|
-
allow_first_business_bootstrap:
|
|
209
|
+
allow_first_business_bootstrap: bootstrap_allowed,
|
|
154
210
|
require_server_handshake: true,
|
|
155
211
|
channel: Channel::Tmux,
|
|
156
|
-
policy_sha256:
|
|
212
|
+
policy_sha256: profile.policy_sha256,
|
|
157
213
|
};
|
|
158
214
|
let _ = team_agent_contract::orchestration::supervisor::tick(
|
|
159
215
|
&mut backend.store,
|
|
@@ -170,15 +226,18 @@ pub fn tick(workspace: &Path, state: &mut Value) -> Result<(), BackendError> {
|
|
|
170
226
|
return Ok(());
|
|
171
227
|
}
|
|
172
228
|
let team = crate::state::projection::team_state_key(state);
|
|
173
|
-
let mut backend = Backend::open(workspace, &team)?;
|
|
174
229
|
let ids: Vec<_> = state
|
|
175
230
|
.get("agents")
|
|
176
231
|
.and_then(Value::as_object)
|
|
177
232
|
.into_iter()
|
|
178
233
|
.flat_map(|agents| agents.iter())
|
|
179
|
-
.filter(|(_, agent)| framework::is_contract(agent))
|
|
234
|
+
.filter(|(_, agent)| framework::is_contract(agent) && !framework::is_paused_unstarted(agent))
|
|
180
235
|
.map(|(id, _)| id.clone())
|
|
181
236
|
.collect();
|
|
237
|
+
if ids.is_empty() {
|
|
238
|
+
return Ok(());
|
|
239
|
+
}
|
|
240
|
+
let mut backend = Backend::open(workspace, &team)?;
|
|
182
241
|
let mut failure = None;
|
|
183
242
|
for id in ids {
|
|
184
243
|
let Some(seat) = backend.store.seat(&SeatId::new(&id)?)? else {
|
|
@@ -192,7 +251,12 @@ pub fn tick(workspace: &Path, state: &mut Value) -> Result<(), BackendError> {
|
|
|
192
251
|
.and_then(|agents| agents.get_mut(&id))
|
|
193
252
|
{
|
|
194
253
|
match framework::project_seat(workspace, &team, &id) {
|
|
195
|
-
Ok(projected) =>
|
|
254
|
+
Ok(projected) => {
|
|
255
|
+
let row = agent.as_object_mut().ok_or(BackendError::Metadata)?;
|
|
256
|
+
for (key, value) in projected.as_object().ok_or(BackendError::Metadata)? {
|
|
257
|
+
row.insert(key.clone(), value.clone());
|
|
258
|
+
}
|
|
259
|
+
},
|
|
196
260
|
Err(error) => failure = Some(error),
|
|
197
261
|
}
|
|
198
262
|
}
|
|
@@ -1,9 +1,195 @@
|
|
|
1
|
-
//! The sole new-runtime provider
|
|
1
|
+
//! The sole new-runtime provider registry. This is not an admission claim:
|
|
2
2
|
//! the descriptor's Unverified gates still apply to each requested operation.
|
|
3
|
-
use
|
|
4
|
-
use
|
|
3
|
+
use std::collections::BTreeMap;
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
use team_agent_contract::contract::{
|
|
6
|
+
descriptor::{PromptCarrier, ProviderDescriptor, ToolNaming},
|
|
7
|
+
types::InstanceIdentity,
|
|
8
|
+
hooks::{PlanHook, ProviderHooks},
|
|
9
|
+
plan::{LaunchPlan, ResolvedLaunch},
|
|
10
|
+
types::{ContractError, Support},
|
|
11
|
+
};
|
|
12
|
+
use team_agent_contract::kiro::{self, KiroAdapter, KIRO_DESCRIPTOR};
|
|
13
|
+
use team_agent_contract::providers::cursor::{self, CursorAdapter, CURSOR_DESCRIPTOR};
|
|
14
|
+
use team_agent_contract::providers::grok_build::{self, GrokAdapter, GROK_BUILD_DESCRIPTOR};
|
|
15
|
+
use team_agent_contract::runtime::{
|
|
16
|
+
delivery::{ControlDefinition, StartupDefinition},
|
|
17
|
+
native_panel::{NativePanelPolicy, SessionPanelPolicy},
|
|
18
|
+
};
|
|
5
19
|
|
|
6
|
-
const DESCRIPTORS: &[&ProviderDescriptor] = &[&KIRO_DESCRIPTOR];
|
|
20
|
+
const DESCRIPTORS: &[&ProviderDescriptor] = &[&KIRO_DESCRIPTOR, &CURSOR_DESCRIPTOR, &GROK_BUILD_DESCRIPTOR];
|
|
21
|
+
|
|
22
|
+
#[derive(Clone)]
|
|
23
|
+
pub enum NativeRuntime {
|
|
24
|
+
Cursor(cursor::native::RuntimeInstallation),
|
|
25
|
+
GrokBuild(grok_build::native::RuntimeInstallation),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Leaf selection stays here; the framework, lifecycle and outbox remain shared.
|
|
29
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
30
|
+
pub(super) enum NativeProvider {
|
|
31
|
+
Kiro,
|
|
32
|
+
Cursor,
|
|
33
|
+
GrokBuild,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl NativeProvider {
|
|
37
|
+
pub fn descriptor(self) -> &'static ProviderDescriptor {
|
|
38
|
+
match self {
|
|
39
|
+
Self::Kiro => &KIRO_DESCRIPTOR,
|
|
40
|
+
Self::Cursor => &CURSOR_DESCRIPTOR,
|
|
41
|
+
Self::GrokBuild => &GROK_BUILD_DESCRIPTOR,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub fn action_budget(self) -> std::time::Duration {
|
|
46
|
+
// Kiro's existing budget is unchanged; Cursor's measured cold-ready
|
|
47
|
+
// path needs a larger finite bound, not an unbounded readiness wait.
|
|
48
|
+
std::time::Duration::from_secs(match self { Self::Kiro => 30, Self::Cursor | Self::GrokBuild => 90 })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn controls(self) -> &'static [ControlDefinition] {
|
|
52
|
+
match self {
|
|
53
|
+
Self::Kiro => kiro::interaction::CONTROLS,
|
|
54
|
+
Self::Cursor => cursor::interaction::CONTROLS,
|
|
55
|
+
Self::GrokBuild => grok_build::interaction::CONTROLS,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// A terminal instruction bridge needs one durable, authorized first packet
|
|
60
|
+
/// before the native client can demonstrate its Team-tool registry.
|
|
61
|
+
pub fn first_business_bootstrap(self) -> bool {
|
|
62
|
+
matches!(self.descriptor().prompt.carrier, Support::Supported(PromptCarrier::RuntimeBridge))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// These providers use the separately fenced native tool-roundtrip ledger,
|
|
66
|
+
/// never a guessed neutral tools panel or another provider's inspector.
|
|
67
|
+
pub fn uses_tool_roundtrips(self) -> bool {
|
|
68
|
+
matches!(self, Self::Cursor | Self::GrokBuild)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn startup(self) -> Option<&'static StartupDefinition> {
|
|
72
|
+
match self {
|
|
73
|
+
Self::GrokBuild => Some(&grok_build::interaction::STARTUP),
|
|
74
|
+
Self::Kiro | Self::Cursor => None,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub fn session_panel(self) -> Option<&'static SessionPanelPolicy> {
|
|
79
|
+
match self {
|
|
80
|
+
Self::GrokBuild => Some(&grok_build::interaction::SESSION_PANEL),
|
|
81
|
+
Self::Kiro | Self::Cursor => None,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub fn tool_panel(self) -> Result<&'static NativePanelPolicy, ContractError> {
|
|
86
|
+
match self {
|
|
87
|
+
Self::Kiro => Ok(&kiro::interaction::TOOL_PANEL),
|
|
88
|
+
Self::Cursor => Err(ContractError::Unverified {
|
|
89
|
+
field: "Cursor native Team-tool inspector",
|
|
90
|
+
reason: cursor::UNVERIFIED,
|
|
91
|
+
}),
|
|
92
|
+
Self::GrokBuild => Err(ContractError::Unverified {
|
|
93
|
+
field: "Grok Build native Team-tool inspector",
|
|
94
|
+
reason: grok_build::UNVERIFIED,
|
|
95
|
+
}),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pub fn server_key_for(self, identity: &InstanceIdentity) -> Result<String, ContractError> {
|
|
100
|
+
if self == Self::GrokBuild {
|
|
101
|
+
return Ok(grok_build::server_key(identity));
|
|
102
|
+
}
|
|
103
|
+
Ok(self.server_key()?.into())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub fn server_key(self) -> Result<&'static str, ContractError> {
|
|
107
|
+
match self.descriptor().tool_names.naming.require("tool naming")? {
|
|
108
|
+
ToolNaming::StaticServer { key } => Ok(key),
|
|
109
|
+
ToolNaming::RuntimeBound => Err(ContractError::Invalid("runtime server-key receipt required")),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pub fn adapter(
|
|
114
|
+
self,
|
|
115
|
+
executable: PathBuf,
|
|
116
|
+
arguments: Vec<String>,
|
|
117
|
+
environment: BTreeMap<String, String>,
|
|
118
|
+
) -> Result<NativeAdapter, ContractError> {
|
|
119
|
+
match self {
|
|
120
|
+
Self::Kiro => KiroAdapter::new(kiro::McpStdio {
|
|
121
|
+
executable, arguments, environment,
|
|
122
|
+
}).map(NativeAdapter::Kiro),
|
|
123
|
+
Self::Cursor => CursorAdapter::new(cursor::McpStdio {
|
|
124
|
+
executable, arguments, environment,
|
|
125
|
+
}).map(NativeAdapter::Cursor),
|
|
126
|
+
Self::GrokBuild => GrokAdapter::new(grok_build::McpStdio {
|
|
127
|
+
executable, arguments, environment,
|
|
128
|
+
}).map(NativeAdapter::GrokBuild),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pub(super) enum NativeAdapter {
|
|
134
|
+
Kiro(KiroAdapter),
|
|
135
|
+
Cursor(CursorAdapter),
|
|
136
|
+
GrokBuild(GrokAdapter),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
impl NativeAdapter {
|
|
140
|
+
pub fn with_launch_context(
|
|
141
|
+
self,
|
|
142
|
+
project: PathBuf,
|
|
143
|
+
runtime: Option<NativeRuntime>,
|
|
144
|
+
) -> Result<Self, ContractError> {
|
|
145
|
+
match self {
|
|
146
|
+
Self::Kiro(adapter) if runtime.is_none() => Ok(Self::Kiro(adapter)),
|
|
147
|
+
Self::Kiro(_) => Err(ContractError::Mismatch("native runtime provider")),
|
|
148
|
+
Self::Cursor(adapter) => {
|
|
149
|
+
let adapter = adapter.with_project(project)?;
|
|
150
|
+
Ok(Self::Cursor(match runtime {
|
|
151
|
+
Some(NativeRuntime::Cursor(runtime)) => adapter.with_runtime(runtime),
|
|
152
|
+
Some(_) => return Err(ContractError::Mismatch("native runtime provider")),
|
|
153
|
+
None => adapter,
|
|
154
|
+
}))
|
|
155
|
+
}
|
|
156
|
+
Self::GrokBuild(adapter) => {
|
|
157
|
+
let adapter = adapter.with_project(project)?;
|
|
158
|
+
Ok(Self::GrokBuild(match runtime {
|
|
159
|
+
Some(NativeRuntime::GrokBuild(runtime)) => adapter.with_runtime(runtime),
|
|
160
|
+
Some(_) => return Err(ContractError::Mismatch("native runtime provider")),
|
|
161
|
+
None => adapter,
|
|
162
|
+
}))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pub fn hooks(&self) -> ProviderHooks<'_> {
|
|
168
|
+
match self {
|
|
169
|
+
Self::Kiro(adapter) => adapter.hooks(),
|
|
170
|
+
Self::Cursor(adapter) => adapter.hooks(),
|
|
171
|
+
Self::GrokBuild(adapter) => adapter.hooks(),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub fn plan(&self, request: &ResolvedLaunch) -> Result<LaunchPlan, ContractError> {
|
|
176
|
+
match self {
|
|
177
|
+
Self::Kiro(adapter) => adapter.plan(request),
|
|
178
|
+
Self::Cursor(adapter) => adapter.plan(request),
|
|
179
|
+
Self::GrokBuild(adapter) => adapter.plan(request),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
pub(super) fn provider(key: &str) -> Result<NativeProvider, ContractError> {
|
|
185
|
+
let descriptor = descriptor(key).ok_or(ContractError::Invalid("unregistered contract provider"))?;
|
|
186
|
+
match descriptor.identity.id {
|
|
187
|
+
"kiro" => Ok(NativeProvider::Kiro),
|
|
188
|
+
"cursor" => Ok(NativeProvider::Cursor),
|
|
189
|
+
"grok_build" => Ok(NativeProvider::GrokBuild),
|
|
190
|
+
_ => Err(ContractError::Invalid("contract provider dispatcher")),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
7
193
|
|
|
8
194
|
pub(crate) fn descriptors() -> &'static [&'static ProviderDescriptor] {
|
|
9
195
|
DESCRIPTORS
|
|
@@ -28,3 +214,61 @@ pub fn recognizes(key: &str) -> bool {
|
|
|
28
214
|
.any(|alias| alias.eq_ignore_ascii_case(key))
|
|
29
215
|
})
|
|
30
216
|
}
|
|
217
|
+
|
|
218
|
+
#[cfg(test)]
|
|
219
|
+
mod tests {
|
|
220
|
+
use super::*;
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn canonical_cursor_does_not_reclassify_legacy_names() {
|
|
224
|
+
assert_eq!(provider("cursor").unwrap(), NativeProvider::Cursor);
|
|
225
|
+
assert_eq!(provider("kiro").unwrap(), NativeProvider::Kiro);
|
|
226
|
+
for legacy in ["cursor_agent", "agent", "pi", "codex", "grok"] {
|
|
227
|
+
assert!(descriptor(legacy).is_none());
|
|
228
|
+
assert!(!recognizes(legacy));
|
|
229
|
+
assert!(provider(legacy).is_err());
|
|
230
|
+
}
|
|
231
|
+
assert_eq!(provider("grok_build").unwrap(), NativeProvider::GrokBuild);
|
|
232
|
+
assert!(recognizes(" Grok_Build "));
|
|
233
|
+
assert!(descriptor("Grok_Build").is_none());
|
|
234
|
+
assert!(GROK_BUILD_DESCRIPTOR.identity.aliases.is_empty());
|
|
235
|
+
assert!(recognizes(" Cursor "));
|
|
236
|
+
assert!(descriptor("Cursor").is_none());
|
|
237
|
+
for descriptor in descriptors() {
|
|
238
|
+
assert_eq!(provider(descriptor.identity.id).unwrap().descriptor(), *descriptor);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn grok_server_key_is_generation_bound_and_has_no_inspector_fallback() {
|
|
244
|
+
use team_agent_contract::contract::types::{Generation, InstanceId, ScopeId, SeatId};
|
|
245
|
+
let mut identity = InstanceIdentity {
|
|
246
|
+
scope: ScopeId::new("scope").unwrap(), seat: SeatId::new("worker").unwrap(),
|
|
247
|
+
instance: InstanceId::new("instance").unwrap(), generation: Generation(1),
|
|
248
|
+
};
|
|
249
|
+
let first = NativeProvider::GrokBuild.server_key_for(&identity).unwrap();
|
|
250
|
+
assert_eq!(first, grok_build::server_key(&identity));
|
|
251
|
+
identity.generation = Generation(2);
|
|
252
|
+
assert_ne!(first, NativeProvider::GrokBuild.server_key_for(&identity).unwrap());
|
|
253
|
+
assert!(NativeProvider::GrokBuild.server_key().is_err());
|
|
254
|
+
assert!(NativeProvider::GrokBuild.tool_panel().is_err());
|
|
255
|
+
assert!(NativeProvider::GrokBuild.startup().is_some());
|
|
256
|
+
assert!(NativeProvider::GrokBuild.session_panel().is_some());
|
|
257
|
+
assert!(NativeProvider::GrokBuild.uses_tool_roundtrips());
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn cursor_has_no_kiro_control_or_tool_inspector_fallback() {
|
|
262
|
+
assert!(NativeProvider::Cursor.controls().is_empty());
|
|
263
|
+
assert!(matches!(NativeProvider::Cursor.tool_panel(), Err(ContractError::Unverified { .. })));
|
|
264
|
+
assert_eq!(NativeProvider::Cursor.server_key().unwrap(), "team");
|
|
265
|
+
assert!(NativeProvider::Cursor.first_business_bootstrap());
|
|
266
|
+
assert!(NativeProvider::Cursor.uses_tool_roundtrips());
|
|
267
|
+
assert!(!NativeProvider::Kiro.first_business_bootstrap());
|
|
268
|
+
assert!(!NativeProvider::Kiro.uses_tool_roundtrips());
|
|
269
|
+
let kiro_panel = NativeProvider::Kiro.tool_panel().unwrap();
|
|
270
|
+
assert_eq!(kiro_panel.server_key, NativeProvider::Kiro.server_key().unwrap());
|
|
271
|
+
assert_eq!(kiro_panel.policy_sha256, kiro::interaction::POLICY);
|
|
272
|
+
assert!(!NativeProvider::Kiro.controls().is_empty());
|
|
273
|
+
}
|
|
274
|
+
}
|