@team-agent/installer 0.1.10 → 0.2.0
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/crates/team-agent-core/src/lib.rs +50 -5
- package/package.json +1 -1
- package/schemas/team.schema.json +1 -0
- package/skills/team-agent/SKILL.md +1 -1
- package/src/team_agent/approvals/__init__.py +65 -0
- package/src/team_agent/approvals/constants.py +6 -0
- package/src/team_agent/approvals/parsing.py +176 -0
- package/src/team_agent/approvals/runtime_prompts.py +171 -0
- package/src/team_agent/approvals/status.py +165 -0
- package/src/team_agent/cli/__init__.py +135 -0
- package/src/team_agent/cli/commands.py +335 -0
- package/src/team_agent/cli/e2e.py +202 -0
- package/src/team_agent/cli/helpers.py +137 -0
- package/src/team_agent/cli/parser.py +470 -0
- package/src/team_agent/compiler.py +98 -33
- package/src/team_agent/coordinator/__init__.py +53 -0
- package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
- package/src/team_agent/coordinator/lifecycle.py +319 -0
- package/src/team_agent/coordinator/metadata.py +61 -0
- package/src/team_agent/coordinator/paths.py +17 -0
- package/src/team_agent/diagnose/__init__.py +48 -0
- package/src/team_agent/diagnose/checks.py +101 -0
- package/src/team_agent/diagnose/health.py +241 -0
- package/src/team_agent/diagnose/preflight.py +194 -0
- package/src/team_agent/diagnose/quick_start.py +233 -0
- package/src/team_agent/display/__init__.py +61 -0
- package/src/team_agent/display/close.py +147 -0
- package/src/team_agent/display/ghostty.py +77 -0
- package/src/team_agent/display/worker_window.py +110 -0
- package/src/team_agent/display/workspace.py +473 -0
- package/src/team_agent/launch/__init__.py +41 -0
- package/src/team_agent/launch/bootstrap.py +85 -0
- package/src/team_agent/launch/config.py +106 -0
- package/src/team_agent/launch/core.py +291 -0
- package/src/team_agent/launch/requirements.py +57 -0
- package/src/team_agent/leader/__init__.py +320 -0
- package/src/team_agent/lifecycle/__init__.py +5 -0
- package/src/team_agent/lifecycle/agents.py +226 -0
- package/src/team_agent/lifecycle/operations.py +321 -0
- package/src/team_agent/lifecycle/start.py +360 -0
- package/src/team_agent/mcp_server/__init__.py +42 -0
- package/src/team_agent/mcp_server/__main__.py +7 -0
- package/src/team_agent/mcp_server/contracts.py +148 -0
- package/src/team_agent/mcp_server/normalize.py +257 -0
- package/src/team_agent/mcp_server/server.py +150 -0
- package/src/team_agent/mcp_server/tools.py +205 -0
- package/src/team_agent/message_store/__init__.py +23 -0
- package/src/team_agent/message_store/agent_health.py +109 -0
- package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
- package/src/team_agent/message_store/result_watchers.py +102 -0
- package/src/team_agent/message_store/schema.py +266 -0
- package/src/team_agent/messaging/__init__.py +1 -0
- package/src/team_agent/messaging/activity_detector.py +190 -0
- package/src/team_agent/messaging/delivery.py +128 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +217 -0
- package/src/team_agent/messaging/internal_delivery.py +46 -0
- package/src/team_agent/messaging/leader.py +317 -0
- package/src/team_agent/messaging/leader_panes.py +343 -0
- package/src/team_agent/messaging/result_delivery.py +300 -0
- package/src/team_agent/messaging/results.py +456 -0
- package/src/team_agent/messaging/scheduler.py +418 -0
- package/src/team_agent/messaging/send.py +493 -0
- package/src/team_agent/messaging/tmux_io.py +337 -0
- package/src/team_agent/messaging/tmux_prompt.py +229 -0
- package/src/team_agent/orchestrator/__init__.py +376 -0
- package/src/team_agent/orchestrator/plan.py +122 -0
- package/src/team_agent/orchestrator/state.py +128 -0
- package/src/team_agent/profiles/__init__.py +82 -0
- package/src/team_agent/profiles/constants.py +19 -0
- package/src/team_agent/profiles/core.py +407 -0
- package/src/team_agent/profiles/helpers.py +69 -0
- package/src/team_agent/profiles/provider_env.py +188 -0
- package/src/team_agent/profiles/smoke.py +201 -0
- package/src/team_agent/provider_cli/__init__.py +43 -0
- package/src/team_agent/provider_cli/adapter.py +167 -0
- package/src/team_agent/provider_cli/base.py +48 -0
- package/src/team_agent/provider_cli/claude.py +457 -0
- package/src/team_agent/provider_cli/codex.py +319 -0
- package/src/team_agent/provider_cli/copilot.py +8 -0
- package/src/team_agent/provider_cli/fake.py +39 -0
- package/src/team_agent/provider_cli/gemini.py +95 -0
- package/src/team_agent/provider_cli/opencode.py +8 -0
- package/src/team_agent/provider_cli/prompt.py +62 -0
- package/src/team_agent/provider_cli/registry.py +18 -0
- package/src/team_agent/provider_cli/unsupported.py +32 -0
- package/src/team_agent/providers.py +67 -949
- package/src/team_agent/quality_gates.py +104 -0
- package/src/team_agent/restart/__init__.py +34 -0
- package/src/team_agent/restart/orchestration.py +328 -0
- package/src/team_agent/restart/selection.py +89 -0
- package/src/team_agent/restart/snapshot.py +70 -0
- package/src/team_agent/runtime.py +802 -5740
- package/src/team_agent/rust_core.py +22 -5
- package/src/team_agent/sessions/__init__.py +25 -0
- package/src/team_agent/sessions/capture.py +93 -0
- package/src/team_agent/sessions/inventory.py +44 -0
- package/src/team_agent/sessions/resume.py +135 -0
- package/src/team_agent/spec.py +3 -1
- package/src/team_agent/state.py +204 -4
- package/src/team_agent/status/__init__.py +63 -0
- package/src/team_agent/status/approvals.py +52 -0
- package/src/team_agent/status/compact.py +158 -0
- package/src/team_agent/status/constants.py +18 -0
- package/src/team_agent/status/inbox.py +28 -0
- package/src/team_agent/status/peek.py +117 -0
- package/src/team_agent/status/queries.py +168 -0
- package/src/team_agent/terminal.py +57 -0
- package/src/team_agent/cli.py +0 -857
- package/src/team_agent/mcp_server.py +0 -579
- package/src/team_agent/profiles.py +0 -882
|
@@ -215,15 +215,54 @@ pub fn validate_profile(profile: &Profile) -> Result<(), Vec<String>> {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
pub fn contains_inline_secret(value: &str) -> bool {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
||
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
contains_secret_assignment(value)
|
|
219
|
+
|| contains_bearer_secret(value)
|
|
220
|
+
|| value
|
|
221
|
+
.split_whitespace()
|
|
222
|
+
.any(|chunk| chunk.starts_with("sk-") || looks_base64_secret(chunk))
|
|
223
223
|
|| value.starts_with("sk-")
|
|
224
224
|
|| looks_base64_secret(value)
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
fn contains_secret_assignment(value: &str) -> bool {
|
|
228
|
+
for line in value.lines() {
|
|
229
|
+
for separator in ['=', ':'] {
|
|
230
|
+
let Some((key, raw)) = line.split_once(separator) else {
|
|
231
|
+
continue;
|
|
232
|
+
};
|
|
233
|
+
let normalized: String = key
|
|
234
|
+
.to_ascii_lowercase()
|
|
235
|
+
.chars()
|
|
236
|
+
.filter(|ch| ch.is_ascii_alphanumeric())
|
|
237
|
+
.collect();
|
|
238
|
+
if !matches!(
|
|
239
|
+
normalized.as_str(),
|
|
240
|
+
"apikey" | "token" | "secret" | "password" | "credential"
|
|
241
|
+
) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
let candidate = raw.trim().trim_matches(|ch| ch == '"' || ch == '\'');
|
|
245
|
+
if candidate.starts_with("sk-") || candidate.len() >= 8 || looks_base64_secret(candidate) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
false
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fn contains_bearer_secret(value: &str) -> bool {
|
|
254
|
+
let mut previous_was_bearer = false;
|
|
255
|
+
for chunk in value.split_whitespace() {
|
|
256
|
+
if previous_was_bearer && chunk.len() >= 16 && chunk.chars().all(|ch| {
|
|
257
|
+
ch.is_ascii_alphanumeric() || matches!(ch, '.' | '_' | '~' | '+' | '/' | '=' | '-')
|
|
258
|
+
}) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
previous_was_bearer = chunk.eq_ignore_ascii_case("bearer");
|
|
262
|
+
}
|
|
263
|
+
false
|
|
264
|
+
}
|
|
265
|
+
|
|
227
266
|
pub fn json_escape(input: &str) -> String {
|
|
228
267
|
let mut out = String::new();
|
|
229
268
|
for ch in input.chars() {
|
|
@@ -273,6 +312,12 @@ mod tests {
|
|
|
273
312
|
assert!(!redacted.contains("sk-test"));
|
|
274
313
|
}
|
|
275
314
|
|
|
315
|
+
#[test]
|
|
316
|
+
fn inline_secret_scan_allows_ordinary_token_prose() {
|
|
317
|
+
assert!(!contains_inline_secret("Use the visible token in logs for delivery evidence."));
|
|
318
|
+
assert!(contains_inline_secret("API_KEY=sk-inline-secret"));
|
|
319
|
+
}
|
|
320
|
+
|
|
276
321
|
#[test]
|
|
277
322
|
fn profile_rejects_inline_secret() {
|
|
278
323
|
let profile = Profile {
|
package/package.json
CHANGED
package/schemas/team.schema.json
CHANGED
|
@@ -134,6 +134,7 @@
|
|
|
134
134
|
"auth_mode": { "enum": ["subscription", "official_api", "compatible_api"] },
|
|
135
135
|
"profile": { "type": "string", "minLength": 1 },
|
|
136
136
|
"credential_ref": { "type": "string", "minLength": 1 },
|
|
137
|
+
"forked_from": { "type": "string", "minLength": 1 },
|
|
137
138
|
"working_directory": { "type": "string", "minLength": 1 },
|
|
138
139
|
"paused": { "type": "boolean" },
|
|
139
140
|
"system_prompt": {
|
|
@@ -118,7 +118,7 @@ For diagnosis, run `team-agent profile show deepseek --workspace . --json`; neve
|
|
|
118
118
|
- `team-agent send --watch-result coder "Do the bounded task"` sends a direct worker message, returns after delivery, and lets the coordinator collect/report completion asynchronously.
|
|
119
119
|
- After `send --watch-result` succeeds, do not run `sleep`, `status`, `inbox`, or `collect` polling loops unless the user explicitly asks for diagnosis; the coordinator will notify the leader when the result arrives.
|
|
120
120
|
- `team-agent send --task task_initial "Start"` routes by task.
|
|
121
|
-
- `team-agent status` shows team, worker health, result-store counts, `session_id`, `captured_via`, and attribution confidence.
|
|
121
|
+
- `team-agent status` shows team, worker health, result-store counts, `session_id`, `captured_via`, and attribution confidence. `team-agent status --json` is compact and context-safe by default; use `team-agent status --detail --json` only for raw runtime-state diagnostics.
|
|
122
122
|
- `team-agent status coder` shows one worker.
|
|
123
123
|
- `team-agent approvals [coder]` shows structured pending approval prompts without copying worker terminal pages.
|
|
124
124
|
- `team-agent inbox coder` shows message history only. Final results are not in inbox.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent.approvals.constants import (
|
|
4
|
+
INTERNAL_MCP_APPROVAL_CHOICE,
|
|
5
|
+
INTERNAL_MCP_AUTO_APPROVE_TOOLS,
|
|
6
|
+
STARTUP_PROMPT_RUNTIME_CHECK_LIMIT,
|
|
7
|
+
)
|
|
8
|
+
from team_agent.approvals.parsing import (
|
|
9
|
+
APPROVAL_CHOICE_RE,
|
|
10
|
+
active_approval_choice_index,
|
|
11
|
+
active_approval_control_index,
|
|
12
|
+
approval_choice_keys,
|
|
13
|
+
approval_prompt_fingerprint,
|
|
14
|
+
capture_has_approval_prompt,
|
|
15
|
+
capture_has_team_orchestrator_mcp_prompt,
|
|
16
|
+
choose_internal_mcp_approval_choice,
|
|
17
|
+
extract_approval_choices,
|
|
18
|
+
extract_approval_prompt,
|
|
19
|
+
extract_command_approval_subject,
|
|
20
|
+
is_approval_control_line,
|
|
21
|
+
line_is_approval_choice,
|
|
22
|
+
)
|
|
23
|
+
from team_agent.approvals.runtime_prompts import (
|
|
24
|
+
handle_internal_mcp_approval_prompt,
|
|
25
|
+
handle_provider_runtime_prompts,
|
|
26
|
+
handle_provider_startup_prompts,
|
|
27
|
+
submit_internal_mcp_approval,
|
|
28
|
+
)
|
|
29
|
+
from team_agent.approvals.status import (
|
|
30
|
+
age_text,
|
|
31
|
+
agent_health_status,
|
|
32
|
+
current_task_for_agent,
|
|
33
|
+
detect_provider_status,
|
|
34
|
+
refresh_agent_runtime_statuses,
|
|
35
|
+
sync_agent_health,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"APPROVAL_CHOICE_RE",
|
|
40
|
+
"INTERNAL_MCP_APPROVAL_CHOICE",
|
|
41
|
+
"INTERNAL_MCP_AUTO_APPROVE_TOOLS",
|
|
42
|
+
"STARTUP_PROMPT_RUNTIME_CHECK_LIMIT",
|
|
43
|
+
"active_approval_choice_index",
|
|
44
|
+
"active_approval_control_index",
|
|
45
|
+
"age_text",
|
|
46
|
+
"agent_health_status",
|
|
47
|
+
"approval_choice_keys",
|
|
48
|
+
"approval_prompt_fingerprint",
|
|
49
|
+
"capture_has_approval_prompt",
|
|
50
|
+
"capture_has_team_orchestrator_mcp_prompt",
|
|
51
|
+
"choose_internal_mcp_approval_choice",
|
|
52
|
+
"current_task_for_agent",
|
|
53
|
+
"detect_provider_status",
|
|
54
|
+
"extract_approval_choices",
|
|
55
|
+
"extract_approval_prompt",
|
|
56
|
+
"extract_command_approval_subject",
|
|
57
|
+
"handle_internal_mcp_approval_prompt",
|
|
58
|
+
"handle_provider_runtime_prompts",
|
|
59
|
+
"handle_provider_startup_prompts",
|
|
60
|
+
"is_approval_control_line",
|
|
61
|
+
"line_is_approval_choice",
|
|
62
|
+
"refresh_agent_runtime_statuses",
|
|
63
|
+
"submit_internal_mcp_approval",
|
|
64
|
+
"sync_agent_health",
|
|
65
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from team_agent.approvals.constants import INTERNAL_MCP_APPROVAL_CHOICE
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
APPROVAL_CHOICE_RE = re.compile(r"(?:[›❯>]\s*)?(\d+)\.\s+(.+?)(?:\s{2,}.+)?$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def capture_has_approval_prompt(text: str) -> bool:
|
|
15
|
+
return extract_approval_prompt("_", text) is not None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_approval_prompt(agent_id: str, text: str) -> dict[str, Any] | None:
|
|
19
|
+
lines = text.splitlines()
|
|
20
|
+
control_index = active_approval_control_index(lines)
|
|
21
|
+
if control_index is None:
|
|
22
|
+
return None
|
|
23
|
+
for index in range(control_index, -1, -1):
|
|
24
|
+
line = lines[index]
|
|
25
|
+
if "Allow the team_orchestrator MCP server to run tool" not in line:
|
|
26
|
+
continue
|
|
27
|
+
tool_match = re.search(r'run tool "([^"]+)"', line)
|
|
28
|
+
return {
|
|
29
|
+
"agent_id": agent_id,
|
|
30
|
+
"state": "waiting_approval",
|
|
31
|
+
"kind": "mcp_tool",
|
|
32
|
+
"tool": tool_match.group(1) if tool_match else None,
|
|
33
|
+
"prompt": line.strip(),
|
|
34
|
+
"choices": extract_approval_choices(lines[index : control_index + 1]),
|
|
35
|
+
}
|
|
36
|
+
for index in range(control_index, -1, -1):
|
|
37
|
+
line = lines[index]
|
|
38
|
+
if line_is_approval_choice(line):
|
|
39
|
+
continue
|
|
40
|
+
tool_match = re.search(r"\bteam_orchestrator\s*[-.]\s*([A-Za-z_][A-Za-z0-9_]*)\b", line)
|
|
41
|
+
if not tool_match:
|
|
42
|
+
continue
|
|
43
|
+
return {
|
|
44
|
+
"agent_id": agent_id,
|
|
45
|
+
"state": "waiting_approval",
|
|
46
|
+
"kind": "mcp_tool",
|
|
47
|
+
"tool": tool_match.group(1),
|
|
48
|
+
"prompt": f"team_orchestrator - {tool_match.group(1)}",
|
|
49
|
+
"choices": extract_approval_choices(lines[index : control_index + 1]),
|
|
50
|
+
}
|
|
51
|
+
for index in range(control_index, -1, -1):
|
|
52
|
+
line = lines[index]
|
|
53
|
+
if "Would you like to run the following command" not in line:
|
|
54
|
+
continue
|
|
55
|
+
return {
|
|
56
|
+
"agent_id": agent_id,
|
|
57
|
+
"state": "waiting_approval",
|
|
58
|
+
"kind": "command",
|
|
59
|
+
"command": extract_command_approval_subject(lines[: control_index + 1], index),
|
|
60
|
+
"prompt": line.strip(),
|
|
61
|
+
"choices": extract_approval_choices(lines[index : control_index + 1]),
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
"agent_id": agent_id,
|
|
65
|
+
"state": "waiting_approval",
|
|
66
|
+
"kind": "unknown",
|
|
67
|
+
"prompt": "approval prompt detected",
|
|
68
|
+
"choices": extract_approval_choices(lines[: control_index + 1]),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def active_approval_control_index(lines: list[str]) -> int | None:
|
|
73
|
+
control_indices = [
|
|
74
|
+
index
|
|
75
|
+
for index, line in enumerate(lines)
|
|
76
|
+
if is_approval_control_line(line)
|
|
77
|
+
]
|
|
78
|
+
if not control_indices:
|
|
79
|
+
return None
|
|
80
|
+
control_index = control_indices[-1]
|
|
81
|
+
if any(line.strip() for line in lines[control_index + 1 :]):
|
|
82
|
+
return None
|
|
83
|
+
return control_index
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def is_approval_control_line(line: str) -> bool:
|
|
87
|
+
normalized = line.lower()
|
|
88
|
+
return "enter to submit | esc to cancel" in normalized or ("esc to cancel" in normalized and "tab to amend" in normalized)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def extract_approval_choices(lines: list[str]) -> list[str]:
|
|
92
|
+
choices: list[str] = []
|
|
93
|
+
for line in lines:
|
|
94
|
+
stripped = line.strip()
|
|
95
|
+
match = APPROVAL_CHOICE_RE.match(stripped)
|
|
96
|
+
if not match:
|
|
97
|
+
continue
|
|
98
|
+
label = match.group(2).strip()
|
|
99
|
+
if label and label not in choices:
|
|
100
|
+
choices.append(label)
|
|
101
|
+
return choices
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def line_is_approval_choice(line: str) -> bool:
|
|
105
|
+
return APPROVAL_CHOICE_RE.match(line.strip()) is not None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def extract_command_approval_subject(lines: list[str], prompt_index: int) -> str | None:
|
|
109
|
+
for line in reversed(lines[:prompt_index]):
|
|
110
|
+
stripped = line.strip()
|
|
111
|
+
if stripped.startswith("Bash(") or stripped.startswith("Shell("):
|
|
112
|
+
return stripped[:200]
|
|
113
|
+
for line in lines[prompt_index + 1 : prompt_index + 8]:
|
|
114
|
+
stripped = line.strip()
|
|
115
|
+
if stripped.startswith("Bash(") or stripped.startswith("Shell("):
|
|
116
|
+
return stripped[:200]
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def active_approval_choice_index(text: str) -> int | None:
|
|
121
|
+
for line in text.splitlines():
|
|
122
|
+
stripped = line.strip()
|
|
123
|
+
if not (stripped.startswith("›") or stripped.startswith("❯") or stripped.startswith(">")):
|
|
124
|
+
continue
|
|
125
|
+
match = re.match(r"[›❯>]\s*(\d+)\.", stripped)
|
|
126
|
+
if match:
|
|
127
|
+
return int(match.group(1)) - 1
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def capture_has_team_orchestrator_mcp_prompt(text: str) -> bool:
|
|
132
|
+
return (
|
|
133
|
+
"Allow the team_orchestrator MCP server to run tool" in text
|
|
134
|
+
or re.search(r"\bteam_orchestrator\s*[-.]\s*[A-Za-z_][A-Za-z0-9_]*\b", text) is not None
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def approval_prompt_fingerprint(prompt: dict[str, Any]) -> str:
|
|
139
|
+
data = {
|
|
140
|
+
"kind": prompt.get("kind"),
|
|
141
|
+
"tool": prompt.get("tool"),
|
|
142
|
+
"prompt": prompt.get("prompt"),
|
|
143
|
+
"choices": prompt.get("choices") or [],
|
|
144
|
+
}
|
|
145
|
+
return hashlib.sha256(json.dumps(data, sort_keys=True, ensure_ascii=False).encode("utf-8")).hexdigest()[:16]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def choose_internal_mcp_approval_choice(prompt: dict[str, Any]) -> str:
|
|
149
|
+
choices = prompt.get("choices") or []
|
|
150
|
+
if INTERNAL_MCP_APPROVAL_CHOICE in choices:
|
|
151
|
+
return INTERNAL_MCP_APPROVAL_CHOICE
|
|
152
|
+
for choice in choices:
|
|
153
|
+
if str(choice).startswith("Yes, and don't ask again"):
|
|
154
|
+
return str(choice)
|
|
155
|
+
if "Allow" in choices:
|
|
156
|
+
return "Allow"
|
|
157
|
+
if "Yes" in choices:
|
|
158
|
+
return "Yes"
|
|
159
|
+
return INTERNAL_MCP_APPROVAL_CHOICE
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def approval_choice_keys(prompt: dict[str, Any], capture_text: str, choice: str) -> list[str]:
|
|
163
|
+
choices = prompt.get("choices") or []
|
|
164
|
+
try:
|
|
165
|
+
target_index = choices.index(choice)
|
|
166
|
+
except ValueError:
|
|
167
|
+
return ["Down", "Enter"]
|
|
168
|
+
active_index = active_approval_choice_index(capture_text)
|
|
169
|
+
if active_index is None:
|
|
170
|
+
return [str(target_index + 1), "Enter"]
|
|
171
|
+
delta = target_index - active_index
|
|
172
|
+
if delta > 0:
|
|
173
|
+
return ["Down"] * delta + ["Enter"]
|
|
174
|
+
if delta < 0:
|
|
175
|
+
return ["Up"] * abs(delta) + ["Enter"]
|
|
176
|
+
return ["Enter"]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from team_agent.approvals.constants import (
|
|
8
|
+
INTERNAL_MCP_AUTO_APPROVE_TOOLS,
|
|
9
|
+
STARTUP_PROMPT_RUNTIME_CHECK_LIMIT,
|
|
10
|
+
)
|
|
11
|
+
from team_agent.approvals.parsing import (
|
|
12
|
+
approval_choice_keys,
|
|
13
|
+
approval_prompt_fingerprint,
|
|
14
|
+
choose_internal_mcp_approval_choice,
|
|
15
|
+
extract_approval_prompt,
|
|
16
|
+
)
|
|
17
|
+
from team_agent.events import EventLog
|
|
18
|
+
from team_agent.status import APPROVAL_SCAN_LINES
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def handle_provider_runtime_prompts(workspace: Path, state: dict[str, Any], event_log: EventLog) -> None:
|
|
22
|
+
from team_agent.runtime import _tmux_session_exists, _tmux_window_exists, get_adapter
|
|
23
|
+
_ = workspace
|
|
24
|
+
session_name = state.get("session_name")
|
|
25
|
+
if not session_name or not _tmux_session_exists(session_name):
|
|
26
|
+
return
|
|
27
|
+
for agent_id, agent_state in state.get("agents", {}).items():
|
|
28
|
+
if agent_state.get("status") in {"paused", "stopped", "missing"}:
|
|
29
|
+
continue
|
|
30
|
+
window = agent_state.get("window", agent_id)
|
|
31
|
+
if not _tmux_window_exists(session_name, window):
|
|
32
|
+
continue
|
|
33
|
+
internal_mcp = handle_internal_mcp_approval_prompt(agent_id, session_name, window, event_log)
|
|
34
|
+
if internal_mcp is not None:
|
|
35
|
+
continue
|
|
36
|
+
adapter = get_adapter(agent_state["provider"])
|
|
37
|
+
for prompt_event in adapter.handle_runtime_prompts(session_name, window):
|
|
38
|
+
event_log.write(
|
|
39
|
+
"runtime.prompt_handled",
|
|
40
|
+
agent_id=agent_id,
|
|
41
|
+
provider=agent_state["provider"],
|
|
42
|
+
**prompt_event,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def handle_provider_startup_prompts(workspace: Path, state: dict[str, Any], event_log: EventLog) -> None:
|
|
47
|
+
from team_agent.runtime import _tmux_session_exists, _tmux_window_exists, get_adapter
|
|
48
|
+
_ = workspace
|
|
49
|
+
session_name = state.get("session_name")
|
|
50
|
+
if not session_name or not _tmux_session_exists(session_name):
|
|
51
|
+
return
|
|
52
|
+
for agent_id, agent_state in state.get("agents", {}).items():
|
|
53
|
+
if agent_state.get("status") in {"paused", "stopped", "missing"}:
|
|
54
|
+
continue
|
|
55
|
+
window = agent_state.get("window", agent_id)
|
|
56
|
+
if not _tmux_window_exists(session_name, window):
|
|
57
|
+
continue
|
|
58
|
+
spawned_at = str(agent_state.get("spawned_at") or "")
|
|
59
|
+
if agent_state.get("startup_prompt_check_spawned_at") != spawned_at:
|
|
60
|
+
agent_state["startup_prompt_check_spawned_at"] = spawned_at
|
|
61
|
+
agent_state["startup_prompt_check_count"] = 0
|
|
62
|
+
check_count = int(agent_state.get("startup_prompt_check_count") or 0)
|
|
63
|
+
if check_count >= STARTUP_PROMPT_RUNTIME_CHECK_LIMIT:
|
|
64
|
+
continue
|
|
65
|
+
agent_state["startup_prompt_check_count"] = check_count + 1
|
|
66
|
+
adapter = get_adapter(agent_state["provider"])
|
|
67
|
+
for prompt_event in adapter.handle_startup_prompts(session_name, window, checks=1, sleep_s=0.0):
|
|
68
|
+
event_log.write(
|
|
69
|
+
"runtime.startup_prompt_handled",
|
|
70
|
+
agent_id=agent_id,
|
|
71
|
+
provider=agent_state["provider"],
|
|
72
|
+
**prompt_event,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def handle_internal_mcp_approval_prompt(
|
|
77
|
+
agent_id: str,
|
|
78
|
+
session_name: str,
|
|
79
|
+
window: str,
|
|
80
|
+
event_log: EventLog,
|
|
81
|
+
) -> dict[str, Any] | None:
|
|
82
|
+
from team_agent.runtime import run_cmd
|
|
83
|
+
target = f"{session_name}:{window}"
|
|
84
|
+
proc = run_cmd(["tmux", "capture-pane", "-p", "-S", f"-{APPROVAL_SCAN_LINES}", "-t", target], timeout=5)
|
|
85
|
+
if proc.returncode != 0:
|
|
86
|
+
return None
|
|
87
|
+
prompt = extract_approval_prompt(agent_id, proc.stdout)
|
|
88
|
+
if not prompt or prompt.get("kind") != "mcp_tool":
|
|
89
|
+
return None
|
|
90
|
+
tool = str(prompt.get("tool") or "")
|
|
91
|
+
fingerprint = approval_prompt_fingerprint(prompt)
|
|
92
|
+
if tool not in INTERNAL_MCP_AUTO_APPROVE_TOOLS:
|
|
93
|
+
result = {
|
|
94
|
+
"ok": False,
|
|
95
|
+
"action": "skipped",
|
|
96
|
+
"reason": "tool_not_allowlisted",
|
|
97
|
+
"tool": tool,
|
|
98
|
+
"fingerprint": fingerprint,
|
|
99
|
+
}
|
|
100
|
+
event_log.write("runtime.internal_mcp_approval.skipped", agent_id=agent_id, **result)
|
|
101
|
+
return result
|
|
102
|
+
result = submit_internal_mcp_approval(agent_id, target, tool, prompt, proc.stdout)
|
|
103
|
+
event_log.write("runtime.internal_mcp_approval.auto", agent_id=agent_id, **result)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def submit_internal_mcp_approval(
|
|
108
|
+
agent_id: str,
|
|
109
|
+
target: str,
|
|
110
|
+
tool: str,
|
|
111
|
+
prompt: dict[str, Any],
|
|
112
|
+
capture_text: str,
|
|
113
|
+
attempts: int = 3,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
from team_agent.runtime import run_cmd
|
|
116
|
+
choice = choose_internal_mcp_approval_choice(prompt)
|
|
117
|
+
fingerprint = approval_prompt_fingerprint(prompt)
|
|
118
|
+
attempt_log: list[dict[str, Any]] = []
|
|
119
|
+
current_prompt = prompt
|
|
120
|
+
current_capture = capture_text
|
|
121
|
+
for attempt in range(1, attempts + 1):
|
|
122
|
+
keys = approval_choice_keys(current_prompt, current_capture, choice)
|
|
123
|
+
proc = run_cmd(["tmux", "send-keys", "-t", target, *keys], timeout=10)
|
|
124
|
+
if proc.returncode != 0:
|
|
125
|
+
return {
|
|
126
|
+
"ok": False,
|
|
127
|
+
"action": "auto_approve",
|
|
128
|
+
"tool": tool,
|
|
129
|
+
"choice": choice,
|
|
130
|
+
"fingerprint": fingerprint,
|
|
131
|
+
"attempts": attempt_log + [{"attempt": attempt, "submitted": False, "error": proc.stderr.strip()}],
|
|
132
|
+
"verification": "send_keys_failed",
|
|
133
|
+
}
|
|
134
|
+
time.sleep(0.35)
|
|
135
|
+
verify = run_cmd(["tmux", "capture-pane", "-p", "-S", f"-{APPROVAL_SCAN_LINES}", "-t", target], timeout=5)
|
|
136
|
+
if verify.returncode != 0:
|
|
137
|
+
attempt_log.append({"attempt": attempt, "submitted": True, "keys": keys, "verification": "capture_failed"})
|
|
138
|
+
continue
|
|
139
|
+
after_prompt = extract_approval_prompt(agent_id, verify.stdout)
|
|
140
|
+
if not after_prompt:
|
|
141
|
+
return {
|
|
142
|
+
"ok": True,
|
|
143
|
+
"action": "auto_approved",
|
|
144
|
+
"tool": tool,
|
|
145
|
+
"choice": choice,
|
|
146
|
+
"fingerprint": fingerprint,
|
|
147
|
+
"attempts": attempt_log + [{"attempt": attempt, "submitted": True, "keys": keys, "verification": "prompt_absent"}],
|
|
148
|
+
"verification": "prompt_absent_after_submit",
|
|
149
|
+
}
|
|
150
|
+
if after_prompt.get("kind") != "mcp_tool" or after_prompt.get("tool") != tool:
|
|
151
|
+
return {
|
|
152
|
+
"ok": True,
|
|
153
|
+
"action": "auto_approved",
|
|
154
|
+
"tool": tool,
|
|
155
|
+
"choice": choice,
|
|
156
|
+
"fingerprint": fingerprint,
|
|
157
|
+
"attempts": attempt_log + [{"attempt": attempt, "submitted": True, "keys": keys, "verification": "different_prompt_present"}],
|
|
158
|
+
"verification": "original_prompt_replaced",
|
|
159
|
+
}
|
|
160
|
+
attempt_log.append({"attempt": attempt, "submitted": True, "keys": keys, "verification": "prompt_still_present"})
|
|
161
|
+
current_prompt = after_prompt
|
|
162
|
+
current_capture = verify.stdout
|
|
163
|
+
return {
|
|
164
|
+
"ok": False,
|
|
165
|
+
"action": "auto_approve",
|
|
166
|
+
"tool": tool,
|
|
167
|
+
"choice": choice,
|
|
168
|
+
"fingerprint": fingerprint,
|
|
169
|
+
"attempts": attempt_log,
|
|
170
|
+
"verification": "prompt_still_present_after_retries",
|
|
171
|
+
}
|