@team-agent/installer 0.1.11 → 0.2.1
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/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 +137 -0
- package/src/team_agent/cli/commands.py +339 -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 +477 -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 +334 -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/paste_buffer_hygiene.py +39 -0
- package/src/team_agent/lifecycle/start.py +363 -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 +138 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +323 -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/owner_bypass.py +29 -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 +428 -0
- package/src/team_agent/messaging/send.py +500 -0
- package/src/team_agent/messaging/session_drift.py +94 -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 +809 -5892
- 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 +218 -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 -858
- 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": {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from team_agent.approvals.parsing import capture_has_approval_prompt
|
|
10
|
+
from team_agent.events import EventLog
|
|
11
|
+
from team_agent.message_store import MessageStore
|
|
12
|
+
from team_agent.state import team_state_key
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def refresh_agent_runtime_statuses(workspace: Path, state: dict[str, Any], event_log: EventLog) -> None:
|
|
16
|
+
from team_agent.runtime import _tmux_session_exists, _tmux_window_exists
|
|
17
|
+
_ = workspace
|
|
18
|
+
session_name = state.get("session_name")
|
|
19
|
+
tmux_exists = _tmux_session_exists(session_name) if session_name else False
|
|
20
|
+
for agent_id, agent_state in state.get("agents", {}).items():
|
|
21
|
+
if agent_state.get("status") in {"paused", "stopped"}:
|
|
22
|
+
continue
|
|
23
|
+
old_status = agent_state.get("status")
|
|
24
|
+
window = agent_state.get("window", agent_id)
|
|
25
|
+
window_present = _tmux_window_exists(session_name, window) if tmux_exists else False
|
|
26
|
+
agent_state["tmux_window_present"] = window_present
|
|
27
|
+
if not window_present:
|
|
28
|
+
if session_name:
|
|
29
|
+
agent_state["status"] = "missing"
|
|
30
|
+
else:
|
|
31
|
+
detected = detect_provider_status(agent_state["provider"], session_name, window)
|
|
32
|
+
if detected:
|
|
33
|
+
agent_state["status"] = detected
|
|
34
|
+
else:
|
|
35
|
+
agent_state.setdefault("status", "running")
|
|
36
|
+
if old_status != agent_state.get("status"):
|
|
37
|
+
event_log.write(
|
|
38
|
+
"runtime.status_detected",
|
|
39
|
+
agent_id=agent_id,
|
|
40
|
+
provider=agent_state.get("provider"),
|
|
41
|
+
old_status=old_status,
|
|
42
|
+
status=agent_state.get("status"),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def sync_agent_health(workspace: Path, state: dict[str, Any], store: MessageStore | None = None) -> dict[str, dict[str, Any]]:
|
|
47
|
+
from team_agent.runtime import _tmux_window_exists, _tmux_pane_info, run_cmd
|
|
48
|
+
from team_agent.messaging.activity_detector import classify_agent_activity
|
|
49
|
+
store = store or MessageStore(workspace)
|
|
50
|
+
session_name = state.get("session_name")
|
|
51
|
+
owner_team_id = team_state_key(state)
|
|
52
|
+
captures: dict[str, dict[str, Any]] = {}
|
|
53
|
+
for agent_id, agent_state in state.get("agents", {}).items():
|
|
54
|
+
health_status = agent_health_status(agent_state)
|
|
55
|
+
last_output_at = agent_state.get("last_output_at")
|
|
56
|
+
window = agent_state.get("window", agent_id)
|
|
57
|
+
scrollback = ""
|
|
58
|
+
pane_info: dict[str, Any] | None = None
|
|
59
|
+
if session_name and _tmux_window_exists(session_name, window):
|
|
60
|
+
proc = run_cmd(["tmux", "capture-pane", "-p", "-S", "-40", "-t", f"{session_name}:{window}"], timeout=5)
|
|
61
|
+
if proc.returncode == 0:
|
|
62
|
+
scrollback = proc.stdout
|
|
63
|
+
digest = hashlib.sha256(proc.stdout.encode("utf-8", errors="ignore")).hexdigest()
|
|
64
|
+
if digest != agent_state.get("last_output_hash"):
|
|
65
|
+
last_output_at = datetime.now(timezone.utc).isoformat()
|
|
66
|
+
agent_state["last_output_hash"] = digest
|
|
67
|
+
agent_state["last_output_at"] = last_output_at
|
|
68
|
+
if capture_has_approval_prompt(proc.stdout):
|
|
69
|
+
health_status = "AWAITING_APPROVAL"
|
|
70
|
+
try:
|
|
71
|
+
pane_info = _tmux_pane_info(f"{session_name}:{window}")
|
|
72
|
+
except Exception:
|
|
73
|
+
pane_info = None
|
|
74
|
+
if scrollback and health_status != "AWAITING_APPROVAL":
|
|
75
|
+
activity = classify_agent_activity(
|
|
76
|
+
agent_id,
|
|
77
|
+
agent_state.get("provider") or "",
|
|
78
|
+
last_output_at,
|
|
79
|
+
pane_info,
|
|
80
|
+
scrollback,
|
|
81
|
+
)
|
|
82
|
+
agent_state["activity"] = {
|
|
83
|
+
"status": activity.get("status"),
|
|
84
|
+
"confidence": activity.get("confidence"),
|
|
85
|
+
"rationale": activity.get("rationale"),
|
|
86
|
+
"classified_at": datetime.now(timezone.utc).isoformat(),
|
|
87
|
+
}
|
|
88
|
+
if activity.get("confidence", 0) >= 0.85:
|
|
89
|
+
raw = str(activity.get("status") or "")
|
|
90
|
+
mapping = {"idle": "IDLE", "working": "WORKING", "stuck": "STUCK", "uncertain": "UNCERTAIN"}
|
|
91
|
+
mapped = mapping.get(raw)
|
|
92
|
+
if mapped:
|
|
93
|
+
health_status = mapped
|
|
94
|
+
current_task = current_task_for_agent(state.get("tasks", []), agent_id)
|
|
95
|
+
store.upsert_agent_health(
|
|
96
|
+
agent_id,
|
|
97
|
+
health_status,
|
|
98
|
+
last_output_at=last_output_at,
|
|
99
|
+
context_usage_pct=agent_state.get("context_usage_pct"),
|
|
100
|
+
current_task_id=current_task,
|
|
101
|
+
owner_team_id=owner_team_id,
|
|
102
|
+
)
|
|
103
|
+
captures[agent_id] = {"scrollback": scrollback, "pane_info": pane_info, "health_status": health_status}
|
|
104
|
+
return captures
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def agent_health_status(agent_state: dict[str, Any]) -> str:
|
|
108
|
+
raw = str(agent_state.get("status") or "").lower()
|
|
109
|
+
if raw in {"busy", "running"}:
|
|
110
|
+
return "RUNNING" if raw == "busy" else "IDLE"
|
|
111
|
+
if raw in {"paused", "blocked"}:
|
|
112
|
+
return "BLOCKED"
|
|
113
|
+
if raw in {"error", "missing", "interrupted"}:
|
|
114
|
+
return "ERROR"
|
|
115
|
+
if raw in {"stopped", "done"}:
|
|
116
|
+
return "DONE"
|
|
117
|
+
return "IDLE"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def current_task_for_agent(tasks: list[dict[str, Any]], agent_id: str) -> str | None:
|
|
121
|
+
active = {"pending", "ready", "running", "blocked", "needs_retry"}
|
|
122
|
+
for task in reversed(tasks):
|
|
123
|
+
if task.get("assignee") == agent_id and task.get("status", "pending") in active:
|
|
124
|
+
return task.get("id")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def age_text(iso_text: str | None) -> str:
|
|
129
|
+
if not iso_text:
|
|
130
|
+
return "-"
|
|
131
|
+
try:
|
|
132
|
+
dt = datetime.fromisoformat(iso_text)
|
|
133
|
+
if dt.tzinfo is None:
|
|
134
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
135
|
+
seconds = max(0, int((datetime.now(timezone.utc) - dt).total_seconds()))
|
|
136
|
+
except ValueError:
|
|
137
|
+
return "-"
|
|
138
|
+
if seconds < 60:
|
|
139
|
+
return f"{seconds}s ago"
|
|
140
|
+
minutes = seconds // 60
|
|
141
|
+
if minutes < 60:
|
|
142
|
+
return f"{minutes}m ago"
|
|
143
|
+
return f"{minutes // 60}h ago"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def detect_provider_status(provider: str, session_name: str, window: str) -> str | None:
|
|
147
|
+
from team_agent.runtime import get_adapter, run_cmd
|
|
148
|
+
proc = run_cmd(["tmux", "capture-pane", "-p", "-t", f"{session_name}:{window}"], timeout=5)
|
|
149
|
+
if proc.returncode != 0:
|
|
150
|
+
return None
|
|
151
|
+
patterns = get_adapter(provider).status_patterns()
|
|
152
|
+
positions: dict[str, int] = {}
|
|
153
|
+
for status_name, pattern in patterns.items():
|
|
154
|
+
if not pattern:
|
|
155
|
+
continue
|
|
156
|
+
try:
|
|
157
|
+
matches = list(re.finditer(pattern, proc.stdout, re.MULTILINE))
|
|
158
|
+
except re.error:
|
|
159
|
+
continue
|
|
160
|
+
if matches:
|
|
161
|
+
positions[status_name] = matches[-1].start()
|
|
162
|
+
if not positions:
|
|
163
|
+
return None
|
|
164
|
+
latest = max(positions, key=positions.get)
|
|
165
|
+
return {"idle": "running", "processing": "busy", "error": "error"}.get(latest)
|