@team-agent/installer 0.5.113 → 0.5.114
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +55 -20
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +152 -8
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +260 -16
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
//! Read-only per-agent routing/preflight. Contract roles opt in independently;
|
|
2
|
+
//! other roles keep the legacy engine, including in the same logical team.
|
|
3
|
+
use std::collections::BTreeSet;
|
|
4
|
+
use std::path::{Path, PathBuf};
|
|
5
|
+
|
|
6
|
+
use serde::{Deserialize, Serialize};
|
|
7
|
+
use team_agent_contract::contract::{
|
|
8
|
+
session::CwdIdentity,
|
|
9
|
+
types::{ProviderId, ScopeId, SeatId},
|
|
10
|
+
};
|
|
11
|
+
use team_agent_contract::host::{digest, digest_hex, process::resolve_cwd};
|
|
12
|
+
use thiserror::Error;
|
|
13
|
+
|
|
14
|
+
use super::prompt::{self, Prompt, Source};
|
|
15
|
+
use crate::cli::QuickStartArgs;
|
|
16
|
+
use crate::communication_mode::CommunicationMode;
|
|
17
|
+
use crate::compiler::split_front_matter;
|
|
18
|
+
use crate::model::yaml::Value;
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Error)]
|
|
21
|
+
pub enum ConfigError {
|
|
22
|
+
#[error("invalid contract configuration field {field} in {path:?}")]
|
|
23
|
+
Invalid { path: PathBuf, field: &'static str },
|
|
24
|
+
#[error("contract runtime capability is not available: {0}")]
|
|
25
|
+
Unsupported(&'static str),
|
|
26
|
+
#[error("contract configuration I/O: {0}")]
|
|
27
|
+
Io(#[from] std::io::Error),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Prompts intentionally have no Debug implementation and are never diagnostics.
|
|
31
|
+
#[derive(Clone, Serialize, Deserialize)]
|
|
32
|
+
pub struct RoleConfig {
|
|
33
|
+
pub id: SeatId,
|
|
34
|
+
pub provider: ProviderId,
|
|
35
|
+
pub source: PathBuf,
|
|
36
|
+
pub role: String,
|
|
37
|
+
pub model: String,
|
|
38
|
+
pub effort: Option<String>,
|
|
39
|
+
pub bypass: bool,
|
|
40
|
+
pub body: String,
|
|
41
|
+
pub prompt: Prompt,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
45
|
+
pub enum RuntimeFamily {
|
|
46
|
+
Legacy,
|
|
47
|
+
Contract,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
51
|
+
pub struct MemberRoute {
|
|
52
|
+
pub id: String,
|
|
53
|
+
pub provider: String,
|
|
54
|
+
pub source: PathBuf,
|
|
55
|
+
pub runtime: RuntimeFamily,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[derive(Clone, Serialize, Deserialize)]
|
|
59
|
+
pub struct TeamConfig {
|
|
60
|
+
pub selector: String,
|
|
61
|
+
pub label: String,
|
|
62
|
+
pub scope: ScopeId,
|
|
63
|
+
pub team_dir: PathBuf,
|
|
64
|
+
pub workspace: CwdIdentity,
|
|
65
|
+
pub objective: String,
|
|
66
|
+
pub members: Vec<MemberRoute>,
|
|
67
|
+
/// Only contract roles; the legacy member definitions are not rewritten.
|
|
68
|
+
pub roles: Vec<RoleConfig>,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn invalid(path: &Path, field: &'static str) -> ConfigError {
|
|
72
|
+
ConfigError::Invalid {
|
|
73
|
+
path: path.into(),
|
|
74
|
+
field,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
fn text(meta: &Value, field: &'static str, path: &Path) -> Result<Option<String>, ConfigError> {
|
|
78
|
+
match meta.get(field) {
|
|
79
|
+
None | Some(Value::Null) => Ok(None),
|
|
80
|
+
Some(Value::Str(value)) if !value.trim().is_empty() && !value.contains('\0') => {
|
|
81
|
+
Ok(Some(value.clone()))
|
|
82
|
+
}
|
|
83
|
+
_ => Err(invalid(path, field)),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
fn required(meta: &Value, field: &'static str, path: &Path) -> Result<String, ConfigError> {
|
|
87
|
+
text(meta, field, path)?.ok_or_else(|| invalid(path, field))
|
|
88
|
+
}
|
|
89
|
+
fn mode(meta: &Value, path: &Path) -> Result<Option<CommunicationMode>, ConfigError> {
|
|
90
|
+
text(meta, "communication_mode", path)?
|
|
91
|
+
.map(|value| {
|
|
92
|
+
CommunicationMode::parse(&value).ok_or_else(|| invalid(path, "communication_mode"))
|
|
93
|
+
})
|
|
94
|
+
.transpose()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// A malformed explicit Kiro header cannot fall back to the default Pi worker.
|
|
98
|
+
fn malformed_kiro_header(body: &str) -> bool {
|
|
99
|
+
body.strip_prefix("---\n").is_some_and(|header| {
|
|
100
|
+
header
|
|
101
|
+
.lines()
|
|
102
|
+
.take_while(|line| *line != "---")
|
|
103
|
+
.any(|line| {
|
|
104
|
+
line.split_once(':').is_some_and(|(key, value)| {
|
|
105
|
+
key.trim() == "provider"
|
|
106
|
+
&& super::registry::recognizes(
|
|
107
|
+
value
|
|
108
|
+
.split('#')
|
|
109
|
+
.next()
|
|
110
|
+
.unwrap_or("")
|
|
111
|
+
.trim()
|
|
112
|
+
.trim_matches(['\'', '"']),
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// None delegates exactly to the existing no-Kiro/leader-only path. No native
|
|
120
|
+
/// command, model probe, config write or process is created by this selection.
|
|
121
|
+
pub fn read_team(args: &QuickStartArgs) -> Result<Option<TeamConfig>, ConfigError> {
|
|
122
|
+
read_team_with_user_source(args, prompt::user_source)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// A unified framework user-policy source can be injected without making the
|
|
126
|
+
/// provider know its storage location. Tests use this port instead of real HOME.
|
|
127
|
+
pub fn read_team_with_user_source(
|
|
128
|
+
args: &QuickStartArgs,
|
|
129
|
+
user_source: impl FnOnce() -> Result<Option<Source>, ConfigError>,
|
|
130
|
+
) -> Result<Option<TeamConfig>, ConfigError> {
|
|
131
|
+
let agents = args.agents_dir.join("agents");
|
|
132
|
+
let entries = match std::fs::read_dir(&agents) {
|
|
133
|
+
Ok(entries) => entries,
|
|
134
|
+
Err(_) => return Ok(None),
|
|
135
|
+
};
|
|
136
|
+
let mut paths = Vec::new();
|
|
137
|
+
for entry in entries {
|
|
138
|
+
let entry = entry?;
|
|
139
|
+
let path = entry.path();
|
|
140
|
+
if path.extension().and_then(|s| s.to_str()) == Some("md") {
|
|
141
|
+
paths.push(path);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
paths.sort();
|
|
145
|
+
let mut roles = Vec::new();
|
|
146
|
+
let mut selected = false;
|
|
147
|
+
for path in paths {
|
|
148
|
+
let raw = match std::fs::read_to_string(&path) {
|
|
149
|
+
Ok(raw) => raw,
|
|
150
|
+
Err(error) if selected => return Err(error.into()),
|
|
151
|
+
Err(_) => return Ok(None), // let the legacy compiler report its existing I/O error
|
|
152
|
+
};
|
|
153
|
+
let (meta, body) = split_front_matter(&raw);
|
|
154
|
+
let normalized = raw.replace("\r\n", "\n").replace('\r', "\n");
|
|
155
|
+
if meta.get("provider").is_none() && malformed_kiro_header(&normalized) {
|
|
156
|
+
return Err(invalid(&path, "front matter"));
|
|
157
|
+
}
|
|
158
|
+
if meta
|
|
159
|
+
.get("provider")
|
|
160
|
+
.and_then(Value::as_str)
|
|
161
|
+
.is_some_and(super::registry::recognizes)
|
|
162
|
+
{
|
|
163
|
+
selected = true;
|
|
164
|
+
}
|
|
165
|
+
roles.push((path, raw, meta, body));
|
|
166
|
+
}
|
|
167
|
+
if !selected {
|
|
168
|
+
return Ok(None);
|
|
169
|
+
}
|
|
170
|
+
if args
|
|
171
|
+
.backend
|
|
172
|
+
.as_deref()
|
|
173
|
+
.is_some_and(|backend| backend != "tmux")
|
|
174
|
+
{
|
|
175
|
+
return Err(ConfigError::Unsupported(
|
|
176
|
+
"Kiro contract transport requires tmux",
|
|
177
|
+
));
|
|
178
|
+
}
|
|
179
|
+
let team_dir = args.agents_dir.canonicalize()?;
|
|
180
|
+
let team_path = team_dir.join("TEAM.md");
|
|
181
|
+
let team_source = Source::read(&team_path)?;
|
|
182
|
+
let (team_meta, team_body) = split_front_matter(&team_source.contents);
|
|
183
|
+
let label = args
|
|
184
|
+
.name
|
|
185
|
+
.clone()
|
|
186
|
+
.or(text(&team_meta, "name", &team_path)?)
|
|
187
|
+
.or_else(|| {
|
|
188
|
+
team_dir
|
|
189
|
+
.file_name()
|
|
190
|
+
.and_then(|s| s.to_str())
|
|
191
|
+
.map(str::to_owned)
|
|
192
|
+
})
|
|
193
|
+
.ok_or_else(|| invalid(&team_path, "name"))?;
|
|
194
|
+
let selector = args.team_id.clone().unwrap_or_else(|| label.clone());
|
|
195
|
+
if selector.trim().is_empty() || selector.chars().any(char::is_control) {
|
|
196
|
+
return Err(invalid(&team_path, "team selector"));
|
|
197
|
+
}
|
|
198
|
+
let workspace =
|
|
199
|
+
resolve_cwd(&args.workspace).map_err(|_| invalid(&args.workspace, "workspace identity"))?;
|
|
200
|
+
let key =
|
|
201
|
+
serde_json::to_vec(&(&workspace, &selector)).map_err(|_| invalid(&team_path, "scope"))?;
|
|
202
|
+
let scope = ScopeId::new(format!("contract-{}", &digest_hex(digest(&key))[..32]))
|
|
203
|
+
.map_err(|_| invalid(&team_path, "scope"))?;
|
|
204
|
+
let objective = text(&team_meta, "objective", &team_path)?.unwrap_or(team_body);
|
|
205
|
+
let user = user_source()?;
|
|
206
|
+
let project = prompt::project_source(&workspace.path)?;
|
|
207
|
+
let team_instructions = prompt::team_sources(&team_source, &team_meta)?;
|
|
208
|
+
let team_mode = mode(&team_meta, &team_path)?.unwrap_or_default();
|
|
209
|
+
let mut ids = BTreeSet::new();
|
|
210
|
+
let mut compiled = Vec::new();
|
|
211
|
+
let mut members = Vec::new();
|
|
212
|
+
for (source, raw, meta, body) in roles {
|
|
213
|
+
let id = text(&meta, "agent_id", &source)?
|
|
214
|
+
.or(text(&meta, "name", &source)?)
|
|
215
|
+
.or_else(|| {
|
|
216
|
+
source
|
|
217
|
+
.file_stem()
|
|
218
|
+
.and_then(|s| s.to_str())
|
|
219
|
+
.map(str::to_owned)
|
|
220
|
+
})
|
|
221
|
+
.ok_or_else(|| invalid(&source, "agent id"))?;
|
|
222
|
+
if !ids.insert(id.clone()) {
|
|
223
|
+
return Err(invalid(&source, "duplicate agent id"));
|
|
224
|
+
}
|
|
225
|
+
let provider = text(&meta, "provider", &source)?.unwrap_or_else(|| "pi".into());
|
|
226
|
+
let registration = super::registry::descriptor(&provider);
|
|
227
|
+
if registration.is_none() && super::registry::recognizes(&provider) {
|
|
228
|
+
return Err(invalid(&source, "provider"));
|
|
229
|
+
}
|
|
230
|
+
let runtime = if registration.is_some() {
|
|
231
|
+
RuntimeFamily::Contract
|
|
232
|
+
} else {
|
|
233
|
+
RuntimeFamily::Legacy
|
|
234
|
+
};
|
|
235
|
+
members.push(MemberRoute {
|
|
236
|
+
id: id.clone(),
|
|
237
|
+
provider,
|
|
238
|
+
source: source.canonicalize()?,
|
|
239
|
+
runtime,
|
|
240
|
+
});
|
|
241
|
+
let Some(registration) = registration else {
|
|
242
|
+
continue;
|
|
243
|
+
};
|
|
244
|
+
let provider =
|
|
245
|
+
ProviderId::new(registration.identity.id).map_err(|_| invalid(&source, "provider"))?;
|
|
246
|
+
let id = SeatId::new(id).map_err(|_| invalid(&source, "agent id"))?;
|
|
247
|
+
if id.as_str() == "leader" {
|
|
248
|
+
return Err(invalid(&source, "reserved worker id"));
|
|
249
|
+
}
|
|
250
|
+
if text(&meta, "auth_mode", &source)?.is_some_and(|value| value != "subscription")
|
|
251
|
+
|| text(&meta, "profile", &source)?.is_some()
|
|
252
|
+
{
|
|
253
|
+
return Err(ConfigError::Unsupported(
|
|
254
|
+
"Kiro uses an existing native subscription; API/profile mappings are unavailable",
|
|
255
|
+
));
|
|
256
|
+
}
|
|
257
|
+
let bypass = match meta.get("dangerously_skip_permissions") {
|
|
258
|
+
None => false,
|
|
259
|
+
Some(Value::Bool(value)) => *value,
|
|
260
|
+
_ => return Err(invalid(&source, "dangerously_skip_permissions")),
|
|
261
|
+
};
|
|
262
|
+
if body.trim().is_empty() || body.contains('\0') {
|
|
263
|
+
return Err(invalid(&source, "role body"));
|
|
264
|
+
}
|
|
265
|
+
if ["args", "cli_args"]
|
|
266
|
+
.iter()
|
|
267
|
+
.any(|field| meta.get(field).is_some())
|
|
268
|
+
{
|
|
269
|
+
return Err(ConfigError::Unsupported(
|
|
270
|
+
"unregistered native arguments cannot override the managed launch plan",
|
|
271
|
+
));
|
|
272
|
+
}
|
|
273
|
+
let role = text(&meta, "role", &source)?.unwrap_or_else(|| id.as_str().into());
|
|
274
|
+
let document = Source {
|
|
275
|
+
path: source.canonicalize()?,
|
|
276
|
+
contents: raw,
|
|
277
|
+
};
|
|
278
|
+
let prompt = prompt::assemble(
|
|
279
|
+
id.as_str(),
|
|
280
|
+
&role,
|
|
281
|
+
mode(&meta, &source)?.unwrap_or(team_mode),
|
|
282
|
+
user.as_ref(),
|
|
283
|
+
project.as_ref(),
|
|
284
|
+
&document,
|
|
285
|
+
&team_instructions,
|
|
286
|
+
)?;
|
|
287
|
+
compiled.push(RoleConfig {
|
|
288
|
+
id,
|
|
289
|
+
provider,
|
|
290
|
+
source: document.path,
|
|
291
|
+
role,
|
|
292
|
+
model: required(&meta, "model", &source)?,
|
|
293
|
+
effort: text(&meta, "effort", &source)?,
|
|
294
|
+
bypass,
|
|
295
|
+
body,
|
|
296
|
+
prompt,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
Ok(Some(TeamConfig {
|
|
300
|
+
selector,
|
|
301
|
+
label,
|
|
302
|
+
scope,
|
|
303
|
+
team_dir,
|
|
304
|
+
workspace,
|
|
305
|
+
objective,
|
|
306
|
+
members,
|
|
307
|
+
roles: compiled,
|
|
308
|
+
}))
|
|
309
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
//! Bounded native discovery. Nothing here creates a runtime or claims T2/T3.
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
|
|
4
|
+
use team_agent_contract::contract::{hooks::*, plan::CatalogObservation, types::*};
|
|
5
|
+
use team_agent_contract::host::{command::RealCommandRunner, process::resolve_cwd};
|
|
6
|
+
use team_agent_contract::kiro::{native::*, KiroAdapter, McpStdio, KIRO_DESCRIPTOR};
|
|
7
|
+
use thiserror::Error;
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Error)]
|
|
10
|
+
pub enum DiscoveryError {
|
|
11
|
+
#[error("required executable is unavailable: {0}")]
|
|
12
|
+
Executable(&'static str),
|
|
13
|
+
#[error("native discovery failed: {0}")]
|
|
14
|
+
Host(#[from] team_agent_contract::host::HostError),
|
|
15
|
+
#[error("native engine probe failed: {0}")]
|
|
16
|
+
Probe(#[from] EngineProbeError),
|
|
17
|
+
#[error("native discovery contract: {0:?}")]
|
|
18
|
+
Contract(ContractError),
|
|
19
|
+
#[error("native model discovery: {0:?}")]
|
|
20
|
+
Catalog(ReadFailure),
|
|
21
|
+
#[error("native discovery I/O: {0}")]
|
|
22
|
+
Io(#[from] std::io::Error),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub struct Discovery {
|
|
26
|
+
pub engine: PathBuf,
|
|
27
|
+
pub native: NativeIdentity,
|
|
28
|
+
pub catalog: CatalogObservation,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub fn executable(name: &'static str) -> Result<PathBuf, DiscoveryError> {
|
|
32
|
+
let path = std::env::var_os("PATH").ok_or(DiscoveryError::Executable(name))?;
|
|
33
|
+
for directory in std::env::split_paths(&path) {
|
|
34
|
+
let candidate = directory.join(name);
|
|
35
|
+
let Ok(metadata) = std::fs::metadata(&candidate) else {
|
|
36
|
+
continue;
|
|
37
|
+
};
|
|
38
|
+
use std::os::unix::fs::PermissionsExt;
|
|
39
|
+
if metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 {
|
|
40
|
+
return Ok(candidate.canonicalize()?);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
Err(DiscoveryError::Executable(name))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub fn discover(workspace: &Path) -> Result<Discovery, DiscoveryError> {
|
|
47
|
+
let home = std::env::var_os("HOME")
|
|
48
|
+
.map(PathBuf::from)
|
|
49
|
+
.ok_or(DiscoveryError::Executable(
|
|
50
|
+
"HOME for native installation lookup",
|
|
51
|
+
))?;
|
|
52
|
+
let engine = resolve_helper(&executable(KIRO_DESCRIPTOR.identity.binary)?, &home)?;
|
|
53
|
+
let native = probe_engine(
|
|
54
|
+
&engine,
|
|
55
|
+
&mut RealCommandRunner::default(),
|
|
56
|
+
discovery_bounds(),
|
|
57
|
+
)?;
|
|
58
|
+
let request = CatalogRequest {
|
|
59
|
+
provider: ProviderId::new(KIRO_DESCRIPTOR.identity.id).map_err(DiscoveryError::Contract)?,
|
|
60
|
+
native: native.clone(),
|
|
61
|
+
executable: engine.clone(),
|
|
62
|
+
cwd: resolve_cwd(workspace)?,
|
|
63
|
+
source: CHAT_CATALOG_SOURCE,
|
|
64
|
+
bounds: discovery_bounds(),
|
|
65
|
+
};
|
|
66
|
+
let mut host = CatalogReader::new(request.clone(), RealCommandRunner::default())
|
|
67
|
+
.map_err(DiscoveryError::Contract)?;
|
|
68
|
+
// H1 does not use these pointers; keep the actual current executable and do
|
|
69
|
+
// not manufacture an alternate CLI or execute an MCP server for discovery.
|
|
70
|
+
let adapter = KiroAdapter::new(McpStdio {
|
|
71
|
+
executable: std::env::current_exe()?.canonicalize()?,
|
|
72
|
+
arguments: Vec::new(),
|
|
73
|
+
environment: Default::default(),
|
|
74
|
+
})
|
|
75
|
+
.map_err(DiscoveryError::Contract)?;
|
|
76
|
+
let catalog = adapter
|
|
77
|
+
.discover(&request, &mut host)
|
|
78
|
+
.map_err(DiscoveryError::Catalog)?;
|
|
79
|
+
Ok(Discovery {
|
|
80
|
+
engine,
|
|
81
|
+
native,
|
|
82
|
+
catalog,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pub fn models(
|
|
87
|
+
args: &crate::cli::ModelsArgs,
|
|
88
|
+
) -> Result<crate::cli::CmdResult, crate::cli::CliError> {
|
|
89
|
+
let result = std::env::current_dir()
|
|
90
|
+
.map_err(DiscoveryError::Io)
|
|
91
|
+
.and_then(|cwd| discover(&cwd));
|
|
92
|
+
render_models(args, result)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn render_models(
|
|
96
|
+
args: &crate::cli::ModelsArgs,
|
|
97
|
+
result: Result<Discovery, DiscoveryError>,
|
|
98
|
+
) -> Result<crate::cli::CmdResult, crate::cli::CliError> {
|
|
99
|
+
use crate::cli::{CmdOutput, CmdResult, ExitCode};
|
|
100
|
+
use serde_json::json;
|
|
101
|
+
let (value, human, exit) = match result {
|
|
102
|
+
Ok(discovery) => {
|
|
103
|
+
let query = args.search.as_deref().map(str::to_lowercase);
|
|
104
|
+
let rows: Vec<_> = discovery.catalog.models.iter()
|
|
105
|
+
.filter(|model| query.as_ref().is_none_or(|query| model.id.to_lowercase().contains(query)))
|
|
106
|
+
.map(|model| {
|
|
107
|
+
let efforts = match &model.efforts {
|
|
108
|
+
Support::Supported(values) => json!({"state":"supported","values":values.iter().map(|value| value.as_str()).collect::<Vec<_>>()}),
|
|
109
|
+
Support::Unsupported(reason) => json!({"state":"unsupported","reason":reason.code}),
|
|
110
|
+
Support::Unverified(reason) => json!({"state":"unverified","reason":reason.code}),
|
|
111
|
+
};
|
|
112
|
+
json!({"provider":args.provider,"model_id":model.id,"role_model":model.id,"aliases":[],"efforts":efforts})
|
|
113
|
+
}).collect();
|
|
114
|
+
let value = json!({"schema_version":"models.v1","ok":true,"provider":args.provider,
|
|
115
|
+
"models":rows,"auth":"unknown","auth_basis":"native_catalog_visibility_is_not_authorization",
|
|
116
|
+
"source":{"schema":discovery.catalog.schema,"executable":discovery.engine,
|
|
117
|
+
"version":discovery.native.version,"executable_sha256":team_agent_contract::host::digest_hex(discovery.native.executable_sha256)},
|
|
118
|
+
"current_role_model":null,"search":args.search});
|
|
119
|
+
let mut lines = vec![format!("models.v1 | {} native model IDs:", args.provider)];
|
|
120
|
+
lines.extend(
|
|
121
|
+
rows.iter()
|
|
122
|
+
.filter_map(|row| row["model_id"].as_str())
|
|
123
|
+
.map(str::to_owned),
|
|
124
|
+
);
|
|
125
|
+
lines.push("Catalog visibility does not prove subscription authorization, runtime model selection, or effort support.".into());
|
|
126
|
+
(value, lines.join("\n"), ExitCode::Ok)
|
|
127
|
+
}
|
|
128
|
+
Err(error) => {
|
|
129
|
+
let message = error.to_string();
|
|
130
|
+
let action = "Check the selected native executable and existing subscription; no model fallback or login retry was performed.";
|
|
131
|
+
(
|
|
132
|
+
json!({"schema_version":"models.v1","ok":false,"provider":args.provider,"models":[],"auth":"not_ready","error":message,"action":action}),
|
|
133
|
+
format!("models.v1 | {message}\n{action}"),
|
|
134
|
+
ExitCode::Error,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
Ok(CmdResult {
|
|
139
|
+
output: CmdOutput::Human(if args.json {
|
|
140
|
+
serde_json::to_string_pretty(&value)?
|
|
141
|
+
} else {
|
|
142
|
+
human
|
|
143
|
+
}),
|
|
144
|
+
exit,
|
|
145
|
+
as_json: false,
|
|
146
|
+
preserve_json_order: true,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[cfg(test)]
|
|
151
|
+
mod tests {
|
|
152
|
+
use super::*;
|
|
153
|
+
use crate::cli::{CmdOutput, ExitCode, ModelsArgs};
|
|
154
|
+
use team_agent_contract::contract::plan::ModelRecord;
|
|
155
|
+
|
|
156
|
+
#[test]
|
|
157
|
+
fn native_catalog_projection_preserves_ids_without_claiming_auth_or_effort() {
|
|
158
|
+
let native = NativeIdentity {
|
|
159
|
+
version: "controlled".into(),
|
|
160
|
+
harness: "v3".into(),
|
|
161
|
+
ui: "tui".into(),
|
|
162
|
+
platform: Platform::Linux,
|
|
163
|
+
executable_sha256: Digest([1; 32]),
|
|
164
|
+
};
|
|
165
|
+
let discovery = Discovery {
|
|
166
|
+
engine: "/controlled/kiro-cli-chat".into(),
|
|
167
|
+
native: native.clone(),
|
|
168
|
+
catalog: CatalogObservation {
|
|
169
|
+
provider: ProviderId::new("kiro").unwrap(),
|
|
170
|
+
native,
|
|
171
|
+
schema: "controlled-json".into(),
|
|
172
|
+
models: vec![ModelRecord {
|
|
173
|
+
id: "Exact-Case-ID".into(),
|
|
174
|
+
efforts: Support::Unverified(Reason {
|
|
175
|
+
code: "not-observed",
|
|
176
|
+
message: "No effort evidence",
|
|
177
|
+
}),
|
|
178
|
+
}],
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
let args = ModelsArgs {
|
|
182
|
+
provider: "kiro".into(),
|
|
183
|
+
search: Some("exact".into()),
|
|
184
|
+
json: true,
|
|
185
|
+
};
|
|
186
|
+
let result = render_models(&args, Ok(discovery)).unwrap();
|
|
187
|
+
assert_eq!(result.exit, ExitCode::Ok);
|
|
188
|
+
let CmdOutput::Human(text) = result.output else {
|
|
189
|
+
panic!("JSON text projection");
|
|
190
|
+
};
|
|
191
|
+
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
192
|
+
assert_eq!(json["auth"], "unknown");
|
|
193
|
+
assert_eq!(json["models"][0]["model_id"], "Exact-Case-ID");
|
|
194
|
+
assert_eq!(json["models"][0]["efforts"]["state"], "unverified");
|
|
195
|
+
assert_eq!(json["source"]["schema"], "controlled-json");
|
|
196
|
+
assert!(json["current_role_model"].is_null());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[test]
|
|
200
|
+
fn version_failure_projection_preserves_the_received_version() {
|
|
201
|
+
let args = ModelsArgs {
|
|
202
|
+
provider: "kiro".into(),
|
|
203
|
+
search: None,
|
|
204
|
+
json: true,
|
|
205
|
+
};
|
|
206
|
+
let error = parse_version(b"kiro-cli-chat 2.29.invalid\n").unwrap_err();
|
|
207
|
+
let result = render_models(&args, Err(DiscoveryError::Probe(error))).unwrap();
|
|
208
|
+
assert_eq!(result.exit, ExitCode::Error);
|
|
209
|
+
let CmdOutput::Human(text) = result.output else {
|
|
210
|
+
panic!("JSON text projection");
|
|
211
|
+
};
|
|
212
|
+
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
213
|
+
assert!(json["error"]
|
|
214
|
+
.as_str()
|
|
215
|
+
.unwrap()
|
|
216
|
+
.contains("kiro-cli-chat 2.29.invalid"));
|
|
217
|
+
assert_eq!(json["models"], serde_json::json!([]));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#[test]
|
|
221
|
+
fn native_discovery_failure_has_no_catalog_or_fallback() {
|
|
222
|
+
let args = ModelsArgs {
|
|
223
|
+
provider: "kiro".into(),
|
|
224
|
+
search: None,
|
|
225
|
+
json: true,
|
|
226
|
+
};
|
|
227
|
+
let result = render_models(&args, Err(DiscoveryError::Executable("kiro-cli"))).unwrap();
|
|
228
|
+
assert_eq!(result.exit, ExitCode::Error);
|
|
229
|
+
let CmdOutput::Human(text) = result.output else {
|
|
230
|
+
panic!("JSON text projection");
|
|
231
|
+
};
|
|
232
|
+
let json: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
233
|
+
assert_eq!(json["ok"], false);
|
|
234
|
+
assert_eq!(json["models"], serde_json::json!([]));
|
|
235
|
+
assert!(json["error"].as_str().unwrap().contains("kiro-cli"));
|
|
236
|
+
}
|
|
237
|
+
}
|