agent-devkit 0.0.4 → 0.1.5
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 +42 -2
- package/package.json +1 -1
- package/runtime/README.md +50 -2
- package/runtime/agent +29 -13
- package/runtime/agents/README.md +3 -0
- package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
- package/runtime/agents/github-pr-reviewer/README.md +7 -0
- package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
- package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
- package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
- package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
- package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
- package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
- package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
- package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
- package/runtime/cli/README.md +37 -1
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/aliases.py +196 -0
- package/runtime/cli/aikit/app_home.py +78 -0
- package/runtime/cli/aikit/audit.py +344 -0
- package/runtime/cli/aikit/calendar.py +163 -0
- package/runtime/cli/aikit/configuration_orchestrator.py +291 -0
- package/runtime/cli/aikit/decision_store.py +158 -0
- package/runtime/cli/aikit/diagnostics.py +9 -0
- package/runtime/cli/aikit/fallback.py +48 -2
- package/runtime/cli/aikit/github_pr.py +254 -0
- package/runtime/cli/aikit/identity.py +75 -0
- package/runtime/cli/aikit/llm.py +249 -48
- package/runtime/cli/aikit/main.py +1240 -32
- package/runtime/cli/aikit/memory.py +148 -8
- package/runtime/cli/aikit/model_router.py +70 -0
- package/runtime/cli/aikit/notifications.py +9 -0
- package/runtime/cli/aikit/ollama.py +237 -0
- package/runtime/cli/aikit/permissions.py +345 -0
- package/runtime/cli/aikit/personality.py +123 -0
- package/runtime/cli/aikit/provider_wizard.py +19 -0
- package/runtime/cli/aikit/providers.py +4 -5
- package/runtime/cli/aikit/review_gate.py +40 -0
- package/runtime/cli/aikit/scheduler.py +18 -0
- package/runtime/cli/aikit/sessions.py +396 -0
- package/runtime/cli/aikit/setup_wizard.py +12 -0
- package/runtime/cli/aikit/tasks.py +351 -0
- package/runtime/cli/aikit/toolchain.py +274 -0
- package/runtime/providers/calendar.yaml +28 -0
- package/runtime/providers/github.yaml +42 -0
- package/runtime/providers/local-notification.yaml +19 -0
- package/runtime/providers/local-scheduler.yaml +24 -0
- package/runtime/vendor/skills/napkin/napkin.md +13 -3
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Local command aliases for the canonical agent CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from cli.aikit.app_home import app_home, ensure_app_home
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ALIAS_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{1,63}$")
|
|
16
|
+
RESERVED_ALIASES = {"agent", "aikit", "ai-devkit", "python", "python3"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def aliases_config_path() -> Path:
|
|
20
|
+
return app_home() / "config" / "aliases.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def aliases_bin_dir() -> Path:
|
|
24
|
+
return app_home() / "bin"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_aliases() -> dict[str, Any]:
|
|
28
|
+
path = aliases_config_path()
|
|
29
|
+
if not path.exists():
|
|
30
|
+
return {"version": 1, "aliases": {}}
|
|
31
|
+
try:
|
|
32
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
33
|
+
except (OSError, json.JSONDecodeError):
|
|
34
|
+
return {"version": 1, "aliases": {}}
|
|
35
|
+
if not isinstance(data, dict):
|
|
36
|
+
return {"version": 1, "aliases": {}}
|
|
37
|
+
aliases = data.get("aliases")
|
|
38
|
+
if not isinstance(aliases, dict):
|
|
39
|
+
data["aliases"] = {}
|
|
40
|
+
data.setdefault("version", 1)
|
|
41
|
+
return data
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def save_aliases(config: dict[str, Any]) -> Path:
|
|
45
|
+
ensure_app_home()
|
|
46
|
+
path = aliases_config_path()
|
|
47
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
path.write_text(json.dumps(config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
49
|
+
return path
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def list_aliases() -> dict[str, Any]:
|
|
53
|
+
ensure_app_home()
|
|
54
|
+
config = load_aliases()
|
|
55
|
+
items = []
|
|
56
|
+
invalid = []
|
|
57
|
+
for name, item in sorted(config.get("aliases", {}).items()):
|
|
58
|
+
if not isinstance(item, dict):
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
alias = validate_alias_name(name)
|
|
62
|
+
except ValueError:
|
|
63
|
+
invalid.append(name)
|
|
64
|
+
continue
|
|
65
|
+
items.append(alias_payload(alias, item))
|
|
66
|
+
return {"kind": "aliases", "status": "ok", "config_path": str(aliases_config_path()), "items": items, "invalid": invalid}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def add_alias(name: str, *, force: bool = False) -> dict[str, Any]:
|
|
70
|
+
alias = validate_alias_name(name)
|
|
71
|
+
ensure_app_home()
|
|
72
|
+
target = alias_executable_path(alias)
|
|
73
|
+
existing = shutil.which(alias)
|
|
74
|
+
if existing and Path(existing).resolve() != target.resolve() and not force:
|
|
75
|
+
raise ValueError(f"alias command already exists on PATH: {existing}. Use --force to overwrite local alias only.")
|
|
76
|
+
|
|
77
|
+
config = load_aliases()
|
|
78
|
+
item = {"name": alias, "created_by": "agent-devkit"}
|
|
79
|
+
config.setdefault("aliases", {})[alias] = item
|
|
80
|
+
write_alias_shims(alias)
|
|
81
|
+
written_path = save_aliases(config)
|
|
82
|
+
payload = alias_payload(alias, item)
|
|
83
|
+
payload.update({"kind": "alias", "status": "added", "config_path": str(written_path)})
|
|
84
|
+
return payload
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def remove_alias(name: str) -> dict[str, Any]:
|
|
88
|
+
alias = validate_alias_name(name)
|
|
89
|
+
config = load_aliases()
|
|
90
|
+
removed = bool(config.get("aliases", {}).pop(alias, None))
|
|
91
|
+
removed_paths: list[str] = []
|
|
92
|
+
for path in alias_paths(alias):
|
|
93
|
+
if path.exists():
|
|
94
|
+
path.unlink()
|
|
95
|
+
removed_paths.append(str(path))
|
|
96
|
+
written_path = save_aliases(config)
|
|
97
|
+
return {
|
|
98
|
+
"kind": "alias",
|
|
99
|
+
"status": "removed" if removed or removed_paths else "missing",
|
|
100
|
+
"name": alias,
|
|
101
|
+
"config_path": str(written_path),
|
|
102
|
+
"removed_paths": removed_paths,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def sync_aliases() -> dict[str, Any]:
|
|
107
|
+
ensure_app_home()
|
|
108
|
+
config = load_aliases()
|
|
109
|
+
synced: list[dict[str, Any]] = []
|
|
110
|
+
invalid: list[str] = []
|
|
111
|
+
for alias, item in sorted(config.get("aliases", {}).items()):
|
|
112
|
+
if not isinstance(item, dict):
|
|
113
|
+
continue
|
|
114
|
+
try:
|
|
115
|
+
safe_alias = validate_alias_name(alias)
|
|
116
|
+
except ValueError:
|
|
117
|
+
invalid.append(alias)
|
|
118
|
+
continue
|
|
119
|
+
write_alias_shims(safe_alias)
|
|
120
|
+
synced.append(alias_payload(safe_alias, item))
|
|
121
|
+
save_aliases(config)
|
|
122
|
+
return {"kind": "aliases", "status": "synced", "config_path": str(aliases_config_path()), "items": synced, "invalid": invalid}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_alias_name(name: str, *, allow_reserved: bool = False) -> str:
|
|
126
|
+
alias = name.strip()
|
|
127
|
+
if not ALIAS_PATTERN.fullmatch(alias):
|
|
128
|
+
raise ValueError("alias must start with a letter and contain only letters, numbers, hyphen or underscore")
|
|
129
|
+
if alias.lower() in RESERVED_ALIASES and not allow_reserved:
|
|
130
|
+
raise ValueError(f"alias is reserved: {alias}")
|
|
131
|
+
return alias
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def alias_payload(alias: str, item: dict[str, Any]) -> dict[str, Any]:
|
|
135
|
+
return {
|
|
136
|
+
"name": alias,
|
|
137
|
+
"path": str(alias_executable_path(alias)),
|
|
138
|
+
"cmd_path": str(aliases_bin_dir() / f"{alias}.cmd"),
|
|
139
|
+
"ps1_path": str(aliases_bin_dir() / f"{alias}.ps1"),
|
|
140
|
+
"created_by": item.get("created_by"),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def write_alias_shims(alias: str) -> None:
|
|
145
|
+
bin_dir = aliases_bin_dir()
|
|
146
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
root = Path(__file__).resolve().parents[2]
|
|
148
|
+
agent_script = root / "agent"
|
|
149
|
+
python = sys.executable
|
|
150
|
+
|
|
151
|
+
posix = alias_executable_path(alias)
|
|
152
|
+
posix.write_text(
|
|
153
|
+
"\n".join(
|
|
154
|
+
[
|
|
155
|
+
"#!/usr/bin/env sh",
|
|
156
|
+
"set -eu",
|
|
157
|
+
f"AI_DEVKIT_INVOKED_AS={json.dumps(alias)} exec {json.dumps(python)} {json.dumps(str(agent_script))} \"$@\"",
|
|
158
|
+
"",
|
|
159
|
+
]
|
|
160
|
+
),
|
|
161
|
+
encoding="utf-8",
|
|
162
|
+
)
|
|
163
|
+
posix.chmod(0o755)
|
|
164
|
+
|
|
165
|
+
cmd = bin_dir / f"{alias}.cmd"
|
|
166
|
+
cmd.write_text(
|
|
167
|
+
"\r\n".join(
|
|
168
|
+
[
|
|
169
|
+
"@echo off",
|
|
170
|
+
f"set AI_DEVKIT_INVOKED_AS={alias}",
|
|
171
|
+
f'"{python}" "{agent_script}" %*',
|
|
172
|
+
"",
|
|
173
|
+
]
|
|
174
|
+
),
|
|
175
|
+
encoding="utf-8",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
ps1 = bin_dir / f"{alias}.ps1"
|
|
179
|
+
ps1.write_text(
|
|
180
|
+
"\n".join(
|
|
181
|
+
[
|
|
182
|
+
f"$env:AI_DEVKIT_INVOKED_AS = {json.dumps(alias)}",
|
|
183
|
+
f"& {json.dumps(python)} {json.dumps(str(agent_script))} @args",
|
|
184
|
+
"",
|
|
185
|
+
]
|
|
186
|
+
),
|
|
187
|
+
encoding="utf-8",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def alias_executable_path(alias: str) -> Path:
|
|
192
|
+
return aliases_bin_dir() / alias
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def alias_paths(alias: str) -> list[Path]:
|
|
196
|
+
return [alias_executable_path(alias), aliases_bin_dir() / f"{alias}.cmd", aliases_bin_dir() / f"{alias}.ps1"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Local AI DevKit application home and filesystem layout helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
APP_HOME_ENV = "AI_DEVKIT_CONFIG_HOME"
|
|
10
|
+
LEGACY_APP_HOME_ENV = "AIKIT_CONFIG_HOME"
|
|
11
|
+
DEFAULT_APP_HOME_NAME = ".ai-devkit"
|
|
12
|
+
|
|
13
|
+
APP_DIRS = (
|
|
14
|
+
"bin",
|
|
15
|
+
"config",
|
|
16
|
+
"memory",
|
|
17
|
+
"sessions",
|
|
18
|
+
"tasks",
|
|
19
|
+
"policies",
|
|
20
|
+
"audit",
|
|
21
|
+
"secrets",
|
|
22
|
+
"cache",
|
|
23
|
+
"logs",
|
|
24
|
+
"state",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def app_home() -> Path:
|
|
29
|
+
"""Return the configured Agent DevKit home directory."""
|
|
30
|
+
raw = os.environ.get(APP_HOME_ENV) or os.environ.get(LEGACY_APP_HOME_ENV)
|
|
31
|
+
if raw:
|
|
32
|
+
return Path(raw).expanduser().resolve()
|
|
33
|
+
return (Path.home() / DEFAULT_APP_HOME_NAME).resolve()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def app_path(*parts: str) -> Path:
|
|
37
|
+
return app_home().joinpath(*parts)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def ensure_app_home() -> Path:
|
|
41
|
+
"""Create the application home skeleton and return its path."""
|
|
42
|
+
home = app_home()
|
|
43
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
for name in APP_DIRS:
|
|
45
|
+
(home / name).mkdir(parents=True, exist_ok=True)
|
|
46
|
+
return home
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def config_path() -> Path:
|
|
50
|
+
return app_path("config.json")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def memory_home() -> Path:
|
|
54
|
+
return app_path("memory")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def sessions_home() -> Path:
|
|
58
|
+
return app_path("sessions")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def tasks_home() -> Path:
|
|
62
|
+
return app_path("tasks")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def audit_home() -> Path:
|
|
66
|
+
return app_path("audit")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def policies_home() -> Path:
|
|
70
|
+
return app_path("policies")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def secrets_home() -> Path:
|
|
74
|
+
return app_path("secrets")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cache_home() -> Path:
|
|
78
|
+
return app_path("cache")
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Local audit trail for Agent DevKit CLI executions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from cli.aikit.app_home import audit_home, ensure_app_home
|
|
14
|
+
from cli.aikit.memory import normalize_prompt, redact_secrets
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
AUDIT_VERSION = 1
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def now_utc() -> datetime:
|
|
21
|
+
return datetime.now(timezone.utc)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def now_iso() -> str:
|
|
25
|
+
return now_utc().isoformat()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ensure_audit_home() -> Path:
|
|
29
|
+
ensure_app_home()
|
|
30
|
+
home = audit_home()
|
|
31
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
return home
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def audit_day_home(day: str | None = None) -> Path:
|
|
36
|
+
resolved_day = day or now_utc().date().isoformat()
|
|
37
|
+
path = ensure_audit_home() / resolved_day
|
|
38
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def record_audit(
|
|
43
|
+
*,
|
|
44
|
+
command: str | None,
|
|
45
|
+
args: dict[str, Any],
|
|
46
|
+
result: dict[str, Any] | None = None,
|
|
47
|
+
error: str | None = None,
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
created_at = now_iso()
|
|
50
|
+
execution_id = f"exec_{now_utc().strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
|
|
51
|
+
prompt = extract_prompt(args)
|
|
52
|
+
safe_result = redact_value(result or {})
|
|
53
|
+
entry = {
|
|
54
|
+
"version": AUDIT_VERSION,
|
|
55
|
+
"id": execution_id,
|
|
56
|
+
"created_at": created_at,
|
|
57
|
+
"user": safe_user(),
|
|
58
|
+
"command": command,
|
|
59
|
+
"prompt": redact_secrets(prompt) if prompt else None,
|
|
60
|
+
"prompt_normalized": normalize_prompt(prompt) if prompt else None,
|
|
61
|
+
"session": extract_session(safe_result),
|
|
62
|
+
"route": safe_result.get("route"),
|
|
63
|
+
"agent": extract_agent(safe_result, args),
|
|
64
|
+
"providers": extract_providers(safe_result),
|
|
65
|
+
"sources": extract_sources(safe_result),
|
|
66
|
+
"commands": extract_commands(safe_result),
|
|
67
|
+
"permissions": extract_permissions(safe_result),
|
|
68
|
+
"llm_backends": extract_llm_backends(safe_result),
|
|
69
|
+
"token_estimate": extract_token_estimate(safe_result),
|
|
70
|
+
"external_actions": extract_external_actions(safe_result),
|
|
71
|
+
"result": summarize_result(safe_result),
|
|
72
|
+
"error": redact_secrets(error) if error else None,
|
|
73
|
+
"redaction_applied": True,
|
|
74
|
+
}
|
|
75
|
+
day_home = audit_day_home(created_at[:10])
|
|
76
|
+
json_path = day_home / f"{execution_id}.json"
|
|
77
|
+
md_path = day_home / f"{execution_id}.md"
|
|
78
|
+
json_path.write_text(json.dumps(entry, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
79
|
+
md_path.write_text(render_audit_md(entry), encoding="utf-8")
|
|
80
|
+
return {"id": execution_id, "json_path": str(json_path), "markdown_path": str(md_path)}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def list_audits(limit: int = 20) -> dict[str, Any]:
|
|
84
|
+
items: list[dict[str, Any]] = []
|
|
85
|
+
for path in sorted(ensure_audit_home().glob("*/*.json"), reverse=True):
|
|
86
|
+
try:
|
|
87
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
88
|
+
except json.JSONDecodeError:
|
|
89
|
+
continue
|
|
90
|
+
if not isinstance(payload, dict):
|
|
91
|
+
continue
|
|
92
|
+
items.append(
|
|
93
|
+
{
|
|
94
|
+
"id": payload.get("id"),
|
|
95
|
+
"created_at": payload.get("created_at"),
|
|
96
|
+
"command": payload.get("command"),
|
|
97
|
+
"status": (payload.get("result") or {}).get("status"),
|
|
98
|
+
"ok": (payload.get("result") or {}).get("ok"),
|
|
99
|
+
"json_path": str(path),
|
|
100
|
+
"markdown_path": str(path.with_suffix(".md")),
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
if len(items) >= limit:
|
|
104
|
+
break
|
|
105
|
+
return {"kind": "audit", "status": "ok", "home": str(ensure_audit_home()), "items": items}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def show_audit(execution_id: str) -> dict[str, Any]:
|
|
109
|
+
path = find_audit_json(execution_id)
|
|
110
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
111
|
+
return {
|
|
112
|
+
"kind": "audit-entry",
|
|
113
|
+
"status": "ok",
|
|
114
|
+
"id": payload.get("id"),
|
|
115
|
+
"entry": payload,
|
|
116
|
+
"json_path": str(path),
|
|
117
|
+
"markdown_path": str(path.with_suffix(".md")),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def export_audit(execution_id: str, *, fmt: str = "md") -> dict[str, Any]:
|
|
122
|
+
path = find_audit_json(execution_id)
|
|
123
|
+
if fmt == "json":
|
|
124
|
+
content = path.read_text(encoding="utf-8")
|
|
125
|
+
export_path = path
|
|
126
|
+
elif fmt == "md":
|
|
127
|
+
md_path = path.with_suffix(".md")
|
|
128
|
+
content = md_path.read_text(encoding="utf-8") if md_path.exists() else render_audit_md(json.loads(path.read_text(encoding="utf-8")))
|
|
129
|
+
export_path = md_path
|
|
130
|
+
else:
|
|
131
|
+
raise ValueError("--format must be md or json")
|
|
132
|
+
return {
|
|
133
|
+
"kind": "audit-export",
|
|
134
|
+
"status": "ok",
|
|
135
|
+
"id": execution_id,
|
|
136
|
+
"format": fmt,
|
|
137
|
+
"path": str(export_path),
|
|
138
|
+
"content": content,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def find_audit_json(execution_id: str) -> Path:
|
|
143
|
+
if not execution_id or "/" in execution_id or "\\" in execution_id:
|
|
144
|
+
raise ValueError(f"invalid audit execution id: {execution_id}")
|
|
145
|
+
matches = sorted(ensure_audit_home().glob(f"*/{execution_id}.json"))
|
|
146
|
+
if not matches:
|
|
147
|
+
prefix_matches = sorted(ensure_audit_home().glob(f"*/{execution_id}*.json"))
|
|
148
|
+
if len(prefix_matches) == 1:
|
|
149
|
+
return prefix_matches[0]
|
|
150
|
+
if len(prefix_matches) > 1:
|
|
151
|
+
raise ValueError(f"ambiguous audit execution id prefix: {execution_id}")
|
|
152
|
+
raise ValueError(f"audit execution not found: {execution_id}")
|
|
153
|
+
return matches[0]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def redact_value(value: Any) -> Any:
|
|
157
|
+
if isinstance(value, str):
|
|
158
|
+
return redact_secrets(value)
|
|
159
|
+
if isinstance(value, list):
|
|
160
|
+
return [redact_value(item) for item in value]
|
|
161
|
+
if isinstance(value, tuple):
|
|
162
|
+
return [redact_value(item) for item in value]
|
|
163
|
+
if isinstance(value, dict):
|
|
164
|
+
redacted: dict[str, Any] = {}
|
|
165
|
+
for key, item in value.items():
|
|
166
|
+
key_text = str(key)
|
|
167
|
+
if secret_key(key_text):
|
|
168
|
+
redacted[key_text] = "[REDACTED_SECRET]"
|
|
169
|
+
else:
|
|
170
|
+
redacted[key_text] = redact_value(item)
|
|
171
|
+
return redacted
|
|
172
|
+
return value
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def secret_key(key: str) -> bool:
|
|
176
|
+
normalized = key.lower().replace("-", "_")
|
|
177
|
+
if normalized in {"token_estimate", "tokens", "prompt_tokens", "completion_tokens", "total_tokens"}:
|
|
178
|
+
return False
|
|
179
|
+
exact = {"token", "secret", "password", "passwd", "pwd", "api_key", "private_key", "pat"}
|
|
180
|
+
if normalized in exact:
|
|
181
|
+
return True
|
|
182
|
+
if normalized.endswith(("_token", "_secret", "_password", "_passwd", "_pwd", "_api_key", "_private_key", "_pat")):
|
|
183
|
+
return True
|
|
184
|
+
compact = re.sub(r"[^a-z0-9]+", "", key.lower())
|
|
185
|
+
if compact in {"tokenestimate", "tokens", "prompttokens", "completiontokens", "totaltokens"}:
|
|
186
|
+
return False
|
|
187
|
+
return any(marker in compact for marker in ("token", "secret", "password", "passwd", "apikey", "privatekey"))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def extract_prompt(args: dict[str, Any]) -> str | None:
|
|
191
|
+
prompt = args.get("prompt")
|
|
192
|
+
if isinstance(prompt, list):
|
|
193
|
+
return " ".join(str(item) for item in prompt).strip()
|
|
194
|
+
return str(prompt).strip() if prompt else None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def extract_session(result: dict[str, Any]) -> dict[str, Any] | None:
|
|
198
|
+
session = result.get("session")
|
|
199
|
+
if isinstance(session, dict):
|
|
200
|
+
return {
|
|
201
|
+
"id": session.get("id"),
|
|
202
|
+
"title": session.get("title"),
|
|
203
|
+
"project": session.get("project"),
|
|
204
|
+
"token_estimate": session.get("token_estimate"),
|
|
205
|
+
}
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def extract_agent(result: dict[str, Any], args: dict[str, Any]) -> dict[str, Any]:
|
|
210
|
+
if isinstance(result.get("agent"), dict):
|
|
211
|
+
return result["agent"]
|
|
212
|
+
if result.get("agent_id"):
|
|
213
|
+
return {"id": result.get("agent_id")}
|
|
214
|
+
if args.get("agent"):
|
|
215
|
+
return {"id": args.get("agent")}
|
|
216
|
+
route = result.get("route") if isinstance(result.get("route"), dict) else {}
|
|
217
|
+
if route.get("agent_id"):
|
|
218
|
+
return {"id": route.get("agent_id"), "capability": route.get("capability_id")}
|
|
219
|
+
return {}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def extract_providers(result: dict[str, Any]) -> Any:
|
|
223
|
+
if result.get("providers") is not None:
|
|
224
|
+
return result.get("providers")
|
|
225
|
+
route = result.get("route") if isinstance(result.get("route"), dict) else {}
|
|
226
|
+
provider = result.get("provider") or route.get("provider") or result.get("source_provider")
|
|
227
|
+
return {"used": [provider], "missing": [], "skipped": []} if provider else {"used": [], "missing": [], "skipped": []}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def extract_sources(result: dict[str, Any]) -> list[Any]:
|
|
231
|
+
source = result.get("source")
|
|
232
|
+
if source:
|
|
233
|
+
return [source]
|
|
234
|
+
return []
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def extract_commands(result: dict[str, Any]) -> list[Any]:
|
|
238
|
+
commands = []
|
|
239
|
+
command = result.get("command")
|
|
240
|
+
if command:
|
|
241
|
+
commands.append(command)
|
|
242
|
+
for attempt in result.get("llm_backend_attempts") or []:
|
|
243
|
+
if isinstance(attempt, dict) and attempt.get("command"):
|
|
244
|
+
commands.append(attempt["command"])
|
|
245
|
+
return commands
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def extract_permissions(result: dict[str, Any]) -> list[Any]:
|
|
249
|
+
permissions = []
|
|
250
|
+
for key in ("permission", "permissions"):
|
|
251
|
+
if result.get(key):
|
|
252
|
+
permissions.append(result[key])
|
|
253
|
+
preview = result.get("preview") if isinstance(result.get("preview"), dict) else {}
|
|
254
|
+
if preview.get("permissions"):
|
|
255
|
+
permissions.append(preview["permissions"])
|
|
256
|
+
return permissions
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def extract_llm_backends(result: dict[str, Any]) -> list[Any]:
|
|
260
|
+
attempts = result.get("llm_backend_attempts")
|
|
261
|
+
if isinstance(attempts, list):
|
|
262
|
+
return attempts
|
|
263
|
+
backend = result.get("llm_backend")
|
|
264
|
+
return [{"id": backend}] if backend else []
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def extract_token_estimate(result: dict[str, Any]) -> int | None:
|
|
268
|
+
session = result.get("session") if isinstance(result.get("session"), dict) else {}
|
|
269
|
+
if session.get("token_estimate") is not None:
|
|
270
|
+
return int(session.get("token_estimate") or 0)
|
|
271
|
+
prompt_length = result.get("prompt_length")
|
|
272
|
+
return int(prompt_length / 4) if isinstance(prompt_length, int) else None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def extract_external_actions(result: dict[str, Any]) -> list[dict[str, Any]]:
|
|
276
|
+
actions: list[dict[str, Any]] = []
|
|
277
|
+
preview = result.get("preview") if isinstance(result.get("preview"), dict) else {}
|
|
278
|
+
if preview.get("external_writes"):
|
|
279
|
+
actions.append({"type": preview.get("action_type"), "external_writes": True})
|
|
280
|
+
if result.get("external_action"):
|
|
281
|
+
actions.append(result["external_action"])
|
|
282
|
+
return actions
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def summarize_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
286
|
+
return {
|
|
287
|
+
"kind": result.get("kind"),
|
|
288
|
+
"status": result.get("status"),
|
|
289
|
+
"ok": result.get("ok"),
|
|
290
|
+
"exit_code": result.get("exit_code"),
|
|
291
|
+
"message": result.get("message"),
|
|
292
|
+
"mode": result.get("mode"),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def safe_user() -> str:
|
|
297
|
+
try:
|
|
298
|
+
return redact_secrets(getpass.getuser())
|
|
299
|
+
except Exception:
|
|
300
|
+
return "unknown"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def render_audit_md(entry: dict[str, Any]) -> str:
|
|
304
|
+
lines = [
|
|
305
|
+
f"# Audit {entry.get('id')}",
|
|
306
|
+
"",
|
|
307
|
+
f"- Created at: `{entry.get('created_at')}`",
|
|
308
|
+
f"- User: `{entry.get('user')}`",
|
|
309
|
+
f"- Command: `{entry.get('command')}`",
|
|
310
|
+
f"- Status: `{(entry.get('result') or {}).get('status')}`",
|
|
311
|
+
f"- OK: `{(entry.get('result') or {}).get('ok')}`",
|
|
312
|
+
"",
|
|
313
|
+
"## Prompt",
|
|
314
|
+
"",
|
|
315
|
+
entry.get("prompt") or "-",
|
|
316
|
+
"",
|
|
317
|
+
"## Agent",
|
|
318
|
+
"",
|
|
319
|
+
"```json",
|
|
320
|
+
json.dumps(entry.get("agent") or {}, ensure_ascii=False, indent=2, sort_keys=True),
|
|
321
|
+
"```",
|
|
322
|
+
"",
|
|
323
|
+
"## Providers",
|
|
324
|
+
"",
|
|
325
|
+
"```json",
|
|
326
|
+
json.dumps(entry.get("providers") or {}, ensure_ascii=False, indent=2, sort_keys=True),
|
|
327
|
+
"```",
|
|
328
|
+
"",
|
|
329
|
+
"## Permissions",
|
|
330
|
+
"",
|
|
331
|
+
"```json",
|
|
332
|
+
json.dumps(entry.get("permissions") or [], ensure_ascii=False, indent=2, sort_keys=True),
|
|
333
|
+
"```",
|
|
334
|
+
"",
|
|
335
|
+
"## Result",
|
|
336
|
+
"",
|
|
337
|
+
"```json",
|
|
338
|
+
json.dumps(entry.get("result") or {}, ensure_ascii=False, indent=2, sort_keys=True),
|
|
339
|
+
"```",
|
|
340
|
+
]
|
|
341
|
+
if entry.get("error"):
|
|
342
|
+
lines.extend(["", "## Error", "", str(entry["error"])])
|
|
343
|
+
lines.append("")
|
|
344
|
+
return "\n".join(lines)
|