agent-devkit 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -1
- package/package.json +1 -1
- package/runtime/README.md +192 -14
- 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 +93 -0
- 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/diagnostics.py +9 -0
- package/runtime/cli/aikit/github_pr.py +254 -0
- package/runtime/cli/aikit/identity.py +75 -0
- package/runtime/cli/aikit/llm.py +240 -48
- package/runtime/cli/aikit/main.py +924 -24
- package/runtime/cli/aikit/memory.py +148 -8
- package/runtime/cli/aikit/notifications.py +9 -0
- package/runtime/cli/aikit/permissions.py +345 -0
- package/runtime/cli/aikit/personality.py +123 -0
- package/runtime/cli/aikit/providers.py +4 -5
- 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/plugins/claude-code-ai-devkit/plugin.json +1 -1
- package/runtime/plugins/claude-skill-ai-devkit/plugin.json +1 -1
- 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/scripts/README.md +1 -1
- package/runtime/scripts/verify-release-alignment.mjs +1 -1
- package/runtime/vendor/skills/napkin/napkin.md +13 -3
|
@@ -8,27 +8,140 @@ from datetime import datetime, timezone
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from cli.aikit.
|
|
11
|
+
from cli.aikit.app_home import (
|
|
12
|
+
app_path,
|
|
13
|
+
cache_home,
|
|
14
|
+
ensure_app_home,
|
|
15
|
+
memory_home as app_memory_home,
|
|
16
|
+
sessions_home,
|
|
17
|
+
tasks_home,
|
|
18
|
+
)
|
|
19
|
+
from cli.aikit.llm import config_path, load_config, save_config
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SECRET_VALUE_PATTERN = re.compile(
|
|
23
|
+
r"(?i)\b("
|
|
24
|
+
r"sk-[a-z0-9_-]{12,}|"
|
|
25
|
+
r"npm_[a-z0-9]{12,}|"
|
|
26
|
+
r"gh[pousr]_[a-z0-9_]{12,}|"
|
|
27
|
+
r"xox[baprs]-[a-z0-9-]{12,}|"
|
|
28
|
+
r"AKIA[0-9A-Z]{12,}|"
|
|
29
|
+
r"ASIA[0-9A-Z]{12,}"
|
|
30
|
+
r")\b"
|
|
31
|
+
)
|
|
32
|
+
SECRET_ASSIGNMENT_PATTERN = re.compile(
|
|
33
|
+
r"(?i)([\"']?)([a-z0-9_]*(?:token|secret|password|passwd|pwd|senha|chave|api[_-]?key|private[_-]?key)[a-z0-9_]*)([\"']?)(\s*[:=]\s*)([\"']?)([^\s,;}\"']+)([\"']?)"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
MEMORY_FILE_TEMPLATES: dict[str, str] = {
|
|
37
|
+
"profile.md": """# Profile
|
|
38
|
+
|
|
39
|
+
Local user profile for Agent DevKit.
|
|
40
|
+
|
|
41
|
+
## User
|
|
42
|
+
|
|
43
|
+
- Name:
|
|
44
|
+
- Primary language:
|
|
45
|
+
- Timezone:
|
|
46
|
+
|
|
47
|
+
## Notes
|
|
48
|
+
|
|
49
|
+
- Add stable facts the agent should remember.
|
|
50
|
+
""",
|
|
51
|
+
"personality.md": """# Personality
|
|
52
|
+
|
|
53
|
+
Configured public identity and response style for Agent DevKit.
|
|
54
|
+
|
|
55
|
+
## Agent
|
|
56
|
+
|
|
57
|
+
- Name: Agent DevKit
|
|
58
|
+
|
|
59
|
+
## Style
|
|
60
|
+
|
|
61
|
+
- Tone: direct
|
|
62
|
+
- Detail level: concise
|
|
63
|
+
""",
|
|
64
|
+
"preferences.md": """# Preferences
|
|
65
|
+
|
|
66
|
+
Reusable user preferences.
|
|
67
|
+
|
|
68
|
+
## Defaults
|
|
69
|
+
|
|
70
|
+
- Prefer local-first execution.
|
|
71
|
+
- Ask before external writes.
|
|
72
|
+
""",
|
|
73
|
+
"projects.md": """# Projects
|
|
74
|
+
|
|
75
|
+
Frequently used projects and repositories.
|
|
76
|
+
|
|
77
|
+
## Items
|
|
78
|
+
|
|
79
|
+
- Add project names, paths, and non-secret references here.
|
|
80
|
+
""",
|
|
81
|
+
"routines.md": """# Routines
|
|
82
|
+
|
|
83
|
+
Recurring workflows, checks, and habits.
|
|
84
|
+
|
|
85
|
+
## Items
|
|
86
|
+
|
|
87
|
+
- Add routines that should be easy to reuse.
|
|
88
|
+
""",
|
|
89
|
+
"napkin.md": """# Agent DevKit Napkin
|
|
90
|
+
|
|
91
|
+
Curated local runbook entries promoted from repeated use.
|
|
92
|
+
|
|
93
|
+
## Execution & Validation
|
|
94
|
+
|
|
95
|
+
- Keep high-value reusable notes here.
|
|
96
|
+
""",
|
|
97
|
+
}
|
|
12
98
|
|
|
13
99
|
|
|
14
100
|
def memory_home() -> Path:
|
|
15
|
-
return
|
|
101
|
+
return app_memory_home()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def ensure_memory() -> dict[str, Any]:
|
|
105
|
+
ensure_app_home()
|
|
106
|
+
home = memory_home()
|
|
107
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
files: list[dict[str, Any]] = []
|
|
109
|
+
created: list[str] = []
|
|
110
|
+
for name, template in MEMORY_FILE_TEMPLATES.items():
|
|
111
|
+
path = home / name
|
|
112
|
+
if not path.exists():
|
|
113
|
+
path.write_text(template, encoding="utf-8")
|
|
114
|
+
created.append(str(path))
|
|
115
|
+
files.append({"name": name, "path": str(path), "exists": path.exists()})
|
|
116
|
+
return {
|
|
117
|
+
"kind": "memory-path",
|
|
118
|
+
"status": "ok",
|
|
119
|
+
"home": str(home),
|
|
120
|
+
"created": created,
|
|
121
|
+
"files": files,
|
|
122
|
+
}
|
|
16
123
|
|
|
17
124
|
|
|
18
125
|
def normalize_prompt(prompt: str) -> str:
|
|
19
|
-
text = " ".join(prompt.lower().split())
|
|
126
|
+
text = " ".join(redact_secrets(prompt).lower().split())
|
|
20
127
|
return text[:160]
|
|
21
128
|
|
|
22
129
|
|
|
130
|
+
def redact_secrets(value: str) -> str:
|
|
131
|
+
redacted = SECRET_VALUE_PATTERN.sub("[REDACTED_SECRET]", value)
|
|
132
|
+
return SECRET_ASSIGNMENT_PATTERN.sub(r"\1\2\3\4\5[REDACTED_SECRET]\7", redacted)
|
|
133
|
+
|
|
134
|
+
|
|
23
135
|
def napkin_context(root: Path, *, agent_id: str | None = None, source_id: str | None = None) -> dict[str, Any]:
|
|
24
136
|
paths = [
|
|
25
137
|
root / "vendor" / "skills" / "napkin" / "napkin.md",
|
|
138
|
+
memory_home() / "napkin.md",
|
|
26
139
|
memory_home() / "global" / "napkin.md",
|
|
27
140
|
]
|
|
28
141
|
if agent_id:
|
|
29
|
-
paths.append(memory_home() / "agents" / agent_id / "napkin.md")
|
|
142
|
+
paths.append(memory_home() / "agents" / sanitize_segment(agent_id) / "napkin.md")
|
|
30
143
|
if source_id:
|
|
31
|
-
paths.append(memory_home() / "sources" / source_id / "napkin.md")
|
|
144
|
+
paths.append(memory_home() / "sources" / sanitize_segment(source_id) / "napkin.md")
|
|
32
145
|
return {
|
|
33
146
|
"loaded": any(path.exists() for path in paths),
|
|
34
147
|
"paths": [
|
|
@@ -61,6 +174,7 @@ def record_usage(
|
|
|
61
174
|
|
|
62
175
|
|
|
63
176
|
def show_memory(root: Path, *, agent_id: str | None = None, source_id: str | None = None) -> dict[str, Any]:
|
|
177
|
+
memory_paths = ensure_memory()
|
|
64
178
|
config = load_config()
|
|
65
179
|
memory = config.get("memory") if isinstance(config.get("memory"), dict) else {}
|
|
66
180
|
usage = memory.get("usage") if isinstance(memory.get("usage"), dict) else {}
|
|
@@ -69,6 +183,7 @@ def show_memory(root: Path, *, agent_id: str | None = None, source_id: str | Non
|
|
|
69
183
|
"status": "ok",
|
|
70
184
|
"config_path": str(config_path()),
|
|
71
185
|
"memory_home": str(memory_home()),
|
|
186
|
+
"files": memory_paths["files"],
|
|
72
187
|
"usage": {
|
|
73
188
|
"prompts": sorted_usage(usage.get("prompts") or {}),
|
|
74
189
|
"routes": sorted_usage(usage.get("routes") or {}),
|
|
@@ -78,10 +193,19 @@ def show_memory(root: Path, *, agent_id: str | None = None, source_id: str | Non
|
|
|
78
193
|
}
|
|
79
194
|
|
|
80
195
|
|
|
81
|
-
def reset_memory(
|
|
196
|
+
def reset_memory(
|
|
197
|
+
*,
|
|
198
|
+
all_memory: bool = False,
|
|
199
|
+
agent_id: str | None = None,
|
|
200
|
+
source_id: str | None = None,
|
|
201
|
+
reset_sessions: bool = False,
|
|
202
|
+
reset_tasks: bool = False,
|
|
203
|
+
reset_cache: bool = False,
|
|
204
|
+
) -> dict[str, Any]:
|
|
82
205
|
config = load_config()
|
|
83
206
|
removed_paths: list[str] = []
|
|
84
|
-
|
|
207
|
+
scoped_reset = any([agent_id, source_id, reset_sessions, reset_tasks, reset_cache])
|
|
208
|
+
if all_memory or not scoped_reset:
|
|
85
209
|
config.pop("memory", None)
|
|
86
210
|
if memory_home().exists():
|
|
87
211
|
removed_paths.append(str(memory_home()))
|
|
@@ -97,6 +221,14 @@ def reset_memory(*, all_memory: bool = False, agent_id: str | None = None, sourc
|
|
|
97
221
|
usage.get("sources", {}).pop(source_id, None)
|
|
98
222
|
path = memory_home() / "sources" / sanitize_segment(source_id)
|
|
99
223
|
remove_path(path, removed_paths)
|
|
224
|
+
if all_memory or reset_sessions:
|
|
225
|
+
remove_path(sessions_home(), removed_paths)
|
|
226
|
+
remove_path(app_path("state", "active-session.json"), removed_paths)
|
|
227
|
+
if all_memory or reset_tasks:
|
|
228
|
+
remove_path(tasks_home(), removed_paths)
|
|
229
|
+
if all_memory or reset_cache:
|
|
230
|
+
remove_path(cache_home(), removed_paths)
|
|
231
|
+
ensure_app_home()
|
|
100
232
|
written_path = save_config(config)
|
|
101
233
|
return {
|
|
102
234
|
"kind": "memory-reset",
|
|
@@ -104,9 +236,16 @@ def reset_memory(*, all_memory: bool = False, agent_id: str | None = None, sourc
|
|
|
104
236
|
"config_path": str(written_path),
|
|
105
237
|
"removed_paths": removed_paths,
|
|
106
238
|
"sources_preserved": True,
|
|
239
|
+
"sessions_reset": bool(all_memory or reset_sessions),
|
|
240
|
+
"tasks_reset": bool(all_memory or reset_tasks),
|
|
241
|
+
"cache_reset": bool(all_memory or reset_cache),
|
|
107
242
|
}
|
|
108
243
|
|
|
109
244
|
|
|
245
|
+
def memory_path_payload() -> dict[str, Any]:
|
|
246
|
+
return ensure_memory()
|
|
247
|
+
|
|
248
|
+
|
|
110
249
|
def increment_bucket(bucket: dict[str, Any], key: str, now: str) -> None:
|
|
111
250
|
item = bucket.setdefault(key, {"count": 0, "first_seen": now, "last_seen": now})
|
|
112
251
|
item["count"] = int(item.get("count") or 0) + 1
|
|
@@ -132,4 +271,5 @@ def remove_path(path: Path, removed_paths: list[str]) -> None:
|
|
|
132
271
|
|
|
133
272
|
|
|
134
273
|
def sanitize_segment(value: str) -> str:
|
|
135
|
-
|
|
274
|
+
sanitized = re.sub(r"[^a-zA-Z0-9._-]+", "-", value).strip(".-")
|
|
275
|
+
return sanitized or "item"
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Local permission policy helpers for Agent DevKit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from cli.aikit.app_home import ensure_app_home, policies_home
|
|
11
|
+
from cli.aikit.memory import redact_secrets
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
PERMISSION_LEVELS = (
|
|
15
|
+
"read-only",
|
|
16
|
+
"draft-only",
|
|
17
|
+
"comment-with-approval",
|
|
18
|
+
"write-with-approval",
|
|
19
|
+
"auto-write",
|
|
20
|
+
"admin",
|
|
21
|
+
)
|
|
22
|
+
DEFAULT_PERMISSION_LEVEL = "read-only"
|
|
23
|
+
PROVIDER_BY_AGENT = {
|
|
24
|
+
"github-pr-reviewer": "github",
|
|
25
|
+
}
|
|
26
|
+
ACTION_REQUIRED_LEVEL = {
|
|
27
|
+
"read": "read-only",
|
|
28
|
+
"draft": "draft-only",
|
|
29
|
+
"comment": "comment-with-approval",
|
|
30
|
+
"write": "write-with-approval",
|
|
31
|
+
"approve": "write-with-approval",
|
|
32
|
+
"request-changes": "write-with-approval",
|
|
33
|
+
"admin": "admin",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def now_iso() -> str:
|
|
38
|
+
return datetime.now(timezone.utc).isoformat()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ensure_policies_home() -> Path:
|
|
42
|
+
ensure_app_home()
|
|
43
|
+
home = policies_home()
|
|
44
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
return home
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def permissions_json_path() -> Path:
|
|
49
|
+
return ensure_policies_home() / "permissions.json"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def permissions_md_path() -> Path:
|
|
53
|
+
return ensure_policies_home() / "permissions.md"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def empty_policy() -> dict[str, Any]:
|
|
57
|
+
return {
|
|
58
|
+
"version": 1,
|
|
59
|
+
"default_level": DEFAULT_PERMISSION_LEVEL,
|
|
60
|
+
"grants": [],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load_permissions() -> dict[str, Any]:
|
|
65
|
+
path = permissions_json_path()
|
|
66
|
+
if not path.exists():
|
|
67
|
+
policy = empty_policy()
|
|
68
|
+
save_permissions(policy)
|
|
69
|
+
return policy
|
|
70
|
+
try:
|
|
71
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
72
|
+
except json.JSONDecodeError:
|
|
73
|
+
backup = path.with_suffix(f".corrupt-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}.json")
|
|
74
|
+
path.replace(backup)
|
|
75
|
+
data = empty_policy()
|
|
76
|
+
if not isinstance(data, dict):
|
|
77
|
+
data = empty_policy()
|
|
78
|
+
data.setdefault("version", 1)
|
|
79
|
+
data.setdefault("default_level", DEFAULT_PERMISSION_LEVEL)
|
|
80
|
+
if data.get("default_level") not in PERMISSION_LEVELS:
|
|
81
|
+
data["default_level"] = DEFAULT_PERMISSION_LEVEL
|
|
82
|
+
if not isinstance(data.get("grants"), list):
|
|
83
|
+
data["grants"] = []
|
|
84
|
+
return data
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def save_permissions(policy: dict[str, Any]) -> Path:
|
|
88
|
+
ensure_policies_home()
|
|
89
|
+
path = permissions_json_path()
|
|
90
|
+
path.write_text(json.dumps(policy, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
91
|
+
permissions_md_path().write_text(render_permissions_md(policy), encoding="utf-8")
|
|
92
|
+
return path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def show_permissions() -> dict[str, Any]:
|
|
96
|
+
policy = load_permissions()
|
|
97
|
+
return {
|
|
98
|
+
"kind": "permissions",
|
|
99
|
+
"status": "ok",
|
|
100
|
+
"default_level": policy["default_level"],
|
|
101
|
+
"levels": list(PERMISSION_LEVELS),
|
|
102
|
+
"grants": [public_grant(item) for item in policy["grants"] if isinstance(item, dict)],
|
|
103
|
+
"json_path": str(permissions_json_path()),
|
|
104
|
+
"markdown_path": str(permissions_md_path()),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def grant_permission(
|
|
109
|
+
agent: str,
|
|
110
|
+
provider: str,
|
|
111
|
+
level: str,
|
|
112
|
+
*,
|
|
113
|
+
project: str | None = None,
|
|
114
|
+
task_id: str | None = None,
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
validate_level(level)
|
|
117
|
+
policy = load_permissions()
|
|
118
|
+
grants = [item for item in policy["grants"] if isinstance(item, dict)]
|
|
119
|
+
grant = find_grant(grants, agent=agent, provider=provider, project=project, task_id=task_id)
|
|
120
|
+
timestamp = now_iso()
|
|
121
|
+
if grant:
|
|
122
|
+
grant["level"] = level
|
|
123
|
+
grant["updated_at"] = timestamp
|
|
124
|
+
else:
|
|
125
|
+
grant = {
|
|
126
|
+
"agent": agent,
|
|
127
|
+
"provider": provider,
|
|
128
|
+
"level": level,
|
|
129
|
+
"project": project,
|
|
130
|
+
"task_id": task_id,
|
|
131
|
+
"created_at": timestamp,
|
|
132
|
+
"updated_at": timestamp,
|
|
133
|
+
}
|
|
134
|
+
grants.append(grant)
|
|
135
|
+
policy["grants"] = grants
|
|
136
|
+
path = save_permissions(policy)
|
|
137
|
+
return {
|
|
138
|
+
"kind": "permissions",
|
|
139
|
+
"status": "granted",
|
|
140
|
+
"grant": public_grant(grant),
|
|
141
|
+
"json_path": str(path),
|
|
142
|
+
"markdown_path": str(permissions_md_path()),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def revoke_permission(
|
|
147
|
+
agent: str,
|
|
148
|
+
provider: str,
|
|
149
|
+
level: str | None = None,
|
|
150
|
+
*,
|
|
151
|
+
project: str | None = None,
|
|
152
|
+
task_id: str | None = None,
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
if level:
|
|
155
|
+
validate_level(level)
|
|
156
|
+
policy = load_permissions()
|
|
157
|
+
removed: list[dict[str, Any]] = []
|
|
158
|
+
kept: list[dict[str, Any]] = []
|
|
159
|
+
for item in policy["grants"]:
|
|
160
|
+
if not isinstance(item, dict):
|
|
161
|
+
continue
|
|
162
|
+
matches = (
|
|
163
|
+
item.get("agent") == agent
|
|
164
|
+
and item.get("provider") == provider
|
|
165
|
+
and item.get("project") == project
|
|
166
|
+
and item.get("task_id") == task_id
|
|
167
|
+
and (level is None or item.get("level") == level)
|
|
168
|
+
)
|
|
169
|
+
if matches:
|
|
170
|
+
removed.append(item)
|
|
171
|
+
else:
|
|
172
|
+
kept.append(item)
|
|
173
|
+
policy["grants"] = kept
|
|
174
|
+
path = save_permissions(policy)
|
|
175
|
+
return {
|
|
176
|
+
"kind": "permissions",
|
|
177
|
+
"status": "revoked" if removed else "not-found",
|
|
178
|
+
"removed": [public_grant(item) for item in removed],
|
|
179
|
+
"json_path": str(path),
|
|
180
|
+
"markdown_path": str(permissions_md_path()),
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def permission_check(
|
|
185
|
+
*,
|
|
186
|
+
agent: str | None,
|
|
187
|
+
provider: str | None,
|
|
188
|
+
action: str,
|
|
189
|
+
project: str | None = None,
|
|
190
|
+
task_id: str | None = None,
|
|
191
|
+
) -> dict[str, Any]:
|
|
192
|
+
required = required_level_for_action(action)
|
|
193
|
+
resolved_provider = provider or provider_for_agent(agent)
|
|
194
|
+
policy = load_permissions()
|
|
195
|
+
grant = best_grant(
|
|
196
|
+
policy["grants"],
|
|
197
|
+
agent=agent,
|
|
198
|
+
provider=resolved_provider,
|
|
199
|
+
project=project,
|
|
200
|
+
task_id=task_id,
|
|
201
|
+
)
|
|
202
|
+
granted = normalize_level(grant.get("level") if grant else policy.get("default_level", DEFAULT_PERMISSION_LEVEL))
|
|
203
|
+
allowed = level_allows(granted, required)
|
|
204
|
+
return {
|
|
205
|
+
"kind": "permission-check",
|
|
206
|
+
"status": "allowed" if allowed else "blocked",
|
|
207
|
+
"ok": allowed,
|
|
208
|
+
"agent": agent,
|
|
209
|
+
"provider": resolved_provider,
|
|
210
|
+
"action": action,
|
|
211
|
+
"required_level": required,
|
|
212
|
+
"granted_level": granted,
|
|
213
|
+
"grant": public_grant(grant) if grant else None,
|
|
214
|
+
"default_level": policy.get("default_level", DEFAULT_PERMISSION_LEVEL),
|
|
215
|
+
"requires_permission": not allowed,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def assert_permission(
|
|
220
|
+
*,
|
|
221
|
+
agent: str | None,
|
|
222
|
+
provider: str | None,
|
|
223
|
+
action: str,
|
|
224
|
+
project: str | None = None,
|
|
225
|
+
task_id: str | None = None,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
return permission_check(agent=agent, provider=provider, action=action, project=project, task_id=task_id)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def provider_for_agent(agent: str | None) -> str | None:
|
|
231
|
+
return PROVIDER_BY_AGENT.get(agent or "")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def required_level_for_action(action: str) -> str:
|
|
235
|
+
return ACTION_REQUIRED_LEVEL.get(action, action if action in PERMISSION_LEVELS else "write-with-approval")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def normalize_level(level: Any) -> str:
|
|
239
|
+
return str(level) if str(level) in PERMISSION_LEVELS else DEFAULT_PERMISSION_LEVEL
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def level_allows(granted: str, required: str) -> bool:
|
|
243
|
+
validate_level(granted)
|
|
244
|
+
validate_level(required)
|
|
245
|
+
return PERMISSION_LEVELS.index(granted) >= PERMISSION_LEVELS.index(required)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def validate_level(level: str) -> None:
|
|
249
|
+
if level not in PERMISSION_LEVELS:
|
|
250
|
+
available = ", ".join(PERMISSION_LEVELS)
|
|
251
|
+
raise ValueError(f"unknown permission level: {level}. available: {available}")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def best_grant(
|
|
255
|
+
grants: list[Any],
|
|
256
|
+
*,
|
|
257
|
+
agent: str | None,
|
|
258
|
+
provider: str | None,
|
|
259
|
+
project: str | None,
|
|
260
|
+
task_id: str | None,
|
|
261
|
+
) -> dict[str, Any] | None:
|
|
262
|
+
candidates = [
|
|
263
|
+
item
|
|
264
|
+
for item in grants
|
|
265
|
+
if isinstance(item, dict)
|
|
266
|
+
and item.get("agent") == agent
|
|
267
|
+
and item.get("provider") == provider
|
|
268
|
+
and item.get("project") in {None, project}
|
|
269
|
+
and item.get("task_id") in {None, task_id}
|
|
270
|
+
]
|
|
271
|
+
if not candidates:
|
|
272
|
+
return None
|
|
273
|
+
candidates.sort(key=grant_specificity, reverse=True)
|
|
274
|
+
return candidates[0]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def find_grant(
|
|
278
|
+
grants: list[dict[str, Any]],
|
|
279
|
+
*,
|
|
280
|
+
agent: str,
|
|
281
|
+
provider: str,
|
|
282
|
+
project: str | None,
|
|
283
|
+
task_id: str | None,
|
|
284
|
+
) -> dict[str, Any] | None:
|
|
285
|
+
for item in grants:
|
|
286
|
+
if (
|
|
287
|
+
item.get("agent") == agent
|
|
288
|
+
and item.get("provider") == provider
|
|
289
|
+
and item.get("project") == project
|
|
290
|
+
and item.get("task_id") == task_id
|
|
291
|
+
):
|
|
292
|
+
return item
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def grant_specificity(grant: dict[str, Any]) -> tuple[int, int]:
|
|
297
|
+
scope = int(bool(grant.get("project"))) + int(bool(grant.get("task_id")))
|
|
298
|
+
return scope, PERMISSION_LEVELS.index(normalize_level(grant.get("level")))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def public_grant(grant: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
302
|
+
if not grant:
|
|
303
|
+
return None
|
|
304
|
+
return {
|
|
305
|
+
"agent": grant.get("agent"),
|
|
306
|
+
"provider": grant.get("provider"),
|
|
307
|
+
"level": grant.get("level"),
|
|
308
|
+
"project": grant.get("project"),
|
|
309
|
+
"task_id": grant.get("task_id"),
|
|
310
|
+
"created_at": grant.get("created_at"),
|
|
311
|
+
"updated_at": grant.get("updated_at"),
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def render_permissions_md(policy: dict[str, Any]) -> str:
|
|
316
|
+
lines = [
|
|
317
|
+
"# Agent DevKit Permissions",
|
|
318
|
+
"",
|
|
319
|
+
f"- Default level: `{redact_secrets(str(policy.get('default_level') or DEFAULT_PERMISSION_LEVEL))}`",
|
|
320
|
+
"",
|
|
321
|
+
"## Levels",
|
|
322
|
+
"",
|
|
323
|
+
]
|
|
324
|
+
for level in PERMISSION_LEVELS:
|
|
325
|
+
lines.append(f"- `{level}`")
|
|
326
|
+
lines.extend(["", "## Grants", ""])
|
|
327
|
+
grants = [item for item in policy.get("grants", []) if isinstance(item, dict)]
|
|
328
|
+
if not grants:
|
|
329
|
+
lines.append("- No explicit grants configured.")
|
|
330
|
+
for item in grants:
|
|
331
|
+
scope = []
|
|
332
|
+
if item.get("project"):
|
|
333
|
+
scope.append(f"project={item['project']}")
|
|
334
|
+
if item.get("task_id"):
|
|
335
|
+
scope.append(f"task={item['task_id']}")
|
|
336
|
+
scope_text = f" ({', '.join(scope)})" if scope else ""
|
|
337
|
+
lines.append(
|
|
338
|
+
"- "
|
|
339
|
+
f"`{redact_secrets(str(item.get('agent') or '-'))}` / "
|
|
340
|
+
f"`{redact_secrets(str(item.get('provider') or '-'))}` -> "
|
|
341
|
+
f"`{redact_secrets(str(item.get('level') or DEFAULT_PERMISSION_LEVEL))}`"
|
|
342
|
+
f"{scope_text}"
|
|
343
|
+
)
|
|
344
|
+
lines.append("")
|
|
345
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Local Agent DevKit personality persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from cli.aikit.identity import DEFAULT_PUBLIC_NAME
|
|
10
|
+
from cli.aikit.memory import MEMORY_FILE_TEMPLATES, ensure_memory, memory_home
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
FIELD_LABELS = {
|
|
14
|
+
"agent_name": "Name",
|
|
15
|
+
"user_name": "User name",
|
|
16
|
+
"language": "Language",
|
|
17
|
+
"tone": "Tone",
|
|
18
|
+
"detail_level": "Detail level",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def personality_path() -> Path:
|
|
23
|
+
ensure_memory()
|
|
24
|
+
return memory_home() / "personality.md"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_personality() -> dict[str, Any]:
|
|
28
|
+
path = personality_path()
|
|
29
|
+
text = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
30
|
+
return {
|
|
31
|
+
"kind": "personality",
|
|
32
|
+
"status": "ok",
|
|
33
|
+
"path": str(path),
|
|
34
|
+
"agent_name": parse_field(text, FIELD_LABELS["agent_name"]) or DEFAULT_PUBLIC_NAME,
|
|
35
|
+
"user_name": parse_field(text, FIELD_LABELS["user_name"]),
|
|
36
|
+
"language": parse_field(text, FIELD_LABELS["language"]),
|
|
37
|
+
"tone": parse_field(text, FIELD_LABELS["tone"]) or "direct",
|
|
38
|
+
"detail_level": parse_field(text, FIELD_LABELS["detail_level"]) or "concise",
|
|
39
|
+
"questions": setup_questions(),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def update_personality(
|
|
44
|
+
*,
|
|
45
|
+
agent_name: str | None = None,
|
|
46
|
+
user_name: str | None = None,
|
|
47
|
+
language: str | None = None,
|
|
48
|
+
tone: str | None = None,
|
|
49
|
+
detail_level: str | None = None,
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
current = load_personality()
|
|
52
|
+
values = {
|
|
53
|
+
"agent_name": clean_field(agent_name) or current.get("agent_name") or DEFAULT_PUBLIC_NAME,
|
|
54
|
+
"user_name": clean_field(user_name) if user_name is not None else current.get("user_name"),
|
|
55
|
+
"language": clean_field(language) if language is not None else current.get("language"),
|
|
56
|
+
"tone": clean_field(tone) or current.get("tone") or "direct",
|
|
57
|
+
"detail_level": clean_field(detail_level) or current.get("detail_level") or "concise",
|
|
58
|
+
}
|
|
59
|
+
path = personality_path()
|
|
60
|
+
path.write_text(render_personality(values), encoding="utf-8")
|
|
61
|
+
payload = load_personality()
|
|
62
|
+
payload["status"] = "updated"
|
|
63
|
+
return payload
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def reset_personality() -> dict[str, Any]:
|
|
67
|
+
path = personality_path()
|
|
68
|
+
path.write_text(MEMORY_FILE_TEMPLATES["personality.md"], encoding="utf-8")
|
|
69
|
+
payload = load_personality()
|
|
70
|
+
payload["status"] = "reset"
|
|
71
|
+
return payload
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def setup_personality() -> dict[str, Any]:
|
|
75
|
+
payload = load_personality()
|
|
76
|
+
payload["status"] = "needs-input"
|
|
77
|
+
payload["message"] = "Use `agent personality edit --name <nome>` to configure personality non-interactively."
|
|
78
|
+
return payload
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def render_personality(values: dict[str, Any]) -> str:
|
|
82
|
+
return f"""# Personality
|
|
83
|
+
|
|
84
|
+
Configured public identity and response style for Agent DevKit.
|
|
85
|
+
|
|
86
|
+
## Agent
|
|
87
|
+
|
|
88
|
+
- Name: {values.get("agent_name") or DEFAULT_PUBLIC_NAME}
|
|
89
|
+
- User name: {values.get("user_name") or ""}
|
|
90
|
+
- Language: {values.get("language") or ""}
|
|
91
|
+
|
|
92
|
+
## Style
|
|
93
|
+
|
|
94
|
+
- Tone: {values.get("tone") or "direct"}
|
|
95
|
+
- Detail level: {values.get("detail_level") or "concise"}
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def parse_field(text: str, label: str) -> str | None:
|
|
100
|
+
pattern = re.compile(rf"(?im)^[ \t]*-[ \t]*{re.escape(label)}[ \t]*:[ \t]*([^\r\n]*)[ \t]*$")
|
|
101
|
+
match = pattern.search(text)
|
|
102
|
+
if not match:
|
|
103
|
+
return None
|
|
104
|
+
value = match.group(1).strip()
|
|
105
|
+
return value or None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def clean_field(value: str | None) -> str | None:
|
|
109
|
+
if value is None:
|
|
110
|
+
return None
|
|
111
|
+
cleaned = " ".join(str(value).split())
|
|
112
|
+
return cleaned or None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def setup_questions() -> list[str]:
|
|
116
|
+
return [
|
|
117
|
+
"Como voce deseja que eu me chame?",
|
|
118
|
+
"Deseja criar um comando com esse nome para me chamar diretamente?",
|
|
119
|
+
"Como voce se chama?",
|
|
120
|
+
"Em qual idioma devo responder por padrao?",
|
|
121
|
+
"Voce prefere respostas curtas, detalhadas, criticas ou didaticas?",
|
|
122
|
+
"Qual backend LLM voce prefere como primario?",
|
|
123
|
+
]
|