agent-devkit 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +66 -13
  2. package/bin/agent.mjs +133 -7
  3. package/package.json +1 -1
  4. package/runtime/README.md +205 -13
  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 +47 -8
  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 +2 -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 +146 -8
  121. package/runtime/cli/aikit/contribution.py +132 -2
  122. package/runtime/cli/aikit/doctor_runtime.py +85 -0
  123. package/runtime/cli/aikit/embedded_mini_brain.py +351 -0
  124. package/runtime/cli/aikit/eval.py +356 -10
  125. package/runtime/cli/aikit/human_output.py +310 -4
  126. package/runtime/cli/aikit/interactive_wizard.py +146 -0
  127. package/runtime/cli/aikit/knowledge_base.py +1067 -0
  128. package/runtime/cli/aikit/llm.py +40 -6
  129. package/runtime/cli/aikit/local_artifacts.py +444 -0
  130. package/runtime/cli/aikit/local_llm.py +176 -0
  131. package/runtime/cli/aikit/local_llm_operator.py +15 -5
  132. package/runtime/cli/aikit/main.py +15 -0
  133. package/runtime/cli/aikit/mcp_manifest.py +798 -0
  134. package/runtime/cli/aikit/mcp_tools.py +643 -5
  135. package/runtime/cli/aikit/memory.py +405 -0
  136. package/runtime/cli/aikit/mini_brain.py +56 -25
  137. package/runtime/cli/aikit/model_router.py +42 -9
  138. package/runtime/cli/aikit/natural_prompt_runtime.py +194 -2
  139. package/runtime/cli/aikit/ollama.py +64 -15
  140. package/runtime/cli/aikit/onboarding.py +551 -0
  141. package/runtime/cli/aikit/output.py +67 -0
  142. package/runtime/cli/aikit/prompt_injection.py +12 -1
  143. package/runtime/cli/aikit/review_gate.py +14 -2
  144. package/runtime/cli/aikit/roadmap_cli.py +1 -1
  145. package/runtime/cli/aikit/secrets.py +3 -2
  146. package/runtime/cli/aikit/setup_wizard_payload.py +3 -0
  147. package/runtime/cli/aikit/shared_memory.py +415 -0
  148. package/runtime/cli/aikit/specialist_readiness.py +152 -0
  149. package/runtime/cli/aikit/tasks.py +104 -1
  150. package/runtime/cli/aikit/team.py +380 -0
  151. package/runtime/cli/aikit/toolchain.py +7 -2
  152. package/runtime/cli/aikit/workflows.py +115 -14
  153. package/runtime/models/qwen2.5-0.5b-instruct/manifest.json +30 -0
  154. package/runtime/providers/knowledge-github.yaml +40 -0
  155. package/runtime/providers/knowledge-google-drive.yaml +32 -0
  156. package/runtime/providers/knowledge-local.yaml +26 -0
  157. package/runtime/providers/knowledge-notion.yaml +32 -0
  158. package/runtime/providers/knowledge-obsidian.yaml +24 -0
  159. package/runtime/providers/knowledge-onedrive.yaml +36 -0
  160. package/runtime/providers/knowledge-s3.yaml +45 -0
  161. package/runtime/providers/knowledge-sharepoint.yaml +39 -0
  162. package/runtime/providers/knowledge-supabase.yaml +43 -0
  163. package/runtime/providers/knowledge-vector.yaml +39 -0
  164. package/runtime/requirements.txt +6 -0
  165. package/runtime/scripts/docker-cli-qa.sh +453 -0
  166. package/runtime/scripts/release-catalog-snapshot.json +55 -4
  167. package/runtime/scripts/release-gate.py +54 -13
  168. package/runtime/tooling/toolchain.yaml +92 -0
  169. package/runtime/vendor/skills/napkin/napkin.md +21 -7
  170. package/runtime/workflows/azure-card-analysis/README.md +3 -0
  171. package/runtime/workflows/azure-card-analysis/workflow.yaml +30 -0
  172. package/runtime/workflows/daily-pr-review/README.md +3 -0
  173. package/runtime/workflows/daily-pr-review/workflow.yaml +31 -0
  174. package/runtime/workflows/incident-analysis/README.md +3 -0
  175. package/runtime/workflows/incident-analysis/workflow.yaml +33 -0
  176. package/runtime/workflows/release-prep/README.md +3 -0
  177. 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
