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.
Files changed (172) hide show
  1. package/README.md +48 -6
  2. package/bin/agent.mjs +133 -7
  3. package/package.json +1 -1
  4. package/runtime/README.md +187 -5
  5. package/runtime/agent +31 -5
  6. package/runtime/agents/README.md +18 -0
  7. package/runtime/agents/contribution-reviewer/AGENTS.md +8 -0
  8. package/runtime/agents/contribution-reviewer/README.md +8 -0
  9. package/runtime/agents/contribution-reviewer/agent.yaml +40 -0
  10. package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/capability.yaml +27 -0
  11. package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/decision-rules.md +5 -0
  12. package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/workflow.md +6 -0
  13. package/runtime/agents/contribution-reviewer/capabilities/review-contribution/capability.yaml +25 -0
  14. package/runtime/agents/contribution-reviewer/capabilities/review-contribution/decision-rules.md +5 -0
  15. package/runtime/agents/contribution-reviewer/capabilities/review-contribution/workflow.md +5 -0
  16. package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/capability.yaml +26 -0
  17. package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/decision-rules.md +5 -0
  18. package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/workflow.md +6 -0
  19. package/runtime/agents/contribution-reviewer/infra/README.md +6 -0
  20. package/runtime/agents/contribution-reviewer/knowledge/context.md +8 -0
  21. package/runtime/agents/contribution-reviewer/knowledge/system.md +8 -0
  22. package/runtime/agents/contribution-reviewer/templates/README.md +3 -0
  23. package/runtime/agents/knowledge-author/AGENTS.md +7 -0
  24. package/runtime/agents/knowledge-author/README.md +7 -0
  25. package/runtime/agents/knowledge-author/agent.yaml +37 -0
  26. package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/capability.yaml +30 -0
  27. package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/decision-rules.md +6 -0
  28. package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/workflow.md +7 -0
  29. package/runtime/agents/knowledge-author/infra/.gitkeep +1 -0
  30. package/runtime/agents/knowledge-author/knowledge/context.md +4 -0
  31. package/runtime/agents/knowledge-author/knowledge/system.md +4 -0
  32. package/runtime/agents/knowledge-author/templates/.gitkeep +1 -0
  33. package/runtime/agents/knowledge-curator/AGENTS.md +7 -0
  34. package/runtime/agents/knowledge-curator/README.md +6 -0
  35. package/runtime/agents/knowledge-curator/agent.yaml +37 -0
  36. package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/capability.yaml +29 -0
  37. package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/decision-rules.md +6 -0
  38. package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/workflow.md +7 -0
  39. package/runtime/agents/knowledge-curator/infra/.gitkeep +1 -0
  40. package/runtime/agents/knowledge-curator/knowledge/context.md +4 -0
  41. package/runtime/agents/knowledge-curator/knowledge/system.md +4 -0
  42. package/runtime/agents/knowledge-curator/templates/.gitkeep +1 -0
  43. package/runtime/agents/knowledge-infra-builder/AGENTS.md +8 -0
  44. package/runtime/agents/knowledge-infra-builder/README.md +8 -0
  45. package/runtime/agents/knowledge-infra-builder/agent.yaml +38 -0
  46. package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/capability.yaml +30 -0
  47. package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/decision-rules.md +6 -0
  48. package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/workflow.md +7 -0
  49. package/runtime/agents/knowledge-infra-builder/infra/.gitkeep +1 -0
  50. package/runtime/agents/knowledge-infra-builder/knowledge/context.md +4 -0
  51. package/runtime/agents/knowledge-infra-builder/knowledge/system.md +4 -0
  52. package/runtime/agents/knowledge-infra-builder/templates/.gitkeep +1 -0
  53. package/runtime/agents/knowledge-owner/AGENTS.md +7 -0
  54. package/runtime/agents/knowledge-owner/README.md +6 -0
  55. package/runtime/agents/knowledge-owner/agent.yaml +37 -0
  56. package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/capability.yaml +28 -0
  57. package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/decision-rules.md +6 -0
  58. package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/workflow.md +7 -0
  59. package/runtime/agents/knowledge-owner/infra/.gitkeep +1 -0
  60. package/runtime/agents/knowledge-owner/knowledge/context.md +4 -0
  61. package/runtime/agents/knowledge-owner/knowledge/system.md +4 -0
  62. package/runtime/agents/knowledge-owner/templates/.gitkeep +1 -0
  63. package/runtime/agents/knowledge-reviewer/AGENTS.md +7 -0
  64. package/runtime/agents/knowledge-reviewer/README.md +7 -0
  65. package/runtime/agents/knowledge-reviewer/agent.yaml +36 -0
  66. package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/capability.yaml +26 -0
  67. package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/decision-rules.md +6 -0
  68. package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/workflow.md +7 -0
  69. package/runtime/agents/knowledge-reviewer/infra/.gitkeep +1 -0
  70. package/runtime/agents/knowledge-reviewer/knowledge/context.md +4 -0
  71. package/runtime/agents/knowledge-reviewer/knowledge/system.md +4 -0
  72. package/runtime/agents/knowledge-reviewer/templates/.gitkeep +1 -0
  73. package/runtime/agents/local-memory-manager/AGENTS.md +5 -0
  74. package/runtime/agents/local-memory-manager/README.md +7 -0
  75. package/runtime/agents/local-memory-manager/agent.yaml +38 -0
  76. package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/capability.yaml +19 -0
  77. package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/decision-rules.md +5 -0
  78. package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/workflow.md +6 -0
  79. package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/capability.yaml +19 -0
  80. package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/decision-rules.md +5 -0
  81. package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/workflow.md +5 -0
  82. package/runtime/agents/local-memory-manager/infra/.gitkeep +1 -0
  83. package/runtime/agents/local-memory-manager/knowledge/context.md +4 -0
  84. package/runtime/agents/local-memory-manager/knowledge/system.md +4 -0
  85. package/runtime/agents/local-memory-manager/templates/.gitkeep +1 -0
  86. package/runtime/agents/memory-sync-manager/AGENTS.md +7 -0
  87. package/runtime/agents/memory-sync-manager/README.md +7 -0
  88. package/runtime/agents/memory-sync-manager/agent.yaml +37 -0
  89. package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/capability.yaml +29 -0
  90. package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/decision-rules.md +6 -0
  91. package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/workflow.md +7 -0
  92. package/runtime/agents/memory-sync-manager/infra/.gitkeep +1 -0
  93. package/runtime/agents/memory-sync-manager/knowledge/context.md +4 -0
  94. package/runtime/agents/memory-sync-manager/knowledge/system.md +4 -0
  95. package/runtime/agents/memory-sync-manager/templates/.gitkeep +1 -0
  96. package/runtime/agents/shared-memory-curator/AGENTS.md +5 -0
  97. package/runtime/agents/shared-memory-curator/README.md +6 -0
  98. package/runtime/agents/shared-memory-curator/agent.yaml +38 -0
  99. package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/capability.yaml +19 -0
  100. package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/decision-rules.md +5 -0
  101. package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/workflow.md +5 -0
  102. package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/capability.yaml +19 -0
  103. package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/decision-rules.md +5 -0
  104. package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/workflow.md +5 -0
  105. package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/capability.yaml +19 -0
  106. package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/decision-rules.md +5 -0
  107. package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/workflow.md +5 -0
  108. package/runtime/agents/shared-memory-curator/infra/.gitkeep +1 -0
  109. package/runtime/agents/shared-memory-curator/knowledge/context.md +5 -0
  110. package/runtime/agents/shared-memory-curator/knowledge/system.md +4 -0
  111. package/runtime/agents/shared-memory-curator/templates/.gitkeep +1 -0
  112. package/runtime/cli/README.md +35 -4
  113. package/runtime/cli/aikit/__init__.py +1 -1
  114. package/runtime/cli/aikit/agent_registry.py +4 -2
  115. package/runtime/cli/aikit/agentic_commands.py +158 -0
  116. package/runtime/cli/aikit/app_home.py +1 -0
  117. package/runtime/cli/aikit/audit.py +16 -6
  118. package/runtime/cli/aikit/catalog.py +278 -8
  119. package/runtime/cli/aikit/cli_dispatch.py +489 -13
  120. package/runtime/cli/aikit/cli_parser.py +145 -7
  121. package/runtime/cli/aikit/contribution.py +132 -2
  122. package/runtime/cli/aikit/doctor_runtime.py +85 -0
  123. package/runtime/cli/aikit/eval.py +356 -10
  124. package/runtime/cli/aikit/human_output.py +310 -4
  125. package/runtime/cli/aikit/interactive_wizard.py +148 -0
  126. package/runtime/cli/aikit/knowledge_base.py +1067 -0
  127. package/runtime/cli/aikit/llm.py +12 -4
  128. package/runtime/cli/aikit/local_artifacts.py +444 -0
  129. package/runtime/cli/aikit/local_llm.py +161 -0
  130. package/runtime/cli/aikit/main.py +15 -0
  131. package/runtime/cli/aikit/mcp_manifest.py +798 -0
  132. package/runtime/cli/aikit/mcp_tools.py +643 -5
  133. package/runtime/cli/aikit/memory.py +405 -0
  134. package/runtime/cli/aikit/mini_brain.py +20 -1
  135. package/runtime/cli/aikit/natural_prompt_runtime.py +125 -1
  136. package/runtime/cli/aikit/ollama.py +64 -15
  137. package/runtime/cli/aikit/onboarding.py +551 -0
  138. package/runtime/cli/aikit/output.py +67 -0
  139. package/runtime/cli/aikit/prompt_injection.py +12 -1
  140. package/runtime/cli/aikit/roadmap_cli.py +1 -1
  141. package/runtime/cli/aikit/secrets.py +3 -2
  142. package/runtime/cli/aikit/setup_wizard_payload.py +3 -0
  143. package/runtime/cli/aikit/shared_memory.py +415 -0
  144. package/runtime/cli/aikit/specialist_readiness.py +152 -0
  145. package/runtime/cli/aikit/tasks.py +104 -1
  146. package/runtime/cli/aikit/team.py +380 -0
  147. package/runtime/cli/aikit/toolchain.py +7 -2
  148. package/runtime/cli/aikit/workflows.py +115 -14
  149. package/runtime/providers/knowledge-github.yaml +40 -0
  150. package/runtime/providers/knowledge-google-drive.yaml +32 -0
  151. package/runtime/providers/knowledge-local.yaml +26 -0
  152. package/runtime/providers/knowledge-notion.yaml +32 -0
  153. package/runtime/providers/knowledge-obsidian.yaml +24 -0
  154. package/runtime/providers/knowledge-onedrive.yaml +36 -0
  155. package/runtime/providers/knowledge-s3.yaml +45 -0
  156. package/runtime/providers/knowledge-sharepoint.yaml +39 -0
  157. package/runtime/providers/knowledge-supabase.yaml +43 -0
  158. package/runtime/providers/knowledge-vector.yaml +39 -0
  159. package/runtime/requirements.txt +6 -0
  160. package/runtime/scripts/docker-cli-qa.sh +453 -0
  161. package/runtime/scripts/release-catalog-snapshot.json +55 -4
  162. package/runtime/scripts/release-gate.py +54 -13
  163. package/runtime/tooling/toolchain.yaml +92 -0
  164. package/runtime/vendor/skills/napkin/napkin.md +21 -7
  165. package/runtime/workflows/azure-card-analysis/README.md +3 -0
  166. package/runtime/workflows/azure-card-analysis/workflow.yaml +30 -0
  167. package/runtime/workflows/daily-pr-review/README.md +3 -0
  168. package/runtime/workflows/daily-pr-review/workflow.yaml +31 -0
  169. package/runtime/workflows/incident-analysis/README.md +3 -0
  170. package/runtime/workflows/incident-analysis/workflow.yaml +33 -0
  171. package/runtime/workflows/release-prep/README.md +3 -0
  172. package/runtime/workflows/release-prep/workflow.yaml +30 -0
