agent-devkit 0.2.0 → 0.3.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/README.md +48 -6
- package/bin/agent.mjs +133 -7
- package/package.json +1 -1
- package/runtime/README.md +187 -5
- 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 +35 -4
- 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 +1 -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 +145 -7
- package/runtime/cli/aikit/contribution.py +132 -2
- package/runtime/cli/aikit/doctor_runtime.py +85 -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 +148 -0
- package/runtime/cli/aikit/knowledge_base.py +1067 -0
- package/runtime/cli/aikit/llm.py +12 -4
- package/runtime/cli/aikit/local_artifacts.py +444 -0
- package/runtime/cli/aikit/local_llm.py +161 -0
- 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 +20 -1
- package/runtime/cli/aikit/natural_prompt_runtime.py +125 -1
- 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/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/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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Specialist agent readiness summaries for onboarding and doctor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter, defaultdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from cli.aikit.agent_registry import load_agent_registry
|
|
10
|
+
from cli.aikit.sources import list_sources, source_status
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SPECIALIST_READINESS_SCHEMA_VERSION = "agent-devkit.specialist-readiness/v1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def specialist_readiness(root: Path) -> dict[str, Any]:
|
|
17
|
+
registry = load_agent_registry(root)
|
|
18
|
+
capabilities = registry.get("capabilities") if isinstance(registry.get("capabilities"), dict) else {}
|
|
19
|
+
agents = registry.get("agents") if isinstance(registry.get("agents"), dict) else {}
|
|
20
|
+
source_summary = configured_source_summary()
|
|
21
|
+
configured_providers = set(source_summary["configured_providers"])
|
|
22
|
+
provider_capabilities: Counter[str] = Counter()
|
|
23
|
+
agent_providers: dict[str, Counter[str]] = defaultdict(Counter)
|
|
24
|
+
agent_capabilities: Counter[str] = Counter()
|
|
25
|
+
source_enabled_capabilities: Counter[str] = Counter()
|
|
26
|
+
|
|
27
|
+
for capability in capabilities.values():
|
|
28
|
+
if not isinstance(capability, dict):
|
|
29
|
+
continue
|
|
30
|
+
agent_id = str(capability.get("agent_id") or "")
|
|
31
|
+
if not agent_id:
|
|
32
|
+
continue
|
|
33
|
+
provider = str(capability.get("provider") or "").strip()
|
|
34
|
+
if provider:
|
|
35
|
+
provider_capabilities[provider] += 1
|
|
36
|
+
agent_providers[agent_id][provider] += 1
|
|
37
|
+
agent_capabilities[agent_id] += 1
|
|
38
|
+
source_contract = capability.get("source_contract") if isinstance(capability.get("source_contract"), dict) else {}
|
|
39
|
+
if source_contract.get("enabled") or source_contract.get("supported"):
|
|
40
|
+
source_enabled_capabilities[agent_id] += 1
|
|
41
|
+
|
|
42
|
+
items = [
|
|
43
|
+
agent_readiness_item(
|
|
44
|
+
agent_id,
|
|
45
|
+
agents.get(agent_id) if isinstance(agents.get(agent_id), dict) else {},
|
|
46
|
+
providers,
|
|
47
|
+
configured_providers=configured_providers,
|
|
48
|
+
source_enabled_count=source_enabled_capabilities.get(agent_id, 0),
|
|
49
|
+
)
|
|
50
|
+
for agent_id, providers in sorted(agent_providers.items())
|
|
51
|
+
]
|
|
52
|
+
missing_provider_counts: Counter[str] = Counter()
|
|
53
|
+
for item in items:
|
|
54
|
+
missing_provider_counts.update(item["missing_providers"])
|
|
55
|
+
|
|
56
|
+
blocked = [item for item in items if item["status"] == "needs-setup"]
|
|
57
|
+
partial = [item for item in items if item["status"] == "partial"]
|
|
58
|
+
ready = [item for item in items if item["status"] == "ready"]
|
|
59
|
+
status = "ready"
|
|
60
|
+
if blocked:
|
|
61
|
+
status = "needs-setup"
|
|
62
|
+
elif partial:
|
|
63
|
+
status = "partial"
|
|
64
|
+
return {
|
|
65
|
+
"kind": "specialist-readiness",
|
|
66
|
+
"schema_version": SPECIALIST_READINESS_SCHEMA_VERSION,
|
|
67
|
+
"status": status,
|
|
68
|
+
"agents_total": len(agents),
|
|
69
|
+
"capabilities_total": len(capabilities),
|
|
70
|
+
"agents_with_provider_requirements": len(items),
|
|
71
|
+
"ready_agents": len(ready),
|
|
72
|
+
"partial_agents": len(partial),
|
|
73
|
+
"needs_setup_agents": len(blocked),
|
|
74
|
+
"providers_required": [
|
|
75
|
+
{"id": provider, "capabilities": count, "configured": provider in configured_providers}
|
|
76
|
+
for provider, count in provider_capabilities.most_common()
|
|
77
|
+
],
|
|
78
|
+
"configured_providers": sorted(configured_providers),
|
|
79
|
+
"missing_providers": [
|
|
80
|
+
{"id": provider, "agents": count}
|
|
81
|
+
for provider, count in missing_provider_counts.most_common()
|
|
82
|
+
],
|
|
83
|
+
"items": items,
|
|
84
|
+
"source_summary": source_summary,
|
|
85
|
+
"next_steps": readiness_next_steps(missing_provider_counts),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def agent_readiness_item(
|
|
90
|
+
agent_id: str,
|
|
91
|
+
agent: dict[str, Any],
|
|
92
|
+
providers: Counter[str],
|
|
93
|
+
*,
|
|
94
|
+
configured_providers: set[str],
|
|
95
|
+
source_enabled_count: int,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
required = sorted(providers)
|
|
98
|
+
configured = sorted(provider for provider in required if provider in configured_providers)
|
|
99
|
+
missing = sorted(provider for provider in required if provider not in configured_providers)
|
|
100
|
+
status = "ready"
|
|
101
|
+
if missing and configured:
|
|
102
|
+
status = "partial"
|
|
103
|
+
elif missing:
|
|
104
|
+
status = "needs-setup"
|
|
105
|
+
return {
|
|
106
|
+
"id": agent_id,
|
|
107
|
+
"name": agent.get("name") or agent_id,
|
|
108
|
+
"status": status,
|
|
109
|
+
"required_providers": required,
|
|
110
|
+
"configured_providers": configured,
|
|
111
|
+
"missing_providers": missing,
|
|
112
|
+
"provider_capabilities": dict(sorted(providers.items())),
|
|
113
|
+
"source_enabled_capabilities": source_enabled_count,
|
|
114
|
+
"setup_commands": [f"agent provider configure {provider}" for provider in missing[:3]],
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def configured_source_summary() -> dict[str, Any]:
|
|
119
|
+
sources = list_sources()
|
|
120
|
+
try:
|
|
121
|
+
status = source_status()
|
|
122
|
+
status_items = status.get("items") if isinstance(status.get("items"), list) else []
|
|
123
|
+
except Exception: # noqa: BLE001 - readiness must remain diagnostic, not fatal.
|
|
124
|
+
status = {"status": "missing", "items": []}
|
|
125
|
+
status_items = []
|
|
126
|
+
configured_providers = sorted(
|
|
127
|
+
{
|
|
128
|
+
str(item.get("provider"))
|
|
129
|
+
for item in status_items
|
|
130
|
+
if isinstance(item, dict) and item.get("status") == "ok" and item.get("provider")
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
providers_with_sources = sorted(
|
|
134
|
+
{
|
|
135
|
+
str(item.get("provider"))
|
|
136
|
+
for item in sources.get("items") or []
|
|
137
|
+
if isinstance(item, dict) and item.get("provider")
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
return {
|
|
141
|
+
"status": status.get("status"),
|
|
142
|
+
"sources_count": len(sources.get("items") or []),
|
|
143
|
+
"configured_providers": configured_providers,
|
|
144
|
+
"providers_with_sources": providers_with_sources,
|
|
145
|
+
"stored_secret": sources.get("stored_secret") is True or status.get("stored_secret") is True,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def readiness_next_steps(missing_provider_counts: Counter[str]) -> list[str]:
|
|
150
|
+
if not missing_provider_counts:
|
|
151
|
+
return []
|
|
152
|
+
return [f"agent provider configure {provider}" for provider, _ in missing_provider_counts.most_common(5)]
|
|
@@ -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 {}
|