@team-agent/installer 0.1.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.
Files changed (36) hide show
  1. package/README.md +201 -0
  2. package/crates/team-agent-core/Cargo.toml +12 -0
  3. package/crates/team-agent-core/src/lib.rs +287 -0
  4. package/crates/team-agent-core/src/main.rs +152 -0
  5. package/examples/team.spec.yaml +206 -0
  6. package/examples/team_state.md +35 -0
  7. package/npm/install.mjs +266 -0
  8. package/package.json +28 -0
  9. package/pyproject.toml +18 -0
  10. package/schemas/result-envelope.schema.json +76 -0
  11. package/schemas/team.schema.json +241 -0
  12. package/scripts/install.py +88 -0
  13. package/scripts/run_regression_tests.py +79 -0
  14. package/skills/team-agent/SKILL.md +173 -0
  15. package/src/team_agent/__init__.py +3 -0
  16. package/src/team_agent/__main__.py +5 -0
  17. package/src/team_agent/cli.py +857 -0
  18. package/src/team_agent/compiler.py +269 -0
  19. package/src/team_agent/coordinator.py +62 -0
  20. package/src/team_agent/errors.py +10 -0
  21. package/src/team_agent/events.py +37 -0
  22. package/src/team_agent/fake_worker.py +80 -0
  23. package/src/team_agent/mcp_server.py +579 -0
  24. package/src/team_agent/message_store.py +497 -0
  25. package/src/team_agent/paths.py +45 -0
  26. package/src/team_agent/permissions.py +123 -0
  27. package/src/team_agent/profiles.py +882 -0
  28. package/src/team_agent/providers.py +1045 -0
  29. package/src/team_agent/routing.py +84 -0
  30. package/src/team_agent/runtime.py +5213 -0
  31. package/src/team_agent/rust_core.py +156 -0
  32. package/src/team_agent/simple_yaml.py +236 -0
  33. package/src/team_agent/spec.py +308 -0
  34. package/src/team_agent/state.py +112 -0
  35. package/src/team_agent/task_graph.py +80 -0
  36. package/templates/team_state.md +32 -0
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from team_agent.paths import repo_root
11
+
12
+
13
+ def core_binary() -> Path | None:
14
+ configured = shutil.which("team-agent-core")
15
+ if configured:
16
+ return Path(configured)
17
+ local = repo_root() / "crates" / "team-agent-core" / "target" / "debug" / "team-agent-core"
18
+ if local.exists():
19
+ return local
20
+ return None
21
+
22
+
23
+ def call_core(command: str, payload: dict[str, Any] | str | None = None) -> dict[str, Any]:
24
+ binary = core_binary()
25
+ if not binary:
26
+ return {"ok": False, "error": "team-agent-core binary not found", "fallback": True}
27
+ raw = json.dumps(payload, ensure_ascii=False) if isinstance(payload, dict) else (payload or "")
28
+ proc = subprocess.run(
29
+ [str(binary), command, "--json"],
30
+ input=raw,
31
+ text=True,
32
+ capture_output=True,
33
+ timeout=10,
34
+ check=False,
35
+ )
36
+ try:
37
+ result = json.loads(proc.stdout or "{}")
38
+ except json.JSONDecodeError:
39
+ result = {"ok": False, "error": proc.stdout.strip() or proc.stderr.strip()}
40
+ if proc.returncode != 0:
41
+ result.setdefault("ok", False)
42
+ result.setdefault("error", proc.stderr.strip() or "team-agent-core failed")
43
+ result["engine"] = "rust" if result.get("ok") else "rust_failed"
44
+ return result
45
+
46
+
47
+ def render_message(payload: dict[str, Any]) -> dict[str, Any]:
48
+ result = call_core("render-message", payload)
49
+ if result.get("ok"):
50
+ return result
51
+ sender = payload.get("from") or payload.get("sender") or "unknown"
52
+ task_id = payload.get("task_id")
53
+ content = payload.get("content") or ""
54
+ token = payload.get("message_id") or "missing"
55
+ header = f"Team Agent message from {sender}"
56
+ if task_id:
57
+ header += f" for {task_id}"
58
+ return {
59
+ "ok": True,
60
+ "text": f"{header}:\n\n{content}\n\n[team-agent-token:{token}]",
61
+ "token": token,
62
+ "engine": "python_fallback",
63
+ "fallback_reason": result.get("error"),
64
+ }
65
+
66
+
67
+ def redact_text(text: str) -> dict[str, Any]:
68
+ result = call_core("redact", {"text": text})
69
+ if result.get("ok"):
70
+ return result
71
+ redacted = []
72
+ for chunk in text.split():
73
+ lower = chunk.lower()
74
+ if (
75
+ "api_key" in lower
76
+ or "apikey" in lower
77
+ or "token=" in lower
78
+ or "secret" in lower
79
+ or lower == "bearer"
80
+ or chunk.startswith("sk-")
81
+ or _looks_base64_secret(chunk)
82
+ ):
83
+ redacted.append("[REDACTED]")
84
+ else:
85
+ redacted.append(chunk)
86
+ return {"ok": True, "text": " ".join(redacted), "engine": "python_fallback", "fallback_reason": result.get("error")}
87
+
88
+
89
+ def validate_profile_metadata(profile: dict[str, Any]) -> dict[str, Any]:
90
+ result = call_core("validate-profile", profile)
91
+ if result.get("ok") or result.get("errors"):
92
+ return result
93
+ errors: list[str] = []
94
+ if profile.get("auth_mode") not in {"subscription", "official_api", "compatible_api"}:
95
+ errors.append("auth_mode must be subscription, official_api, or compatible_api")
96
+ for field in ["provider", "model", "profile"]:
97
+ if not profile.get(field):
98
+ errors.append(f"{field} must not be empty")
99
+ if contains_inline_secret(str(profile.get(field) or "")):
100
+ errors.append("profile metadata contains a probable inline secret")
101
+ return {"ok": not errors, "errors": errors, "engine": "python_fallback", "fallback_reason": result.get("error")}
102
+
103
+
104
+ def list_targets() -> dict[str, Any]:
105
+ result = call_core("list-targets")
106
+ if result.get("ok"):
107
+ return result
108
+ proc = subprocess.run(
109
+ [
110
+ "tmux",
111
+ "list-panes",
112
+ "-a",
113
+ "-F",
114
+ "#{pane_id}\t#{session_name}\t#{window_index}\t#{window_name}\t#{pane_index}\t#{pane_tty}\t#{pane_current_command}\t#{pane_active}",
115
+ ],
116
+ text=True,
117
+ capture_output=True,
118
+ timeout=5,
119
+ check=False,
120
+ )
121
+ if proc.returncode != 0:
122
+ return {"ok": False, "error": proc.stderr.strip() or "tmux list-panes failed", "engine": "python_fallback"}
123
+ targets = []
124
+ for line in proc.stdout.splitlines():
125
+ parts = line.split("\t")
126
+ if len(parts) != 8:
127
+ continue
128
+ target = {
129
+ "pane_id": parts[0],
130
+ "session_name": parts[1],
131
+ "window_index": parts[2],
132
+ "window_name": parts[3],
133
+ "pane_index": parts[4],
134
+ "pane_tty": parts[5],
135
+ "pane_current_command": parts[6],
136
+ "pane_active": parts[7] == "1",
137
+ }
138
+ target["fingerprint"] = f"{target['session_name']}|{target['window_index']}|{target['pane_index']}|{target['pane_tty']}"
139
+ targets.append(target)
140
+ return {"ok": True, "targets": targets, "engine": "python_fallback", "fallback_reason": result.get("error")}
141
+
142
+
143
+ def contains_inline_secret(value: str) -> bool:
144
+ lower = value.lower()
145
+ return (
146
+ "api_key" in lower
147
+ or "apikey" in lower
148
+ or "token" in lower
149
+ or "secret" in lower
150
+ or value.startswith("sk-")
151
+ or _looks_base64_secret(value)
152
+ )
153
+
154
+
155
+ def _looks_base64_secret(value: str) -> bool:
156
+ return len(value) >= 32 and re.fullmatch(r"[A-Za-z0-9+/=_-]+", value) is not None
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ from typing import Any
6
+
7
+
8
+ def loads(text: str) -> Any:
9
+ stripped = text.lstrip()
10
+ if stripped.startswith("{") or stripped.startswith("["):
11
+ return json.loads(text)
12
+ lines = text.splitlines()
13
+ value, index = _parse_block(lines, 0, 0)
14
+ while index < len(lines) and not _content(lines[index]):
15
+ index += 1
16
+ if index != len(lines):
17
+ raise ValueError(f"unexpected content at line {index + 1}: {lines[index]}")
18
+ return value
19
+
20
+
21
+ def dumps(value: Any, indent: int = 0) -> str:
22
+ lines = _dump(value, indent)
23
+ return "\n".join(lines) + "\n"
24
+
25
+
26
+ def _parse_block(lines: list[str], index: int, indent: int) -> tuple[Any, int]:
27
+ index = _skip_blank(lines, index)
28
+ if index >= len(lines):
29
+ return None, index
30
+ current_indent = _indent(lines[index])
31
+ if current_indent < indent:
32
+ return None, index
33
+ if _stripped(lines[index]).startswith("- "):
34
+ return _parse_list(lines, index, current_indent)
35
+ return _parse_dict(lines, index, current_indent)
36
+
37
+
38
+ def _parse_dict(lines: list[str], index: int, indent: int) -> tuple[dict[str, Any], int]:
39
+ obj: dict[str, Any] = {}
40
+ while index < len(lines):
41
+ if not _content(lines[index]):
42
+ index += 1
43
+ continue
44
+ line_indent = _indent(lines[index])
45
+ if line_indent < indent:
46
+ break
47
+ if line_indent > indent:
48
+ raise ValueError(f"unexpected indentation at line {index + 1}: {lines[index]}")
49
+ stripped = _stripped(lines[index])
50
+ if stripped.startswith("- "):
51
+ break
52
+ key, raw = _split_key_value(stripped, index)
53
+ if raw == "|":
54
+ value, index = _parse_block_scalar(lines, index + 1, indent + 2)
55
+ elif raw == "":
56
+ value, index = _parse_block(lines, index + 1, indent + 2)
57
+ else:
58
+ value = _parse_scalar(raw)
59
+ index += 1
60
+ obj[key] = value
61
+ return obj, index
62
+
63
+
64
+ def _parse_list(lines: list[str], index: int, indent: int) -> tuple[list[Any], int]:
65
+ items: list[Any] = []
66
+ while index < len(lines):
67
+ if not _content(lines[index]):
68
+ index += 1
69
+ continue
70
+ line_indent = _indent(lines[index])
71
+ if line_indent < indent:
72
+ break
73
+ if line_indent != indent:
74
+ raise ValueError(f"unexpected list indentation at line {index + 1}: {lines[index]}")
75
+ stripped = _stripped(lines[index])
76
+ if not stripped.startswith("- "):
77
+ break
78
+ item_text = stripped[2:].strip()
79
+ if item_text == "":
80
+ value, index = _parse_block(lines, index + 1, indent + 2)
81
+ items.append(value)
82
+ continue
83
+ if _looks_like_key_value(item_text):
84
+ key, raw = _split_key_value(item_text, index)
85
+ item: dict[str, Any] = {}
86
+ if raw == "|":
87
+ value, next_index = _parse_block_scalar(lines, index + 1, indent + 2)
88
+ elif raw == "":
89
+ value, next_index = _parse_block(lines, index + 1, indent + 2)
90
+ else:
91
+ value = _parse_scalar(raw)
92
+ next_index = index + 1
93
+ item[key] = value
94
+ if next_index < len(lines) and _indent(lines[next_index]) == indent + 2:
95
+ extra, next_index = _parse_dict(lines, next_index, indent + 2)
96
+ item.update(extra)
97
+ items.append(item)
98
+ index = next_index
99
+ else:
100
+ items.append(_parse_scalar(item_text))
101
+ index += 1
102
+ return items, index
103
+
104
+
105
+ def _parse_block_scalar(lines: list[str], index: int, indent: int) -> tuple[str, int]:
106
+ block: list[str] = []
107
+ while index < len(lines):
108
+ if not lines[index].strip():
109
+ block.append("")
110
+ index += 1
111
+ continue
112
+ line_indent = _indent(lines[index])
113
+ if line_indent < indent:
114
+ break
115
+ block.append(lines[index][indent:])
116
+ index += 1
117
+ return "\n".join(block).rstrip() + "\n", index
118
+
119
+
120
+ def _parse_scalar(raw: str) -> Any:
121
+ if raw in {"null", "Null", "NULL", "~"}:
122
+ return None
123
+ if raw in {"true", "True", "TRUE"}:
124
+ return True
125
+ if raw in {"false", "False", "FALSE"}:
126
+ return False
127
+ try:
128
+ return int(raw)
129
+ except ValueError:
130
+ pass
131
+ if raw.startswith("[") and raw.endswith("]"):
132
+ try:
133
+ return ast.literal_eval(raw)
134
+ except (SyntaxError, ValueError):
135
+ return raw
136
+ if raw == "{}":
137
+ return {}
138
+ if (raw.startswith('"') and raw.endswith('"')) or (raw.startswith("'") and raw.endswith("'")):
139
+ try:
140
+ return ast.literal_eval(raw)
141
+ except (SyntaxError, ValueError):
142
+ return raw[1:-1]
143
+ return raw
144
+
145
+
146
+ def _dump(value: Any, indent: int) -> list[str]:
147
+ pad = " " * indent
148
+ if isinstance(value, dict):
149
+ lines: list[str] = []
150
+ for key, item in value.items():
151
+ if item == []:
152
+ lines.append(f"{pad}{key}: []")
153
+ elif item == {}:
154
+ lines.append(f"{pad}{key}: {{}}")
155
+ elif isinstance(item, (dict, list)):
156
+ lines.append(f"{pad}{key}:")
157
+ lines.extend(_dump(item, indent + 2))
158
+ elif isinstance(item, str) and "\n" in item:
159
+ lines.append(f"{pad}{key}: |")
160
+ for block_line in item.rstrip("\n").splitlines():
161
+ lines.append(f"{pad} {block_line}")
162
+ else:
163
+ lines.append(f"{pad}{key}: {_format_scalar(item)}")
164
+ return lines
165
+ if isinstance(value, list):
166
+ lines = []
167
+ for item in value:
168
+ if isinstance(item, dict):
169
+ if not item:
170
+ lines.append(f"{pad}- {{}}")
171
+ continue
172
+ first = True
173
+ for key, child in item.items():
174
+ prefix = "- " if first else " "
175
+ if child == []:
176
+ lines.append(f"{pad}{prefix}{key}: []")
177
+ elif child == {}:
178
+ lines.append(f"{pad}{prefix}{key}: {{}}")
179
+ elif isinstance(child, (dict, list)):
180
+ lines.append(f"{pad}{prefix}{key}:")
181
+ lines.extend(_dump(child, indent + 4))
182
+ else:
183
+ lines.append(f"{pad}{prefix}{key}: {_format_scalar(child)}")
184
+ first = False
185
+ elif isinstance(item, list):
186
+ lines.append(f"{pad}-")
187
+ lines.extend(_dump(item, indent + 2))
188
+ else:
189
+ lines.append(f"{pad}- {_format_scalar(item)}")
190
+ return lines
191
+ return [f"{pad}{_format_scalar(value)}"]
192
+
193
+
194
+ def _format_scalar(value: Any) -> str:
195
+ if value is None:
196
+ return "null"
197
+ if value is True:
198
+ return "true"
199
+ if value is False:
200
+ return "false"
201
+ if isinstance(value, int):
202
+ return str(value)
203
+ return json.dumps(str(value), ensure_ascii=False)
204
+
205
+
206
+ def _split_key_value(stripped: str, index: int) -> tuple[str, str]:
207
+ if ":" not in stripped:
208
+ raise ValueError(f"expected key: value at line {index + 1}")
209
+ key, raw = stripped.split(":", 1)
210
+ return key.strip(), raw.strip()
211
+
212
+
213
+ def _looks_like_key_value(text: str) -> bool:
214
+ if ":" not in text:
215
+ return False
216
+ key = text.split(":", 1)[0]
217
+ return bool(key) and all(ch.isalnum() or ch in "_-" for ch in key)
218
+
219
+
220
+ def _content(line: str) -> bool:
221
+ stripped = line.strip()
222
+ return bool(stripped) and not stripped.startswith("#")
223
+
224
+
225
+ def _skip_blank(lines: list[str], index: int) -> int:
226
+ while index < len(lines) and not _content(lines[index]):
227
+ index += 1
228
+ return index
229
+
230
+
231
+ def _indent(line: str) -> int:
232
+ return len(line) - len(line.lstrip(" "))
233
+
234
+
235
+ def _stripped(line: str) -> str:
236
+ return line.strip()