@@ -4,6 +4,14 @@ from __future__ import annotations
4
4
 
5
5
  import re
6
6
  import shutil
7
+ import hashlib
8
+ import hmac
9
+ import base64
10
+ import io
11
+ import json
12
+ import os
13
+ import tarfile
14
+ import tempfile
7
15
  from datetime import datetime, timezone
8
16
  from pathlib import Path
9
17
  from typing import Any
@@ -95,12 +103,18 @@ Curated local runbook entries promoted from repeated use.
95
103
  - Keep high-value reusable notes here.
96
104
  """,
97
105
  }
106
+ BACKUP_PACKAGE_SCHEMA_VERSION = "agent-devkit.memory-backup-package/v1"
107
+ BACKUP_PACKAGE_ALGORITHM = "PBKDF2-HMAC-SHA256/XOR-HMAC-SHA256"
98
108
 
99
109
 
100
110
  def memory_home() -> Path:
101
111
  return app_memory_home()
102
112
 
103
113
 
114
+ def memory_backups_home() -> Path:
115
+ return app_path("backups", "memory")
116
+
117
+
104
118
  def ensure_memory() -> dict[str, Any]:
105
119
  ensure_app_home()
106
120
  home = memory_home()
@@ -193,6 +207,183 @@ def show_memory(root: Path, *, agent_id: str | None = None, source_id: str | Non
193
207
  }
194
208
 
195
209
 
210
+ def create_memory_backup(
211
+ *,
212
+ title: str | None = None,
213
+ encrypted: bool = False,
214
+ passphrase_env: str | None = None,
215
+ ) -> dict[str, Any]:
216
+ memory_paths = ensure_memory()
217
+ backups = memory_backups_home()
218
+ backups.mkdir(parents=True, exist_ok=True)
219
+ backup_id = unique_backup_id(title)
220
+ backup_root = backups / backup_id
221
+ backup_root.mkdir(parents=True, exist_ok=False)
222
+ backup_memory = backup_root / ("_memory-staging" if encrypted else "memory")
223
+ shutil.copytree(memory_home(), backup_memory)
224
+
225
+ files = backup_file_inventory(backup_memory)
226
+ manifest = {
227
+ "schema_version": "agent-devkit.memory-backup/v1",
228
+ "id": backup_id,
229
+ "title": title or "Memory backup",
230
+ "created_at": datetime.now(timezone.utc).isoformat(),
231
+ "source_home": str(memory_home()),
232
+ "storage": "local-filesystem",
233
+ "remote_upload": False,
234
+ "encrypted": encrypted,
235
+ "sensitive_local_copy": not encrypted,
236
+ "files": files,
237
+ "package": None,
238
+ "notes": [
239
+ "This is a local backup only; no remote provider upload was executed.",
240
+ "Remote backup must be encrypted before upload and requires explicit opt-in.",
241
+ ],
242
+ }
243
+ if encrypted:
244
+ package_path = backup_root / f"{backup_id}.adkmb"
245
+ passphrase = passphrase_from_env(passphrase_env)
246
+ write_encrypted_backup_package(package_path, backup_memory, manifest, passphrase)
247
+ manifest["package"] = str(package_path)
248
+ manifest["package_name"] = package_path.name
249
+ shutil.rmtree(backup_memory)
250
+ manifest_path = backup_root / "manifest.json"
251
+ manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
252
+ next_steps = [
253
+ f"Restore with `agent memory backup restore {backup_id} --yes`.",
254
+ f"Delete with `agent memory backup delete {backup_id} --yes`.",
255
+ ]
256
+ if encrypted:
257
+ next_steps[0] = f"Restore with `agent memory backup restore {backup_id} --passphrase-env {passphrase_env or 'AGENT_DEVKIT_BACKUP_PASSPHRASE'} --yes`."
258
+ next_steps.insert(1, f"Portable package: {manifest['package']}")
259
+ return {
260
+ "kind": "memory-backup",
261
+ "status": "created",
262
+ "backup": public_backup(manifest, backup_root),
263
+ "home": str(backups),
264
+ "memory_home": memory_paths["home"],
265
+ "next_steps": next_steps,
266
+ }
267
+
268
+
269
+ def list_memory_backups() -> dict[str, Any]:
270
+ backups = memory_backups_home()
271
+ items = []
272
+ if backups.exists():
273
+ for manifest_path in sorted(backups.glob("*/manifest.json")):
274
+ manifest = load_backup_manifest(manifest_path)
275
+ if manifest:
276
+ items.append(public_backup(manifest, manifest_path.parent))
277
+ return {
278
+ "kind": "memory-backups",
279
+ "status": "ok",
280
+ "home": str(backups),
281
+ "count": len(items),
282
+ "items": items,
283
+ }
284
+
285
+
286
+ def restore_memory_backup(
287
+ backup_id: str | None,
288
+ *,
289
+ yes: bool = False,
290
+ backup_file: str | None = None,
291
+ passphrase_env: str | None = None,
292
+ ) -> dict[str, Any]:
293
+ backup_root, manifest = require_memory_backup(backup_id, backup_file=backup_file, passphrase_env=passphrase_env)
294
+ payload = {
295
+ "kind": "memory-backup-restore",
296
+ "backup": public_backup(manifest, backup_root),
297
+ "memory_home": str(memory_home()),
298
+ "requires_confirmation": True,
299
+ }
300
+ if not yes:
301
+ return {
302
+ **payload,
303
+ "status": "planned",
304
+ "executed": False,
305
+ "next_steps": [restore_next_step(manifest, backup_file=backup_file, passphrase_env=passphrase_env)],
306
+ }
307
+
308
+ ensure_app_home()
309
+ safety_backup: str | None = None
310
+ with decrypted_memory_source(backup_root, manifest, passphrase_env=passphrase_env) as source_memory:
311
+ if memory_home().exists():
312
+ safety_root = memory_backups_home() / f"pre-restore-{timestamp_id()}"
313
+ safety_root.mkdir(parents=True, exist_ok=False)
314
+ shutil.copytree(memory_home(), safety_root / "memory")
315
+ safety_backup = str(safety_root)
316
+ shutil.rmtree(memory_home())
317
+ shutil.copytree(source_memory, memory_home())
318
+ return {
319
+ **payload,
320
+ "status": "restored",
321
+ "executed": True,
322
+ "safety_backup": safety_backup,
323
+ }
324
+
325
+
326
+ class decrypted_memory_source:
327
+ def __init__(self, backup_root: Path, manifest: dict[str, Any], *, passphrase_env: str | None = None) -> None:
328
+ self.backup_root = backup_root
329
+ self.manifest = manifest
330
+ self.passphrase_env = passphrase_env
331
+ self.temp_dir: tempfile.TemporaryDirectory[str] | None = None
332
+
333
+ def __enter__(self) -> Path:
334
+ plain = self.backup_root / "memory"
335
+ if plain.exists():
336
+ return plain
337
+ package_path = package_path_from_manifest(self.backup_root, self.manifest)
338
+ if not package_path:
339
+ raise ValueError(f"memory backup has no restorable memory payload: {self.manifest.get('id')}")
340
+ passphrase = passphrase_from_env(self.passphrase_env)
341
+ self.temp_dir = tempfile.TemporaryDirectory(prefix="agent-devkit-memory-restore-")
342
+ target = Path(self.temp_dir.name)
343
+ extract_encrypted_backup_package(package_path, target, passphrase)
344
+ memory = target / "memory"
345
+ if not memory.exists():
346
+ raise ValueError("encrypted memory backup package did not contain memory/")
347
+ return memory
348
+
349
+ def __exit__(self, _exc_type: object, _exc: object, _tb: object) -> None:
350
+ if self.temp_dir:
351
+ self.temp_dir.cleanup()
352
+
353
+
354
+ def restore_next_step(manifest: dict[str, Any], *, backup_file: str | None, passphrase_env: str | None) -> str:
355
+ if manifest.get("encrypted"):
356
+ env_name = passphrase_env or "AGENT_DEVKIT_BACKUP_PASSPHRASE"
357
+ if backup_file:
358
+ return f"Re-run with `agent memory backup restore --file {backup_file} --passphrase-env {env_name} --yes` to restore local memory."
359
+ return f"Re-run with `agent memory backup restore {manifest['id']} --passphrase-env {env_name} --yes` to restore local memory."
360
+ if backup_file:
361
+ return f"Re-run with `agent memory backup restore --file {backup_file} --yes` to restore local memory."
362
+ return f"Re-run with `agent memory backup restore {manifest['id']} --yes` to restore local memory."
363
+
364
+
365
+ def delete_memory_backup(backup_id: str | None, *, yes: bool = False) -> dict[str, Any]:
366
+ backup_root, manifest = require_memory_backup(backup_id)
367
+ payload = {
368
+ "kind": "memory-backup-delete",
369
+ "backup": public_backup(manifest, backup_root),
370
+ "requires_confirmation": True,
371
+ }
372
+ if not yes:
373
+ return {
374
+ **payload,
375
+ "status": "planned",
376
+ "executed": False,
377
+ "next_steps": [f"Re-run with `agent memory backup delete {manifest['id']} --yes` to remove this local backup."],
378
+ }
379
+ shutil.rmtree(backup_root)
380
+ return {
381
+ **payload,
382
+ "status": "deleted",
383
+ "executed": True,
384
+ }
385
+
386
+
196
387
  def reset_memory(
197
388
  *,
198
389
  all_memory: bool = False,
@@ -246,6 +437,220 @@ def memory_path_payload() -> dict[str, Any]:
246
437
  return ensure_memory()
247
438
 
248
439
 
440
+ def unique_backup_id(title: str | None) -> str:
441
+ base = sanitize_segment(title or "memory-backup")
442
+ candidate = f"{base}-{timestamp_id()}"
443
+ backups = memory_backups_home()
444
+ if not (backups / candidate).exists():
445
+ return candidate
446
+ suffix = 2
447
+ while (backups / f"{candidate}-{suffix}").exists():
448
+ suffix += 1
449
+ return f"{candidate}-{suffix}"
450
+
451
+
452
+ def timestamp_id() -> str:
453
+ return datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
454
+
455
+
456
+ def backup_file_inventory(root: Path) -> list[dict[str, Any]]:
457
+ items: list[dict[str, Any]] = []
458
+ for path in sorted(item for item in root.rglob("*") if item.is_file()):
459
+ relative = str(path.relative_to(root))
460
+ data = path.read_bytes()
461
+ items.append(
462
+ {
463
+ "path": relative,
464
+ "bytes": len(data),
465
+ "sha256": hashlib.sha256(data).hexdigest(),
466
+ }
467
+ )
468
+ return items
469
+
470
+
471
+ def public_backup(manifest: dict[str, Any], root: Path) -> dict[str, Any]:
472
+ files = manifest.get("files") if isinstance(manifest.get("files"), list) else []
473
+ return {
474
+ "id": manifest.get("id"),
475
+ "title": manifest.get("title"),
476
+ "created_at": manifest.get("created_at"),
477
+ "path": str(root),
478
+ "storage": manifest.get("storage"),
479
+ "remote_upload": manifest.get("remote_upload") is True,
480
+ "encrypted": manifest.get("encrypted") is True,
481
+ "sensitive_local_copy": manifest.get("sensitive_local_copy") is True,
482
+ "package": manifest.get("package"),
483
+ "package_name": manifest.get("package_name"),
484
+ "file_count": len(files),
485
+ }
486
+
487
+
488
+ def require_memory_backup(
489
+ backup_id: str | None,
490
+ *,
491
+ backup_file: str | None = None,
492
+ passphrase_env: str | None = None,
493
+ ) -> tuple[Path, dict[str, Any]]:
494
+ if backup_file:
495
+ package = Path(backup_file).expanduser().resolve()
496
+ manifest = read_encrypted_backup_header(package, passphrase_env=passphrase_env)
497
+ return package.parent, manifest
498
+ item_id = sanitize_segment(backup_id or "")
499
+ if not item_id:
500
+ raise ValueError("memory backup requires a backup id")
501
+ root = memory_backups_home() / item_id
502
+ manifest_path = root / "manifest.json"
503
+ if not manifest_path.exists():
504
+ raise ValueError(f"memory backup not found: {item_id}")
505
+ manifest = load_backup_manifest(manifest_path)
506
+ if not manifest:
507
+ raise ValueError(f"invalid memory backup manifest: {item_id}")
508
+ return root, manifest
509
+
510
+
511
+ def load_backup_manifest(path: Path) -> dict[str, Any] | None:
512
+ try:
513
+ payload = json.loads(path.read_text(encoding="utf-8"))
514
+ except (OSError, json.JSONDecodeError):
515
+ return None
516
+ return payload if isinstance(payload, dict) else None
517
+
518
+
519
+ def passphrase_from_env(passphrase_env: str | None) -> str:
520
+ env_name = passphrase_env or "AGENT_DEVKIT_BACKUP_PASSPHRASE"
521
+ if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", env_name):
522
+ raise ValueError("--passphrase-env must be an environment variable name")
523
+ value = os.environ.get(env_name)
524
+ if not value:
525
+ raise ValueError(f"memory backup passphrase environment variable is not set: {env_name}")
526
+ if len(value) < 8:
527
+ raise ValueError("memory backup passphrase must have at least 8 characters")
528
+ return value
529
+
530
+
531
+ def write_encrypted_backup_package(package_path: Path, memory_dir: Path, manifest: dict[str, Any], passphrase: str) -> None:
532
+ tar_bytes = memory_tar_bytes(memory_dir)
533
+ salt = os.urandom(16)
534
+ nonce = os.urandom(16)
535
+ key = derive_backup_key(passphrase, salt)
536
+ ciphertext = xor_bytes(tar_bytes, key, nonce)
537
+ header = {
538
+ "schema_version": BACKUP_PACKAGE_SCHEMA_VERSION,
539
+ "algorithm": BACKUP_PACKAGE_ALGORITHM,
540
+ "kdf": "PBKDF2-HMAC-SHA256",
541
+ "iterations": 200_000,
542
+ "salt": base64.b64encode(salt).decode("ascii"),
543
+ "nonce": base64.b64encode(nonce).decode("ascii"),
544
+ "manifest": {key: value for key, value in manifest.items() if key != "files"},
545
+ "files": manifest.get("files") or [],
546
+ }
547
+ header_bytes = json.dumps(header, ensure_ascii=False, sort_keys=True).encode("utf-8")
548
+ tag = hmac.new(key, header_bytes + ciphertext, hashlib.sha256).hexdigest()
549
+ envelope = {
550
+ "schema_version": BACKUP_PACKAGE_SCHEMA_VERSION,
551
+ "header": header,
552
+ "ciphertext": base64.b64encode(ciphertext).decode("ascii"),
553
+ "tag": tag,
554
+ }
555
+ package_path.write_text(json.dumps(envelope, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
556
+
557
+
558
+ def read_encrypted_backup_header(package_path: Path, *, passphrase_env: str | None = None) -> dict[str, Any]:
559
+ envelope = read_backup_package(package_path)
560
+ header = envelope["header"]
561
+ manifest = header.get("manifest") if isinstance(header.get("manifest"), dict) else {}
562
+ payload = dict(manifest)
563
+ payload["files"] = header.get("files") if isinstance(header.get("files"), list) else []
564
+ payload["encrypted"] = True
565
+ payload["sensitive_local_copy"] = False
566
+ payload["package"] = str(package_path)
567
+ payload["package_name"] = package_path.name
568
+ payload.setdefault("id", package_path.stem)
569
+ payload.setdefault("title", package_path.stem)
570
+ return payload
571
+
572
+
573
+ def extract_encrypted_backup_package(package_path: Path, target: Path, passphrase: str) -> None:
574
+ envelope = read_backup_package(package_path)
575
+ header = envelope["header"]
576
+ salt = base64.b64decode(header["salt"])
577
+ nonce = base64.b64decode(header["nonce"])
578
+ ciphertext = base64.b64decode(envelope["ciphertext"])
579
+ key = derive_backup_key(passphrase, salt)
580
+ header_bytes = json.dumps(header, ensure_ascii=False, sort_keys=True).encode("utf-8")
581
+ expected_tag = hmac.new(key, header_bytes + ciphertext, hashlib.sha256).hexdigest()
582
+ if not hmac.compare_digest(expected_tag, str(envelope.get("tag") or "")):
583
+ raise ValueError("memory backup package integrity check failed")
584
+ tar_bytes = xor_bytes(ciphertext, key, nonce)
585
+ extract_memory_tar(tar_bytes, target)
586
+
587
+
588
+ def read_backup_package(package_path: Path) -> dict[str, Any]:
589
+ if not package_path.exists():
590
+ raise ValueError(f"memory backup package not found: {package_path}")
591
+ try:
592
+ envelope = json.loads(package_path.read_text(encoding="utf-8"))
593
+ except (OSError, json.JSONDecodeError) as exc:
594
+ raise ValueError(f"invalid memory backup package: {package_path}") from exc
595
+ if not isinstance(envelope, dict) or envelope.get("schema_version") != BACKUP_PACKAGE_SCHEMA_VERSION:
596
+ raise ValueError(f"unsupported memory backup package: {package_path}")
597
+ header = envelope.get("header")
598
+ if not isinstance(header, dict) or header.get("algorithm") != BACKUP_PACKAGE_ALGORITHM:
599
+ raise ValueError(f"unsupported memory backup package algorithm: {package_path}")
600
+ return envelope
601
+
602
+
603
+ def memory_tar_bytes(memory_dir: Path) -> bytes:
604
+ buffer = io.BytesIO()
605
+ with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
606
+ for path in sorted(item for item in memory_dir.rglob("*") if item.is_file()):
607
+ archive.add(path, arcname=str(Path("memory") / path.relative_to(memory_dir)))
608
+ return buffer.getvalue()
609
+
610
+
611
+ def extract_memory_tar(payload: bytes, target: Path) -> None:
612
+ target.mkdir(parents=True, exist_ok=True)
613
+ with tarfile.open(fileobj=io.BytesIO(payload), mode="r:gz") as archive:
614
+ for member in archive.getmembers():
615
+ member_path = Path(member.name)
616
+ if member_path.is_absolute() or ".." in member_path.parts or not member.name.startswith("memory/"):
617
+ raise ValueError(f"unsafe path in memory backup package: {member.name}")
618
+ if not member.isfile():
619
+ continue
620
+ output = target / member_path
621
+ output.parent.mkdir(parents=True, exist_ok=True)
622
+ extracted = archive.extractfile(member)
623
+ if extracted is None:
624
+ continue
625
+ output.write_bytes(extracted.read())
626
+
627
+
628
+ def derive_backup_key(passphrase: str, salt: bytes) -> bytes:
629
+ return hashlib.pbkdf2_hmac("sha256", passphrase.encode("utf-8"), salt, 200_000, dklen=32)
630
+
631
+
632
+ def xor_bytes(payload: bytes, key: bytes, nonce: bytes) -> bytes:
633
+ output = bytearray()
634
+ counter = 0
635
+ for index in range(0, len(payload), 32):
636
+ block = payload[index : index + 32]
637
+ stream = hmac.new(key, nonce + counter.to_bytes(8, "big"), hashlib.sha256).digest()
638
+ output.extend(byte ^ stream[offset] for offset, byte in enumerate(block))
639
+ counter += 1
640
+ return bytes(output)
641
+
642
+
643
+ def package_path_from_manifest(backup_root: Path, manifest: dict[str, Any]) -> Path | None:
644
+ raw = manifest.get("package")
645
+ if isinstance(raw, str) and raw:
646
+ return Path(raw).expanduser().resolve()
647
+ package_name = manifest.get("package_name")
648
+ if isinstance(package_name, str) and package_name:
649
+ return backup_root / package_name
650
+ candidates = sorted(backup_root.glob("*.adkmb"))
651
+ return candidates[0] if candidates else None
652
+
653
+
249
654
  def increment_bucket(bucket: dict[str, Any], key: str, now: str) -> None:
250
655
  item = bucket.setdefault(key, {"count": 0, "first_seen": now, "last_seen": now})
251
656
  item["count"] = int(item.get("count") or 0) + 1
@@ -100,10 +100,12 @@ def setup_mini_brain(
100
100
  ) -> dict[str, Any]:
101
101
  if dry_run or not yes:
102
102
  status = "planned" if dry_run else "needs-confirmation"
103
+ needs_confirmation = not dry_run and not yes
103
104
  return {
104
105
  "kind": "mini-brain-setup",
105
106
  "status": status,
106
107
  "ok": bool(dry_run),
108
+ "exit_code": 2 if needs_confirmation else 0,
107
109
  "dry_run": dry_run,
108
110
  "yes": yes,
109
111
  "stored_secret": False,
@@ -114,11 +116,19 @@ def setup_mini_brain(
114
116
  }
115
117
 
116
118
  pull = ollama_pull(model, yes=True, dry_run=False)
119
+ toolchain_install = None
120
+ if pull.get("status") == "missing":
121
+ from cli.aikit.toolchain import install_toolchain
122
+
123
+ toolchain_install = install_toolchain(None, "ollama", dry_run=False, yes=True)
124
+ if toolchain_install.get("status") == "installed":
125
+ pull = ollama_pull(model, yes=True, dry_run=False)
117
126
  if not pull.get("ok"):
118
- return {
127
+ payload = {
119
128
  "kind": "mini-brain-setup",
120
129
  "status": "failed",
121
130
  "ok": False,
131
+ "exit_code": int(pull.get("exit_code") or 2),
122
132
  "dry_run": False,
123
133
  "yes": True,
124
134
  "stored_secret": False,
@@ -127,6 +137,14 @@ def setup_mini_brain(
127
137
  "next_steps": ["Install Ollama or run agent ollama pull qwen3:0.6b --yes"],
128
138
  "message": pull.get("message") or "Could not pull the mini-brain model.",
129
139
  }
140
+ if toolchain_install:
141
+ payload["toolchain_install"] = toolchain_install
142
+ payload["next_steps"] = [
143
+ "Review `agent toolchain doctor ollama`.",
144
+ "Run `agent toolchain install ollama --yes` if you approve external installation.",
145
+ "Then run `agent setup mini-brain --yes` again.",
146
+ ]
147
+ return payload
130
148
 
131
149
  existing_config = load_config()
132
150
  existing_ollama = (
@@ -155,6 +173,7 @@ def setup_mini_brain(
155
173
  "config_path": str(written_path),
156
174
  "mini_brain": contract,
157
175
  "pull": pull,
176
+ "toolchain_install": toolchain_install,
158
177
  "llm_configure": configured,
159
178
  "next_steps": ["Use low-risk setup, wizard and summary prompts normally."],
160
179
  }
@@ -3,9 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import re
6
7
  from pathlib import Path
7
8
  from typing import Any
8
9
 
10
+ from cli.aikit.agent_registry import load_agent_registry
9
11
  from cli.aikit.agent_executor import execute_plan_tasks
10
12
  from cli.aikit.capability_runtime import load_agent, run_capability
11
13
  from cli.aikit.calendar import calendar_summary, calendar_today, calendar_tomorrow
@@ -22,7 +24,7 @@ from cli.aikit.memory import napkin_context, record_usage
22
24
  from cli.aikit.model_router import build_model_plan
23
25
  from cli.aikit.module_controller import run_module_controller
24
26
  from cli.aikit.orchestrator import attach_source_to_primary_task, build_execution_plan, mark_plan_after_execution, mark_review_task
25
- from cli.aikit.personality import load_personality
27
+ from cli.aikit.personality import load_personality, update_personality
26
28
  from cli.aikit.provider_wizard import missing_source_wizard
27
29
  from cli.aikit.review_gate import build_review_gate
28
30
  from cli.aikit.router import route_prompt
@@ -36,6 +38,106 @@ def effective_dry_run(args: argparse.Namespace) -> bool:
36
38
  return bool(getattr(args, "dry_run", False) or getattr(args, "global_dry_run", False))
37
39
 
38
40
 
41
+ def parse_rename_prompt(prompt: str) -> str | None:
42
+ text = " ".join(prompt.strip().split())
43
+ patterns = [
44
+ r"(?i)\b(?:mude|troque|altere|renomeie)\s+(?:o\s+)?seu\s+nome\s+para\s+(.+)$",
45
+ r"(?i)\b(?:seu\s+nome\s+agora\s+e|seu\s+nome\s+agora\s+é)\s+(.+)$",
46
+ r"(?i)\b(?:teu\s+nome\s+agora\s+e|teu\s+nome\s+agora\s+é)\s+(.+)$",
47
+ ]
48
+ for pattern in patterns:
49
+ match = re.search(pattern, text)
50
+ if not match:
51
+ continue
52
+ value = clean_requested_name(match.group(1))
53
+ if value:
54
+ return value
55
+ return None
56
+
57
+
58
+ def clean_requested_name(value: str) -> str | None:
59
+ cleaned = " ".join(value.strip().strip("\"'`.,;:!").split())
60
+ if not cleaned:
61
+ return None
62
+ return cleaned[:80]
63
+
64
+
65
+ def is_capabilities_help_prompt(prompt: str) -> bool:
66
+ normalized = normalize_text(prompt)
67
+ if normalized in {"ajuda", "help", "o que voce faz", "o que voce consegue fazer"}:
68
+ return True
69
+ capability_markers = (
70
+ "o que voce consegue fazer",
71
+ "o que voce pode fazer",
72
+ "como voce pode ajudar",
73
+ "como usar o agent",
74
+ "como usar voce",
75
+ "quais suas capacidades",
76
+ "quais sao suas capacidades",
77
+ "quais agentes voce tem",
78
+ "que agentes voce tem",
79
+ )
80
+ return any(marker in normalized for marker in capability_markers)
81
+
82
+
83
+ def normalize_text(value: str) -> str:
84
+ replacements = str.maketrans(
85
+ {
86
+ "á": "a",
87
+ "à": "a",
88
+ "ã": "a",
89
+ "â": "a",
90
+ "é": "e",
91
+ "ê": "e",
92
+ "í": "i",
93
+ "ó": "o",
94
+ "õ": "o",
95
+ "ô": "o",
96
+ "ú": "u",
97
+ "ç": "c",
98
+ }
99
+ )
100
+ text = value.lower().translate(replacements)
101
+ return " ".join(re.sub(r"[^a-z0-9._:-]+", " ", text).split())
102
+
103
+
104
+ def local_capabilities_help_response(prompt: str, *, name: str) -> dict[str, Any]:
105
+ registry = load_agent_registry(ROOT)
106
+ agents = registry.get("agents") if isinstance(registry.get("agents"), dict) else {}
107
+ capabilities = registry.get("capabilities") if isinstance(registry.get("capabilities"), dict) else {}
108
+ examples = [
109
+ 'agent "analise o card 7914 do projeto sustentacao no azure"',
110
+ 'agent "mude seu nome para Ianota"',
111
+ "agent onboard minimal",
112
+ "agent catalog search pr",
113
+ "agent mcp tools",
114
+ "agent memory show",
115
+ ]
116
+ response = (
117
+ f"Eu sou {name}. Posso operar como harness/agente local do Agent DevKit: "
118
+ "validar onboarding, lembrar preferencias locais, rotear pedidos para agentes especialistas, "
119
+ "criar wizards de configuracao quando faltar provider/source, executar capabilities deterministicas, "
120
+ "expor ferramentas via MCP, preparar automacoes locais e usar LLMs configuradas quando a tarefa exigir raciocinio aberto. "
121
+ f"Neste runtime encontrei {len(agents)} agentes e {len(capabilities)} capabilities."
122
+ )
123
+ return {
124
+ "kind": "agent",
125
+ "status": "ok",
126
+ "ok": True,
127
+ "requires_llm": False,
128
+ "prompt_received": True,
129
+ "prompt_length": len(prompt),
130
+ "mode": "local-capabilities-help",
131
+ "identity": {"name": name, "source": "local"},
132
+ "response": response,
133
+ "catalog": {
134
+ "agents": len(agents),
135
+ "capabilities": len(capabilities),
136
+ },
137
+ "next_steps": examples,
138
+ }
139
+
140
+
39
141
  def agent_requires_llm(args: argparse.Namespace) -> dict[str, Any]:
40
142
  prompt = " ".join(args.prompt).strip()
41
143
  return run_agent_prompt_request(
@@ -70,6 +172,21 @@ def run_agent_prompt_request(request: AgentPromptRequest) -> dict[str, Any]:
70
172
  raise DevKitError(str(exc)) from exc
71
173
  personality = load_personality()
72
174
  name = public_name(personality=personality, invoked_as=request.prog_name)
175
+ rename = parse_rename_prompt(prompt)
176
+ if rename:
177
+ updated = update_personality(agent_name=rename)
178
+ result = {
179
+ "kind": "agent",
180
+ "status": "ok",
181
+ "ok": True,
182
+ "requires_llm": False,
183
+ "prompt_received": True,
184
+ "prompt_length": len(prompt),
185
+ "identity": {"name": updated.get("agent_name"), "source": "local"},
186
+ "action": "rename",
187
+ "response": f"Pronto. Meu nome local agora e {updated.get('agent_name')}.",
188
+ }
189
+ return finalize_agent_session(result, session, prompt, backend=request.llm)
73
190
  if is_identity_question(prompt):
74
191
  result = {
75
192
  "kind": "agent",
@@ -82,6 +199,13 @@ def run_agent_prompt_request(request: AgentPromptRequest) -> dict[str, Any]:
82
199
  "response": local_identity_response(prompt, name=name),
83
200
  }
84
201
  return finalize_agent_session(result, session, prompt, backend=request.llm)
202
+ if is_capabilities_help_prompt(prompt):
203
+ return finalize_agent_session(
204
+ local_capabilities_help_response(prompt, name=name),
205
+ session,
206
+ prompt,
207
+ backend=request.llm,
208
+ )
85
209
  natural_result = dispatch_natural_operational_prompt(prompt)
86
210
  if natural_result:
87
211
  return finalize_agent_session(natural_result, session, prompt, backend=request.llm)