agent-devkit 0.2.0 → 0.3.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/README.md +66 -13
- package/bin/agent.mjs +133 -7
- package/package.json +1 -1
- package/runtime/README.md +205 -13
- package/runtime/agent +31 -5
- package/runtime/agents/README.md +18 -0
- package/runtime/agents/contribution-reviewer/AGENTS.md +8 -0
- package/runtime/agents/contribution-reviewer/README.md +8 -0
- package/runtime/agents/contribution-reviewer/agent.yaml +40 -0
- package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/capability.yaml +27 -0
- package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/decision-rules.md +5 -0
- package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/workflow.md +6 -0
- package/runtime/agents/contribution-reviewer/capabilities/review-contribution/capability.yaml +25 -0
- package/runtime/agents/contribution-reviewer/capabilities/review-contribution/decision-rules.md +5 -0
- package/runtime/agents/contribution-reviewer/capabilities/review-contribution/workflow.md +5 -0
- package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/capability.yaml +26 -0
- package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/decision-rules.md +5 -0
- package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/workflow.md +6 -0
- package/runtime/agents/contribution-reviewer/infra/README.md +6 -0
- package/runtime/agents/contribution-reviewer/knowledge/context.md +8 -0
- package/runtime/agents/contribution-reviewer/knowledge/system.md +8 -0
- package/runtime/agents/contribution-reviewer/templates/README.md +3 -0
- package/runtime/agents/knowledge-author/AGENTS.md +7 -0
- package/runtime/agents/knowledge-author/README.md +7 -0
- package/runtime/agents/knowledge-author/agent.yaml +37 -0
- package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/capability.yaml +30 -0
- package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/decision-rules.md +6 -0
- package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/workflow.md +7 -0
- package/runtime/agents/knowledge-author/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-author/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-author/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-author/templates/.gitkeep +1 -0
- package/runtime/agents/knowledge-curator/AGENTS.md +7 -0
- package/runtime/agents/knowledge-curator/README.md +6 -0
- package/runtime/agents/knowledge-curator/agent.yaml +37 -0
- package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/capability.yaml +29 -0
- package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/decision-rules.md +6 -0
- package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/workflow.md +7 -0
- package/runtime/agents/knowledge-curator/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-curator/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-curator/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-curator/templates/.gitkeep +1 -0
- package/runtime/agents/knowledge-infra-builder/AGENTS.md +8 -0
- package/runtime/agents/knowledge-infra-builder/README.md +8 -0
- package/runtime/agents/knowledge-infra-builder/agent.yaml +38 -0
- package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/capability.yaml +30 -0
- package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/decision-rules.md +6 -0
- package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/workflow.md +7 -0
- package/runtime/agents/knowledge-infra-builder/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-infra-builder/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-infra-builder/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-infra-builder/templates/.gitkeep +1 -0
- package/runtime/agents/knowledge-owner/AGENTS.md +7 -0
- package/runtime/agents/knowledge-owner/README.md +6 -0
- package/runtime/agents/knowledge-owner/agent.yaml +37 -0
- package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/capability.yaml +28 -0
- package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/decision-rules.md +6 -0
- package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/workflow.md +7 -0
- package/runtime/agents/knowledge-owner/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-owner/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-owner/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-owner/templates/.gitkeep +1 -0
- package/runtime/agents/knowledge-reviewer/AGENTS.md +7 -0
- package/runtime/agents/knowledge-reviewer/README.md +7 -0
- package/runtime/agents/knowledge-reviewer/agent.yaml +36 -0
- package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/capability.yaml +26 -0
- package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/decision-rules.md +6 -0
- package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/workflow.md +7 -0
- package/runtime/agents/knowledge-reviewer/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-reviewer/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-reviewer/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-reviewer/templates/.gitkeep +1 -0
- package/runtime/agents/local-memory-manager/AGENTS.md +5 -0
- package/runtime/agents/local-memory-manager/README.md +7 -0
- package/runtime/agents/local-memory-manager/agent.yaml +38 -0
- package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/capability.yaml +19 -0
- package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/decision-rules.md +5 -0
- package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/workflow.md +6 -0
- package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/capability.yaml +19 -0
- package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/decision-rules.md +5 -0
- package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/workflow.md +5 -0
- package/runtime/agents/local-memory-manager/infra/.gitkeep +1 -0
- package/runtime/agents/local-memory-manager/knowledge/context.md +4 -0
- package/runtime/agents/local-memory-manager/knowledge/system.md +4 -0
- package/runtime/agents/local-memory-manager/templates/.gitkeep +1 -0
- package/runtime/agents/memory-sync-manager/AGENTS.md +7 -0
- package/runtime/agents/memory-sync-manager/README.md +7 -0
- package/runtime/agents/memory-sync-manager/agent.yaml +37 -0
- package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/capability.yaml +29 -0
- package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/decision-rules.md +6 -0
- package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/workflow.md +7 -0
- package/runtime/agents/memory-sync-manager/infra/.gitkeep +1 -0
- package/runtime/agents/memory-sync-manager/knowledge/context.md +4 -0
- package/runtime/agents/memory-sync-manager/knowledge/system.md +4 -0
- package/runtime/agents/memory-sync-manager/templates/.gitkeep +1 -0
- package/runtime/agents/shared-memory-curator/AGENTS.md +5 -0
- package/runtime/agents/shared-memory-curator/README.md +6 -0
- package/runtime/agents/shared-memory-curator/agent.yaml +38 -0
- package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/capability.yaml +19 -0
- package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/decision-rules.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/workflow.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/capability.yaml +19 -0
- package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/decision-rules.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/workflow.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/capability.yaml +19 -0
- package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/decision-rules.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/workflow.md +5 -0
- package/runtime/agents/shared-memory-curator/infra/.gitkeep +1 -0
- package/runtime/agents/shared-memory-curator/knowledge/context.md +5 -0
- package/runtime/agents/shared-memory-curator/knowledge/system.md +4 -0
- package/runtime/agents/shared-memory-curator/templates/.gitkeep +1 -0
- package/runtime/cli/README.md +47 -8
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/agent_registry.py +4 -2
- package/runtime/cli/aikit/agentic_commands.py +158 -0
- package/runtime/cli/aikit/app_home.py +2 -0
- package/runtime/cli/aikit/audit.py +16 -6
- package/runtime/cli/aikit/catalog.py +278 -8
- package/runtime/cli/aikit/cli_dispatch.py +489 -13
- package/runtime/cli/aikit/cli_parser.py +146 -8
- package/runtime/cli/aikit/contribution.py +132 -2
- package/runtime/cli/aikit/doctor_runtime.py +85 -0
- package/runtime/cli/aikit/embedded_mini_brain.py +351 -0
- package/runtime/cli/aikit/eval.py +356 -10
- package/runtime/cli/aikit/human_output.py +310 -4
- package/runtime/cli/aikit/interactive_wizard.py +146 -0
- package/runtime/cli/aikit/knowledge_base.py +1067 -0
- package/runtime/cli/aikit/llm.py +40 -6
- package/runtime/cli/aikit/local_artifacts.py +444 -0
- package/runtime/cli/aikit/local_llm.py +176 -0
- package/runtime/cli/aikit/local_llm_operator.py +15 -5
- package/runtime/cli/aikit/main.py +15 -0
- package/runtime/cli/aikit/mcp_manifest.py +798 -0
- package/runtime/cli/aikit/mcp_tools.py +643 -5
- package/runtime/cli/aikit/memory.py +405 -0
- package/runtime/cli/aikit/mini_brain.py +56 -25
- package/runtime/cli/aikit/model_router.py +42 -9
- package/runtime/cli/aikit/natural_prompt_runtime.py +194 -2
- package/runtime/cli/aikit/ollama.py +64 -15
- package/runtime/cli/aikit/onboarding.py +551 -0
- package/runtime/cli/aikit/output.py +67 -0
- package/runtime/cli/aikit/prompt_injection.py +12 -1
- package/runtime/cli/aikit/review_gate.py +14 -2
- package/runtime/cli/aikit/roadmap_cli.py +1 -1
- package/runtime/cli/aikit/secrets.py +3 -2
- package/runtime/cli/aikit/setup_wizard_payload.py +3 -0
- package/runtime/cli/aikit/shared_memory.py +415 -0
- package/runtime/cli/aikit/specialist_readiness.py +152 -0
- package/runtime/cli/aikit/tasks.py +104 -1
- package/runtime/cli/aikit/team.py +380 -0
- package/runtime/cli/aikit/toolchain.py +7 -2
- package/runtime/cli/aikit/workflows.py +115 -14
- package/runtime/models/qwen2.5-0.5b-instruct/manifest.json +30 -0
- package/runtime/providers/knowledge-github.yaml +40 -0
- package/runtime/providers/knowledge-google-drive.yaml +32 -0
- package/runtime/providers/knowledge-local.yaml +26 -0
- package/runtime/providers/knowledge-notion.yaml +32 -0
- package/runtime/providers/knowledge-obsidian.yaml +24 -0
- package/runtime/providers/knowledge-onedrive.yaml +36 -0
- package/runtime/providers/knowledge-s3.yaml +45 -0
- package/runtime/providers/knowledge-sharepoint.yaml +39 -0
- package/runtime/providers/knowledge-supabase.yaml +43 -0
- package/runtime/providers/knowledge-vector.yaml +39 -0
- package/runtime/requirements.txt +6 -0
- package/runtime/scripts/docker-cli-qa.sh +453 -0
- package/runtime/scripts/release-catalog-snapshot.json +55 -4
- package/runtime/scripts/release-gate.py +54 -13
- package/runtime/tooling/toolchain.yaml +92 -0
- package/runtime/vendor/skills/napkin/napkin.md +21 -7
- package/runtime/workflows/azure-card-analysis/README.md +3 -0
- package/runtime/workflows/azure-card-analysis/workflow.yaml +30 -0
- package/runtime/workflows/daily-pr-review/README.md +3 -0
- package/runtime/workflows/daily-pr-review/workflow.yaml +31 -0
- package/runtime/workflows/incident-analysis/README.md +3 -0
- package/runtime/workflows/incident-analysis/workflow.yaml +33 -0
- package/runtime/workflows/release-prep/README.md +3 -0
- package/runtime/workflows/release-prep/workflow.yaml +30 -0
|
@@ -100,7 +100,7 @@ def create_task(
|
|
|
100
100
|
"created_at": created_at,
|
|
101
101
|
"updated_at": created_at,
|
|
102
102
|
"schedule": schedule or {"type": "manual"},
|
|
103
|
-
"action": action or {"type": "prompt", "prompt": redact_secrets(prompt
|
|
103
|
+
"action": action or ({"type": "prompt", "prompt": redact_secrets(prompt)} if prompt else {"type": "noop"}),
|
|
104
104
|
"permissions": permissions or {"mode": "report-only"},
|
|
105
105
|
"notifications": notifications or [{"type": "terminal"}],
|
|
106
106
|
"run_count": 0,
|
|
@@ -231,6 +231,41 @@ def run_task(
|
|
|
231
231
|
payload["events"] = scheduler_events + [scheduler_event_summary(payload)]
|
|
232
232
|
payload["events_path"] = str(scheduler_events_path())
|
|
233
233
|
return payload
|
|
234
|
+
action_result = execute_task_action(task, origin=origin)
|
|
235
|
+
action_ok = bool(action_result.get("ok")) or action_result.get("status") == "ok"
|
|
236
|
+
if not action_ok:
|
|
237
|
+
append_history(str(task["id"]), f"Task `{task['id']}` failed: {action_result.get('message') or action_result.get('status')}")
|
|
238
|
+
payload = {
|
|
239
|
+
"kind": "task-run",
|
|
240
|
+
"status": "failed",
|
|
241
|
+
"ok": False,
|
|
242
|
+
"dry_run": False,
|
|
243
|
+
"task": public_task(task),
|
|
244
|
+
"preview": preview,
|
|
245
|
+
"autonomy_contract": preview["autonomy_contract"],
|
|
246
|
+
"result": action_result,
|
|
247
|
+
"message": action_result.get("message") or "Task action failed.",
|
|
248
|
+
"exit_code": int(action_result.get("exit_code") or 1),
|
|
249
|
+
}
|
|
250
|
+
if scheduler_origin:
|
|
251
|
+
finished_at = now_iso()
|
|
252
|
+
attach_scheduler_metadata(
|
|
253
|
+
payload,
|
|
254
|
+
task,
|
|
255
|
+
event="scheduled_task.failed",
|
|
256
|
+
run_id=run_id,
|
|
257
|
+
scheduled_for=scheduled_for,
|
|
258
|
+
started_at=started_at,
|
|
259
|
+
finished_at=finished_at,
|
|
260
|
+
summary="Scheduled task action failed.",
|
|
261
|
+
)
|
|
262
|
+
attach_scheduler_audit(payload, task)
|
|
263
|
+
attach_task_notification(payload, task, event_name(origin, "failed"), origin=origin)
|
|
264
|
+
if scheduler_origin:
|
|
265
|
+
record_scheduler_event(payload)
|
|
266
|
+
payload["events"] = scheduler_events + [scheduler_event_summary(payload)]
|
|
267
|
+
payload["events_path"] = str(scheduler_events_path())
|
|
268
|
+
return payload
|
|
234
269
|
task["run_count"] = int(task.get("run_count") or 0) + 1
|
|
235
270
|
task["last_run_at"] = now_iso()
|
|
236
271
|
task["updated_at"] = task["last_run_at"]
|
|
@@ -244,6 +279,8 @@ def run_task(
|
|
|
244
279
|
"task": public_task(task),
|
|
245
280
|
"preview": preview,
|
|
246
281
|
"autonomy_contract": preview["autonomy_contract"],
|
|
282
|
+
"result": action_result,
|
|
283
|
+
"response": action_result.get("response"),
|
|
247
284
|
}
|
|
248
285
|
if scheduler_origin:
|
|
249
286
|
finished_at = now_iso()
|
|
@@ -368,6 +405,72 @@ def event_name(origin: str, outcome: str) -> str:
|
|
|
368
405
|
return f"{namespace}.{outcome}"
|
|
369
406
|
|
|
370
407
|
|
|
408
|
+
def execute_task_action(task: dict[str, Any], *, origin: str) -> dict[str, Any]:
|
|
409
|
+
action = task.get("action") if isinstance(task.get("action"), dict) else {}
|
|
410
|
+
action_type = str(action.get("type") or "prompt")
|
|
411
|
+
if action_type == "noop":
|
|
412
|
+
return {
|
|
413
|
+
"kind": "task-action",
|
|
414
|
+
"status": "ok",
|
|
415
|
+
"ok": True,
|
|
416
|
+
"message": "No-op local task completed.",
|
|
417
|
+
}
|
|
418
|
+
if action_type == "prompt":
|
|
419
|
+
prompt = str(action.get("prompt") or "").strip()
|
|
420
|
+
if not prompt:
|
|
421
|
+
return {
|
|
422
|
+
"kind": "task-action",
|
|
423
|
+
"status": "failed",
|
|
424
|
+
"ok": False,
|
|
425
|
+
"message": "Task prompt is empty.",
|
|
426
|
+
"exit_code": 2,
|
|
427
|
+
}
|
|
428
|
+
from cli.aikit.core.requests import AgentPromptRequest
|
|
429
|
+
from cli.aikit.natural_prompt_runtime import run_agent_prompt_request
|
|
430
|
+
|
|
431
|
+
return run_agent_prompt_request(
|
|
432
|
+
AgentPromptRequest(
|
|
433
|
+
prompt=prompt,
|
|
434
|
+
prog_name="agent",
|
|
435
|
+
project=None,
|
|
436
|
+
new_session=False,
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
if action_type == "capability":
|
|
440
|
+
agent_id = str(action.get("agent") or "").strip()
|
|
441
|
+
capability_id = str(action.get("capability") or "").strip()
|
|
442
|
+
if not agent_id or not capability_id:
|
|
443
|
+
return {
|
|
444
|
+
"kind": "task-action",
|
|
445
|
+
"status": "failed",
|
|
446
|
+
"ok": False,
|
|
447
|
+
"message": "Capability task requires agent and capability.",
|
|
448
|
+
"exit_code": 2,
|
|
449
|
+
}
|
|
450
|
+
args = action.get("args") if isinstance(action.get("args"), list) else []
|
|
451
|
+
inputs = action.get("inputs") if isinstance(action.get("inputs"), dict) else {}
|
|
452
|
+
if action.get("external_writes") is True and not args and not inputs:
|
|
453
|
+
return {
|
|
454
|
+
"kind": "task-action",
|
|
455
|
+
"status": "ok",
|
|
456
|
+
"ok": True,
|
|
457
|
+
"agent": agent_id,
|
|
458
|
+
"capability": capability_id,
|
|
459
|
+
"message": "External-write task permission was granted; no capability args were supplied, so no external action was executed.",
|
|
460
|
+
"external_action_executed": False,
|
|
461
|
+
}
|
|
462
|
+
from cli.aikit.capability_runtime import load_agent, run_capability
|
|
463
|
+
|
|
464
|
+
return run_capability(load_agent(agent_id), capability_id, [str(item) for item in args], capture_output=True, origin=origin)
|
|
465
|
+
return {
|
|
466
|
+
"kind": "task-action",
|
|
467
|
+
"status": "failed",
|
|
468
|
+
"ok": False,
|
|
469
|
+
"message": f"Unsupported task action type: {action_type}",
|
|
470
|
+
"exit_code": 2,
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
371
474
|
def new_run_id() -> str:
|
|
372
475
|
return f"run_{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
|
|
373
476
|
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""Project-local team profile support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from cli.aikit.errors import DevKitError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TEAM_SCHEMA_VERSION = "agent-devkit.team/v1"
|
|
14
|
+
TEAM_DIR = ".agent-devkit"
|
|
15
|
+
TEAM_FILE = "team.yaml"
|
|
16
|
+
SECRET_KEY_PATTERN = re.compile(r"(token|secret|password|api[_-]?key|pat|credential)", re.IGNORECASE)
|
|
17
|
+
SECRET_VALUE_PATTERN = re.compile(r"(sk-[A-Za-z0-9]|ghp_[A-Za-z0-9]|xox[baprs]-|-----BEGIN [A-Z ]*PRIVATE KEY-----)")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def team_init(project: Path | None = None, *, force: bool = False) -> dict[str, Any]:
|
|
21
|
+
path = team_profile_path(project)
|
|
22
|
+
if path.exists() and not force:
|
|
23
|
+
return team_status(project)
|
|
24
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
payload = default_team_payload()
|
|
26
|
+
write_team_payload(path, payload)
|
|
27
|
+
return {
|
|
28
|
+
"kind": "team",
|
|
29
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
30
|
+
"status": "initialized",
|
|
31
|
+
"path": str(path),
|
|
32
|
+
"profile": payload["active_profile"],
|
|
33
|
+
"secret_free": True,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def team_status(project: Path | None = None) -> dict[str, Any]:
|
|
38
|
+
path = team_profile_path(project)
|
|
39
|
+
if not path.exists():
|
|
40
|
+
return {
|
|
41
|
+
"kind": "team",
|
|
42
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
43
|
+
"status": "not-configured",
|
|
44
|
+
"path": str(path),
|
|
45
|
+
"active_profile": None,
|
|
46
|
+
"profiles": [],
|
|
47
|
+
"next_steps": ["Run `agent team init` to create a project-local team profile."],
|
|
48
|
+
}
|
|
49
|
+
payload = read_team_payload(path)
|
|
50
|
+
findings = secret_findings(payload)
|
|
51
|
+
profiles = payload.get("profiles") if isinstance(payload.get("profiles"), dict) else {}
|
|
52
|
+
active_profile = str(payload.get("active_profile") or "")
|
|
53
|
+
return {
|
|
54
|
+
"kind": "team",
|
|
55
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
56
|
+
"status": "ok" if active_profile in profiles and not findings else "blocked",
|
|
57
|
+
"path": str(path),
|
|
58
|
+
"active_profile": active_profile,
|
|
59
|
+
"profiles": sorted(profiles),
|
|
60
|
+
"secret_free": not findings,
|
|
61
|
+
"findings": findings,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def team_doctor(project: Path | None = None) -> dict[str, Any]:
|
|
66
|
+
status = team_status(project)
|
|
67
|
+
checks = [
|
|
68
|
+
{"id": "team-file-exists", "status": "passed" if status["status"] != "not-configured" else "failed"},
|
|
69
|
+
{"id": "active-profile-exists", "status": "passed" if status.get("active_profile") in set(status.get("profiles") or []) else "failed"},
|
|
70
|
+
{"id": "no-secret-material", "status": "passed" if status.get("secret_free") is not False else "failed"},
|
|
71
|
+
]
|
|
72
|
+
return {
|
|
73
|
+
"kind": "team-doctor",
|
|
74
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
75
|
+
"status": "ok" if all(check["status"] == "passed" for check in checks) else "blocked",
|
|
76
|
+
"team": status,
|
|
77
|
+
"checks": checks,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def team_onboard(project: Path | None = None) -> dict[str, Any]:
|
|
82
|
+
status = team_status(project)
|
|
83
|
+
if status["status"] == "not-configured":
|
|
84
|
+
return {
|
|
85
|
+
"kind": "team-onboarding",
|
|
86
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
87
|
+
"status": "needs-init",
|
|
88
|
+
"team": status,
|
|
89
|
+
"next_steps": ["Run `agent team init`.", "Review `.agent-devkit/team.yaml` before committing it."],
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
"kind": "team-onboarding",
|
|
93
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
94
|
+
"status": "ok" if status["status"] == "ok" else "blocked",
|
|
95
|
+
"team": status,
|
|
96
|
+
"next_steps": [
|
|
97
|
+
"Configure personal secret refs locally with `agent secret set ... --env ...`.",
|
|
98
|
+
"Run `agent doctor --project .` to validate personal and project setup.",
|
|
99
|
+
"Install desired team workflows locally with `agent workflow install <id> --dry-run` first.",
|
|
100
|
+
],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def team_profile_list(project: Path | None = None) -> dict[str, Any]:
|
|
105
|
+
payload = require_team_payload(project)
|
|
106
|
+
profiles = payload.get("profiles") if isinstance(payload.get("profiles"), dict) else {}
|
|
107
|
+
return {
|
|
108
|
+
"kind": "team-profiles",
|
|
109
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
110
|
+
"status": "ok",
|
|
111
|
+
"active_profile": payload.get("active_profile"),
|
|
112
|
+
"items": [public_profile(profile_id, profile) for profile_id, profile in sorted(profiles.items())],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def team_profile_show(profile_id: str | None, project: Path | None = None) -> dict[str, Any]:
|
|
117
|
+
payload = require_team_payload(project)
|
|
118
|
+
profile_id = profile_id or str(payload.get("active_profile") or "")
|
|
119
|
+
profile = require_profile(payload, profile_id)
|
|
120
|
+
return {
|
|
121
|
+
"kind": "team-profile",
|
|
122
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
123
|
+
"status": "ok",
|
|
124
|
+
"active": profile_id == payload.get("active_profile"),
|
|
125
|
+
"profile": public_profile(profile_id, profile, detailed=True),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def team_profile_use(profile_id: str | None, project: Path | None = None) -> dict[str, Any]:
|
|
130
|
+
if not profile_id:
|
|
131
|
+
raise DevKitError("team profile use requires a profile id")
|
|
132
|
+
path = team_profile_path(project)
|
|
133
|
+
payload = require_team_payload(project)
|
|
134
|
+
require_profile(payload, profile_id)
|
|
135
|
+
payload["active_profile"] = profile_id
|
|
136
|
+
write_team_payload(path, payload)
|
|
137
|
+
return team_profile_show(profile_id, project)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def team_profile_export(profile_id: str | None, destination: str | None, project: Path | None = None) -> dict[str, Any]:
|
|
141
|
+
payload = require_team_payload(project)
|
|
142
|
+
profile_id = profile_id or str(payload.get("active_profile") or "")
|
|
143
|
+
profile = require_profile(payload, profile_id)
|
|
144
|
+
export_payload = {"schema_version": TEAM_SCHEMA_VERSION, "profile_id": profile_id, "profile": profile}
|
|
145
|
+
findings = secret_findings(export_payload)
|
|
146
|
+
if findings:
|
|
147
|
+
raise DevKitError("team profile export blocked because profile contains secret-like material")
|
|
148
|
+
if not destination:
|
|
149
|
+
return {
|
|
150
|
+
"kind": "team-profile-export",
|
|
151
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
152
|
+
"status": "planned",
|
|
153
|
+
"profile_id": profile_id,
|
|
154
|
+
"payload": export_payload,
|
|
155
|
+
"writes": [],
|
|
156
|
+
}
|
|
157
|
+
target = Path(destination).expanduser().resolve()
|
|
158
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
write_team_payload(target, export_payload)
|
|
160
|
+
return {
|
|
161
|
+
"kind": "team-profile-export",
|
|
162
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
163
|
+
"status": "exported",
|
|
164
|
+
"profile_id": profile_id,
|
|
165
|
+
"path": str(target),
|
|
166
|
+
"writes": [str(target)],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def team_profile_import(source: str | None, project: Path | None = None) -> dict[str, Any]:
|
|
171
|
+
if not source:
|
|
172
|
+
raise DevKitError("team profile import requires a path")
|
|
173
|
+
source_path = Path(source).expanduser().resolve()
|
|
174
|
+
if not source_path.exists():
|
|
175
|
+
raise DevKitError(f"team profile import path not found: {source_path}")
|
|
176
|
+
imported = read_team_payload(source_path)
|
|
177
|
+
findings = secret_findings(imported)
|
|
178
|
+
if findings:
|
|
179
|
+
return {
|
|
180
|
+
"kind": "team-profile-import",
|
|
181
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
182
|
+
"status": "blocked",
|
|
183
|
+
"path": str(source_path),
|
|
184
|
+
"findings": findings,
|
|
185
|
+
}
|
|
186
|
+
profile_id = str(imported.get("profile_id") or imported.get("id") or source_path.stem)
|
|
187
|
+
profile = imported.get("profile") if isinstance(imported.get("profile"), dict) else imported
|
|
188
|
+
path = team_profile_path(project)
|
|
189
|
+
payload = default_team_payload() if not path.exists() else read_team_payload(path)
|
|
190
|
+
profiles = payload.setdefault("profiles", {})
|
|
191
|
+
if not isinstance(profiles, dict):
|
|
192
|
+
raise DevKitError("team profile registry is invalid")
|
|
193
|
+
profiles[profile_id] = profile
|
|
194
|
+
write_team_payload(path, payload)
|
|
195
|
+
return {
|
|
196
|
+
"kind": "team-profile-import",
|
|
197
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
198
|
+
"status": "imported",
|
|
199
|
+
"profile_id": profile_id,
|
|
200
|
+
"path": str(path),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def team_profile_path(project: Path | None = None) -> Path:
|
|
205
|
+
root = (project or Path.cwd()).resolve()
|
|
206
|
+
return root / TEAM_DIR / TEAM_FILE
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def default_team_payload() -> dict[str, Any]:
|
|
210
|
+
return {
|
|
211
|
+
"schema_version": TEAM_SCHEMA_VERSION,
|
|
212
|
+
"active_profile": "default",
|
|
213
|
+
"profiles": {
|
|
214
|
+
"default": {
|
|
215
|
+
"description": "Default project team profile",
|
|
216
|
+
"providers": [],
|
|
217
|
+
"sources": [],
|
|
218
|
+
"workflows": ["daily-pr-review"],
|
|
219
|
+
"permissions": {"default_mode": "report-only", "external_writes": "confirm"},
|
|
220
|
+
"local_llm": {"enabled": "personal", "max_context_chars": 6000},
|
|
221
|
+
"prompt_injection": {"external_content_policy": "quote-as-data"},
|
|
222
|
+
"secret_refs": [],
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def require_team_payload(project: Path | None = None) -> dict[str, Any]:
|
|
229
|
+
path = team_profile_path(project)
|
|
230
|
+
if not path.exists():
|
|
231
|
+
raise DevKitError("team profile not configured. Run `agent team init` first.")
|
|
232
|
+
return read_team_payload(path)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def require_profile(payload: dict[str, Any], profile_id: str) -> dict[str, Any]:
|
|
236
|
+
profiles = payload.get("profiles") if isinstance(payload.get("profiles"), dict) else {}
|
|
237
|
+
profile = profiles.get(profile_id)
|
|
238
|
+
if not isinstance(profile, dict):
|
|
239
|
+
raise DevKitError(f"team profile not found: {profile_id}")
|
|
240
|
+
return profile
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def public_profile(profile_id: str, profile: dict[str, Any], *, detailed: bool = False) -> dict[str, Any]:
|
|
244
|
+
item = {
|
|
245
|
+
"id": profile_id,
|
|
246
|
+
"description": profile.get("description"),
|
|
247
|
+
"providers": list_value(profile.get("providers")),
|
|
248
|
+
"sources": list_value(profile.get("sources")),
|
|
249
|
+
"workflows": list_value(profile.get("workflows")),
|
|
250
|
+
"secret_refs_count": len(list_value(profile.get("secret_refs"))),
|
|
251
|
+
}
|
|
252
|
+
if detailed:
|
|
253
|
+
item["permissions"] = profile.get("permissions") if isinstance(profile.get("permissions"), dict) else {}
|
|
254
|
+
item["local_llm"] = profile.get("local_llm") if isinstance(profile.get("local_llm"), dict) else {}
|
|
255
|
+
item["prompt_injection"] = profile.get("prompt_injection") if isinstance(profile.get("prompt_injection"), dict) else {}
|
|
256
|
+
return item
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def list_value(value: Any) -> list[Any]:
|
|
260
|
+
return value if isinstance(value, list) else []
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def read_team_payload(path: Path) -> dict[str, Any]:
|
|
264
|
+
text = path.read_text(encoding="utf-8")
|
|
265
|
+
try:
|
|
266
|
+
import yaml # type: ignore
|
|
267
|
+
|
|
268
|
+
payload = yaml.safe_load(text) or {}
|
|
269
|
+
except ImportError:
|
|
270
|
+
payload = read_simple_team_yaml(text)
|
|
271
|
+
if not isinstance(payload, dict):
|
|
272
|
+
raise DevKitError(f"team profile file is invalid: {path}")
|
|
273
|
+
return payload
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def write_team_payload(path: Path, payload: dict[str, Any]) -> None:
|
|
277
|
+
try:
|
|
278
|
+
import yaml # type: ignore
|
|
279
|
+
|
|
280
|
+
text = yaml.safe_dump(payload, allow_unicode=True, sort_keys=False)
|
|
281
|
+
except ImportError:
|
|
282
|
+
text = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
|
283
|
+
path.write_text(text, encoding="utf-8")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def read_simple_team_yaml(text: str) -> dict[str, Any]:
|
|
287
|
+
stripped = text.strip()
|
|
288
|
+
if not stripped:
|
|
289
|
+
return {}
|
|
290
|
+
if stripped.startswith("{"):
|
|
291
|
+
data = json.loads(text)
|
|
292
|
+
return data if isinstance(data, dict) else {}
|
|
293
|
+
lines = [
|
|
294
|
+
(len(raw_line) - len(raw_line.lstrip(" ")), raw_line.strip())
|
|
295
|
+
for raw_line in text.splitlines()
|
|
296
|
+
if raw_line.strip() and not raw_line.lstrip().startswith("#")
|
|
297
|
+
]
|
|
298
|
+
data, index = parse_simple_yaml_block(lines, 0, 0)
|
|
299
|
+
if index < len(lines):
|
|
300
|
+
raise DevKitError("team profile YAML fallback parser could not read the complete file")
|
|
301
|
+
return data if isinstance(data, dict) else {}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def parse_simple_yaml_block(lines: list[tuple[int, str]], index: int, indent: int) -> tuple[Any, int]:
|
|
305
|
+
if index < len(lines) and lines[index][0] == indent and lines[index][1].startswith("- "):
|
|
306
|
+
values: list[Any] = []
|
|
307
|
+
while index < len(lines) and lines[index][0] == indent and lines[index][1].startswith("- "):
|
|
308
|
+
values.append(parse_simple_scalar(lines[index][1][2:].strip()))
|
|
309
|
+
index += 1
|
|
310
|
+
return values, index
|
|
311
|
+
|
|
312
|
+
data: dict[str, Any] = {}
|
|
313
|
+
while index < len(lines):
|
|
314
|
+
line_indent, content = lines[index]
|
|
315
|
+
if line_indent < indent or content.startswith("- "):
|
|
316
|
+
break
|
|
317
|
+
if line_indent > indent:
|
|
318
|
+
raise DevKitError("team profile YAML fallback parser found unexpected indentation")
|
|
319
|
+
if ":" not in content:
|
|
320
|
+
raise DevKitError("team profile YAML fallback parser found an unsupported line")
|
|
321
|
+
key, raw_value = content.split(":", 1)
|
|
322
|
+
key = key.strip()
|
|
323
|
+
raw_value = raw_value.strip()
|
|
324
|
+
index += 1
|
|
325
|
+
if raw_value:
|
|
326
|
+
data[key] = parse_simple_scalar(raw_value)
|
|
327
|
+
continue
|
|
328
|
+
if index >= len(lines) or lines[index][0] < indent:
|
|
329
|
+
data[key] = {}
|
|
330
|
+
continue
|
|
331
|
+
if lines[index][0] == indent and lines[index][1].startswith("- "):
|
|
332
|
+
data[key], index = parse_simple_yaml_block(lines, index, indent)
|
|
333
|
+
continue
|
|
334
|
+
if lines[index][0] <= indent:
|
|
335
|
+
data[key] = {}
|
|
336
|
+
continue
|
|
337
|
+
data[key], index = parse_simple_yaml_block(lines, index, lines[index][0])
|
|
338
|
+
return data, index
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def parse_simple_scalar(value: str) -> Any:
|
|
342
|
+
if value == "[]":
|
|
343
|
+
return []
|
|
344
|
+
if value == "{}":
|
|
345
|
+
return {}
|
|
346
|
+
if value in {"true", "false"}:
|
|
347
|
+
return value == "true"
|
|
348
|
+
if value.isdigit():
|
|
349
|
+
return int(value)
|
|
350
|
+
if value.startswith('"') and value.endswith('"'):
|
|
351
|
+
try:
|
|
352
|
+
return json.loads(value)
|
|
353
|
+
except json.JSONDecodeError:
|
|
354
|
+
return value.strip('"')
|
|
355
|
+
if value.startswith("'") and value.endswith("'"):
|
|
356
|
+
return value.strip("'")
|
|
357
|
+
return value
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def secret_findings(value: Any, *, path: str = "") -> list[dict[str, Any]]:
|
|
361
|
+
findings: list[dict[str, Any]] = []
|
|
362
|
+
if isinstance(value, dict):
|
|
363
|
+
for key, item in value.items():
|
|
364
|
+
key_path = f"{path}.{key}" if path else str(key)
|
|
365
|
+
if SECRET_KEY_PATTERN.search(str(key)) and not is_reference_key(str(key)) and item not in (None, "", [], {}):
|
|
366
|
+
findings.append({"path": key_path, "reason": "secret-like-key"})
|
|
367
|
+
findings.extend(secret_findings(item, path=key_path))
|
|
368
|
+
return findings
|
|
369
|
+
if isinstance(value, list):
|
|
370
|
+
for index, item in enumerate(value):
|
|
371
|
+
findings.extend(secret_findings(item, path=f"{path}[{index}]"))
|
|
372
|
+
return findings
|
|
373
|
+
if isinstance(value, str) and SECRET_VALUE_PATTERN.search(value):
|
|
374
|
+
findings.append({"path": path, "reason": "secret-like-value"})
|
|
375
|
+
return findings
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def is_reference_key(key: str) -> bool:
|
|
379
|
+
normalized = key.lower().replace("-", "_")
|
|
380
|
+
return normalized in {"secret_ref", "secret_refs", "credential_ref", "credential_refs"} or normalized.endswith("_ref") or normalized.endswith("_refs")
|
|
@@ -162,12 +162,17 @@ def install_payload(
|
|
|
162
162
|
|
|
163
163
|
def load_toolchain(root: Path | None = None) -> dict[str, dict[str, Any]]:
|
|
164
164
|
path = toolchain_path(root)
|
|
165
|
+
if not path.exists():
|
|
166
|
+
return fallback_toolchain()
|
|
165
167
|
try:
|
|
166
168
|
import yaml # type: ignore
|
|
167
169
|
except ImportError:
|
|
168
170
|
return fallback_toolchain()
|
|
169
|
-
|
|
170
|
-
|
|
171
|
+
try:
|
|
172
|
+
with path.open(encoding="utf-8") as file:
|
|
173
|
+
data = yaml.safe_load(file) or {}
|
|
174
|
+
except OSError:
|
|
175
|
+
return fallback_toolchain()
|
|
171
176
|
tools = data.get("tools") if isinstance(data, dict) else {}
|
|
172
177
|
if not isinstance(tools, dict):
|
|
173
178
|
return {}
|