@@ -5,14 +5,20 @@ from __future__ import annotations
5
5
  from datetime import datetime, timezone
6
6
  from typing import Any
7
7
 
8
+ from cli.aikit.embedded_mini_brain import (
9
+ EMBEDDED_BACKEND_ID,
10
+ EMBEDDED_MODEL_ID,
11
+ embedded_mini_brain_status,
12
+ setup_embedded_mini_brain,
13
+ )
8
14
  from cli.aikit.llm import BACKENDS, configure_backend, doctor_backend, load_config, save_config
9
- from cli.aikit.ollama import ollama_pull, ollama_status
15
+ from cli.aikit.ollama import ollama_status
10
16
 
11
17
 
12
18
  MINI_BRAIN_CONFIG_KEY = "mini_brain"
13
- DEFAULT_HF_MODEL = "Qwen/Qwen3-0.6B"
19
+ DEFAULT_HF_MODEL = EMBEDDED_MODEL_ID
14
20
  DEFAULT_OLLAMA_MODEL = "qwen3:0.6b"
15
- DEFAULT_PROVIDER = "ollama"
21
+ DEFAULT_PROVIDER = EMBEDDED_BACKEND_ID
16
22
  DEFAULT_BASE_URL = "http://localhost:11434/v1"
17
23
  ALLOWED_TASKS = [
18
24
  "setup_help",
@@ -50,14 +56,15 @@ def mini_brain_contract(
50
56
  ) -> dict[str, Any]:
51
57
  config = load_config() if config is None else config
52
58
  stored = config.get(MINI_BRAIN_CONFIG_KEY) if isinstance(config.get(MINI_BRAIN_CONFIG_KEY), dict) else {}
53
- enabled = bool(stored.get("enabled"))
59
+ enabled = bool(stored.get("enabled", True))
54
60
  provider = stored.get("provider") or stored.get("runtime") or DEFAULT_PROVIDER
55
61
  hf_model = stored.get("hf_model") or stored.get("model") or DEFAULT_HF_MODEL
56
62
  ollama_model = stored.get("ollama_model") or DEFAULT_OLLAMA_MODEL
63
+ embedded = embedded_mini_brain_status()
57
64
  ollama_payload = ollama_status() if ollama_payload is None else ollama_payload
58
65
  ollama_backend = doctor_backend(BACKENDS["ollama"], config) if ollama_backend is None else ollama_backend
59
- backend_configured = ollama_backend.get("status") == "ok"
60
- runtime_available = ollama_payload.get("status") == "ok" or backend_configured
66
+ ollama_configured = ollama_backend.get("configured") is True
67
+ runtime_available = embedded.get("available") is True
61
68
  available = enabled and provider == DEFAULT_PROVIDER and runtime_available
62
69
  status = "ok" if available else "disabled" if not enabled else "unavailable"
63
70
  return {
@@ -65,7 +72,8 @@ def mini_brain_contract(
65
72
  "status": status,
66
73
  "enabled": enabled,
67
74
  "available": available,
68
- "configured": enabled and provider == DEFAULT_PROVIDER and backend_configured,
75
+ "configured": available,
76
+ "embedded_configured": provider == DEFAULT_PROVIDER,
69
77
  "provider": provider,
70
78
  "runtime": provider,
71
79
  "hf_model": hf_model,
@@ -76,6 +84,7 @@ def mini_brain_contract(
76
84
  "limits": dict_value(stored.get("limits"), DEFAULT_LIMITS),
77
85
  "guardrails": list_value(stored.get("guardrails"), DEFAULT_GUARDRAILS),
78
86
  "stored_secret": False,
87
+ "embedded": embedded,
79
88
  "ollama": {
80
89
  "status": ollama_payload.get("status"),
81
90
  "daemon": (ollama_payload.get("daemon") or {}).get("status")
@@ -87,6 +96,7 @@ def mini_brain_contract(
87
96
  "status": ollama_backend.get("status"),
88
97
  "model": ollama_backend.get("model"),
89
98
  "base_url": ollama_backend.get("base_url"),
99
+ "configured": ollama_configured,
90
100
  },
91
101
  }
92
102
 
@@ -98,47 +108,59 @@ def setup_mini_brain(
98
108
  set_default: bool = False,
99
109
  model: str = DEFAULT_OLLAMA_MODEL,
100
110
  ) -> dict[str, Any]:
111
+ embedded = embedded_mini_brain_status()
101
112
  if dry_run or not yes:
102
113
  status = "planned" if dry_run else "needs-confirmation"
114
+ needs_confirmation = not dry_run and not yes
103
115
  return {
104
116
  "kind": "mini-brain-setup",
105
117
  "status": status,
106
118
  "ok": bool(dry_run),
119
+ "exit_code": 2 if needs_confirmation else 0,
107
120
  "dry_run": dry_run,
108
121
  "yes": yes,
109
122
  "stored_secret": False,
110
123
  "mini_brain": planned_contract(model=model),
111
- "pull": ollama_pull(model, yes=False, dry_run=dry_run),
124
+ "embedded": embedded,
125
+ "embedded_install": setup_embedded_mini_brain(dry_run=True, yes=False),
126
+ "ollama_setup": {
127
+ "status": "skipped",
128
+ "ok": True,
129
+ "provider": "ollama",
130
+ "model": model,
131
+ "message": "Ollama is optional; use `agent local-llm install` to add local worker models.",
132
+ },
112
133
  "next_steps": ["agent setup mini-brain --yes"],
113
- "message": "Use --yes to pull Qwen3-0.6B with Ollama and enable the mini-brain.",
134
+ "message": "Use --yes to download and enable the embedded Qwen2.5-0.5B mini-brain.",
114
135
  }
115
136
 
116
- pull = ollama_pull(model, yes=True, dry_run=False)
117
- if not pull.get("ok"):
137
+ embedded_install = setup_embedded_mini_brain(dry_run=False, yes=True)
138
+ embedded = embedded_mini_brain_status()
139
+ if embedded_install.get("ok") is not True:
118
140
  return {
119
141
  "kind": "mini-brain-setup",
120
142
  "status": "failed",
121
143
  "ok": False,
144
+ "exit_code": embedded_install.get("exit_code", 1),
122
145
  "dry_run": False,
123
146
  "yes": True,
124
147
  "stored_secret": False,
125
- "mini_brain": planned_contract(model=model),
126
- "pull": pull,
127
- "next_steps": ["Install Ollama or run agent ollama pull qwen3:0.6b --yes"],
128
- "message": pull.get("message") or "Could not pull the mini-brain model.",
148
+ "mini_brain": mini_brain_contract(),
149
+ "embedded": embedded,
150
+ "embedded_install": embedded_install,
151
+ "ollama_setup": {
152
+ "status": "skipped",
153
+ "ok": True,
154
+ "provider": "ollama",
155
+ "model": model,
156
+ "message": "Ollama remains optional for additional local worker models.",
157
+ },
158
+ "message": "Embedded mini-brain setup failed before the backend could be enabled.",
129
159
  }
130
160
 
131
- existing_config = load_config()
132
- existing_ollama = (
133
- existing_config.get("llm", {}).get("backends", {}).get(DEFAULT_PROVIDER)
134
- if isinstance(existing_config.get("llm"), dict)
135
- else {}
136
- )
137
- existing_base_url = existing_ollama.get("base_url") if isinstance(existing_ollama, dict) else None
138
161
  configured = configure_backend(
139
162
  DEFAULT_PROVIDER,
140
- base_url=existing_base_url or DEFAULT_BASE_URL,
141
- model=model,
163
+ model=DEFAULT_HF_MODEL,
142
164
  set_default=set_default,
143
165
  )
144
166
  config = load_config()
@@ -154,7 +176,15 @@ def setup_mini_brain(
154
176
  "stored_secret": False,
155
177
  "config_path": str(written_path),
156
178
  "mini_brain": contract,
157
- "pull": pull,
179
+ "embedded": embedded,
180
+ "embedded_install": embedded_install,
181
+ "ollama_setup": {
182
+ "status": "skipped",
183
+ "ok": True,
184
+ "provider": "ollama",
185
+ "model": model,
186
+ "message": "Ollama remains optional for additional local worker models.",
187
+ },
158
188
  "llm_configure": configured,
159
189
  "next_steps": ["Use low-risk setup, wizard and summary prompts normally."],
160
190
  }
@@ -177,6 +207,7 @@ def planned_contract(*, model: str = DEFAULT_OLLAMA_MODEL) -> dict[str, Any]:
177
207
  "limits": dict(DEFAULT_LIMITS),
178
208
  "guardrails": list(DEFAULT_GUARDRAILS),
179
209
  "stored_secret": False,
210
+ "embedded": embedded_mini_brain_status(),
180
211
  }
181
212
 
182
213