@openrig/cli 0.3.1 → 0.3.2

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 (264) hide show
  1. package/daemon/assets/plugins/openrig-core/skills/forming-an-openrig-mental-model/SKILL.md +1 -1
  2. package/daemon/dist/adapters/codex-runtime-adapter.d.ts.map +1 -1
  3. package/daemon/dist/adapters/codex-runtime-adapter.js +2 -1
  4. package/daemon/dist/adapters/codex-runtime-adapter.js.map +1 -1
  5. package/daemon/dist/adapters/tmux.d.ts +36 -0
  6. package/daemon/dist/adapters/tmux.d.ts.map +1 -1
  7. package/daemon/dist/adapters/tmux.js +66 -0
  8. package/daemon/dist/adapters/tmux.js.map +1 -1
  9. package/daemon/dist/db/migrations/037_mission_control_actions.d.ts +5 -1
  10. package/daemon/dist/db/migrations/037_mission_control_actions.d.ts.map +1 -1
  11. package/daemon/dist/db/migrations/037_mission_control_actions.js +5 -1
  12. package/daemon/dist/db/migrations/037_mission_control_actions.js.map +1 -1
  13. package/daemon/dist/db/migrations/041_rig_policy.d.ts +32 -0
  14. package/daemon/dist/db/migrations/041_rig_policy.d.ts.map +1 -0
  15. package/daemon/dist/db/migrations/041_rig_policy.js +47 -0
  16. package/daemon/dist/db/migrations/041_rig_policy.js.map +1 -0
  17. package/daemon/dist/domain/agent-images/resume-token-discovery.js +1 -1
  18. package/daemon/dist/domain/agent-images/resume-token-discovery.js.map +1 -1
  19. package/daemon/dist/domain/agent-images/snapshot-capturer.d.ts.map +1 -1
  20. package/daemon/dist/domain/agent-images/snapshot-capturer.js +2 -1
  21. package/daemon/dist/domain/agent-images/snapshot-capturer.js.map +1 -1
  22. package/daemon/dist/domain/agent-manifest.js +27 -0
  23. package/daemon/dist/domain/agent-manifest.js.map +1 -1
  24. package/daemon/dist/domain/bootstrap-orchestrator.d.ts.map +1 -1
  25. package/daemon/dist/domain/bootstrap-orchestrator.js +86 -6
  26. package/daemon/dist/domain/bootstrap-orchestrator.js.map +1 -1
  27. package/daemon/dist/domain/bundle-agent-images-router.d.ts +95 -0
  28. package/daemon/dist/domain/bundle-agent-images-router.d.ts.map +1 -0
  29. package/daemon/dist/domain/bundle-agent-images-router.js +140 -0
  30. package/daemon/dist/domain/bundle-agent-images-router.js.map +1 -0
  31. package/daemon/dist/domain/bundle-assembler.d.ts +18 -1
  32. package/daemon/dist/domain/bundle-assembler.d.ts.map +1 -1
  33. package/daemon/dist/domain/bundle-assembler.js +11 -1
  34. package/daemon/dist/domain/bundle-assembler.js.map +1 -1
  35. package/daemon/dist/domain/bundle-audit.d.ts +89 -0
  36. package/daemon/dist/domain/bundle-audit.d.ts.map +1 -0
  37. package/daemon/dist/domain/bundle-audit.js +87 -0
  38. package/daemon/dist/domain/bundle-audit.js.map +1 -0
  39. package/daemon/dist/domain/bundle-conflict-detector.d.ts +57 -0
  40. package/daemon/dist/domain/bundle-conflict-detector.d.ts.map +1 -0
  41. package/daemon/dist/domain/bundle-conflict-detector.js +49 -0
  42. package/daemon/dist/domain/bundle-conflict-detector.js.map +1 -0
  43. package/daemon/dist/domain/bundle-context-packs-router.d.ts +100 -0
  44. package/daemon/dist/domain/bundle-context-packs-router.d.ts.map +1 -0
  45. package/daemon/dist/domain/bundle-context-packs-router.js +158 -0
  46. package/daemon/dist/domain/bundle-context-packs-router.js.map +1 -0
  47. package/daemon/dist/domain/bundle-plugins-router.d.ts +69 -0
  48. package/daemon/dist/domain/bundle-plugins-router.d.ts.map +1 -0
  49. package/daemon/dist/domain/bundle-plugins-router.js +87 -0
  50. package/daemon/dist/domain/bundle-plugins-router.js.map +1 -0
  51. package/daemon/dist/domain/bundle-skills-router.d.ts +62 -0
  52. package/daemon/dist/domain/bundle-skills-router.d.ts.map +1 -0
  53. package/daemon/dist/domain/bundle-skills-router.js +90 -0
  54. package/daemon/dist/domain/bundle-skills-router.js.map +1 -0
  55. package/daemon/dist/domain/bundle-types.d.ts +106 -0
  56. package/daemon/dist/domain/bundle-types.d.ts.map +1 -1
  57. package/daemon/dist/domain/bundle-types.js +375 -0
  58. package/daemon/dist/domain/bundle-types.js.map +1 -1
  59. package/daemon/dist/domain/bundle-workflow-specs-router.d.ts +94 -0
  60. package/daemon/dist/domain/bundle-workflow-specs-router.d.ts.map +1 -0
  61. package/daemon/dist/domain/bundle-workflow-specs-router.js +144 -0
  62. package/daemon/dist/domain/bundle-workflow-specs-router.js.map +1 -0
  63. package/daemon/dist/domain/context-monitor.d.ts +5 -1
  64. package/daemon/dist/domain/context-monitor.d.ts.map +1 -1
  65. package/daemon/dist/domain/context-monitor.js +28 -15
  66. package/daemon/dist/domain/context-monitor.js.map +1 -1
  67. package/daemon/dist/domain/graph-projection.d.ts +6 -0
  68. package/daemon/dist/domain/graph-projection.d.ts.map +1 -1
  69. package/daemon/dist/domain/graph-projection.js +6 -0
  70. package/daemon/dist/domain/graph-projection.js.map +1 -1
  71. package/daemon/dist/domain/native-resume-probe.js +7 -7
  72. package/daemon/dist/domain/native-resume-probe.js.map +1 -1
  73. package/daemon/dist/domain/node-inventory.d.ts +18 -0
  74. package/daemon/dist/domain/node-inventory.d.ts.map +1 -1
  75. package/daemon/dist/domain/node-inventory.js +98 -0
  76. package/daemon/dist/domain/node-inventory.js.map +1 -1
  77. package/daemon/dist/domain/node-launcher.d.ts +12 -0
  78. package/daemon/dist/domain/node-launcher.d.ts.map +1 -1
  79. package/daemon/dist/domain/node-launcher.js +25 -1
  80. package/daemon/dist/domain/node-launcher.js.map +1 -1
  81. package/daemon/dist/domain/pod-bundle-assembler.d.ts +16 -1
  82. package/daemon/dist/domain/pod-bundle-assembler.d.ts.map +1 -1
  83. package/daemon/dist/domain/pod-bundle-assembler.js +11 -1
  84. package/daemon/dist/domain/pod-bundle-assembler.js.map +1 -1
  85. package/daemon/dist/domain/profile-resolver.d.ts +10 -0
  86. package/daemon/dist/domain/profile-resolver.d.ts.map +1 -1
  87. package/daemon/dist/domain/profile-resolver.js +4 -0
  88. package/daemon/dist/domain/profile-resolver.js.map +1 -1
  89. package/daemon/dist/domain/ps-projection.d.ts +22 -0
  90. package/daemon/dist/domain/ps-projection.d.ts.map +1 -1
  91. package/daemon/dist/domain/ps-projection.js +57 -0
  92. package/daemon/dist/domain/ps-projection.js.map +1 -1
  93. package/daemon/dist/domain/queue-repository.d.ts +37 -1
  94. package/daemon/dist/domain/queue-repository.d.ts.map +1 -1
  95. package/daemon/dist/domain/queue-repository.js +99 -2
  96. package/daemon/dist/domain/queue-repository.js.map +1 -1
  97. package/daemon/dist/domain/restore-check-service.d.ts +4 -0
  98. package/daemon/dist/domain/restore-check-service.d.ts.map +1 -1
  99. package/daemon/dist/domain/restore-check-service.js +16 -7
  100. package/daemon/dist/domain/restore-check-service.js.map +1 -1
  101. package/daemon/dist/domain/rig-policy/rig-policy-defaults.d.ts +48 -0
  102. package/daemon/dist/domain/rig-policy/rig-policy-defaults.d.ts.map +1 -0
  103. package/daemon/dist/domain/rig-policy/rig-policy-defaults.js +96 -0
  104. package/daemon/dist/domain/rig-policy/rig-policy-defaults.js.map +1 -0
  105. package/daemon/dist/domain/rig-policy/rig-policy-store.d.ts +46 -0
  106. package/daemon/dist/domain/rig-policy/rig-policy-store.d.ts.map +1 -0
  107. package/daemon/dist/domain/rig-policy/rig-policy-store.js +171 -0
  108. package/daemon/dist/domain/rig-policy/rig-policy-store.js.map +1 -0
  109. package/daemon/dist/domain/rig-policy/rig-policy-types.d.ts +145 -0
  110. package/daemon/dist/domain/rig-policy/rig-policy-types.d.ts.map +1 -0
  111. package/daemon/dist/domain/rig-policy/rig-policy-types.js +52 -0
  112. package/daemon/dist/domain/rig-policy/rig-policy-types.js.map +1 -0
  113. package/daemon/dist/domain/rig-policy/rig-policy-validator.d.ts +58 -0
  114. package/daemon/dist/domain/rig-policy/rig-policy-validator.d.ts.map +1 -0
  115. package/daemon/dist/domain/rig-policy/rig-policy-validator.js +211 -0
  116. package/daemon/dist/domain/rig-policy/rig-policy-validator.js.map +1 -0
  117. package/daemon/dist/domain/rigspec-instantiator.d.ts +1 -1
  118. package/daemon/dist/domain/rigspec-instantiator.d.ts.map +1 -1
  119. package/daemon/dist/domain/rigspec-instantiator.js +109 -18
  120. package/daemon/dist/domain/rigspec-instantiator.js.map +1 -1
  121. package/daemon/dist/domain/seat-activity-service.d.ts +79 -0
  122. package/daemon/dist/domain/seat-activity-service.d.ts.map +1 -0
  123. package/daemon/dist/domain/seat-activity-service.js +123 -0
  124. package/daemon/dist/domain/seat-activity-service.js.map +1 -0
  125. package/daemon/dist/domain/slices/slice-indexer.d.ts +12 -0
  126. package/daemon/dist/domain/slices/slice-indexer.d.ts.map +1 -1
  127. package/daemon/dist/domain/slices/slice-indexer.js +23 -0
  128. package/daemon/dist/domain/slices/slice-indexer.js.map +1 -1
  129. package/daemon/dist/domain/spec-library-service.d.ts.map +1 -1
  130. package/daemon/dist/domain/spec-library-service.js +19 -0
  131. package/daemon/dist/domain/spec-library-service.js.map +1 -1
  132. package/daemon/dist/domain/types.d.ts +79 -3
  133. package/daemon/dist/domain/types.d.ts.map +1 -1
  134. package/daemon/dist/domain/types.js.map +1 -1
  135. package/daemon/dist/domain/workflow-spec-cache.d.ts +22 -0
  136. package/daemon/dist/domain/workflow-spec-cache.d.ts.map +1 -1
  137. package/daemon/dist/domain/workflow-spec-cache.js +37 -0
  138. package/daemon/dist/domain/workflow-spec-cache.js.map +1 -1
  139. package/daemon/dist/domain/workspace/default-workspace-scaffold.d.ts +7 -0
  140. package/daemon/dist/domain/workspace/default-workspace-scaffold.d.ts.map +1 -1
  141. package/daemon/dist/domain/workspace/default-workspace-scaffold.js +156 -3
  142. package/daemon/dist/domain/workspace/default-workspace-scaffold.js.map +1 -1
  143. package/daemon/dist/domain/workspace/workspace-doctor.d.ts +197 -0
  144. package/daemon/dist/domain/workspace/workspace-doctor.d.ts.map +1 -0
  145. package/daemon/dist/domain/workspace/workspace-doctor.js +448 -0
  146. package/daemon/dist/domain/workspace/workspace-doctor.js.map +1 -0
  147. package/daemon/dist/index.d.ts.map +1 -1
  148. package/daemon/dist/index.js +11 -0
  149. package/daemon/dist/index.js.map +1 -1
  150. package/daemon/dist/routes/bundles.d.ts.map +1 -1
  151. package/daemon/dist/routes/bundles.js +966 -7
  152. package/daemon/dist/routes/bundles.js.map +1 -1
  153. package/daemon/dist/routes/config.d.ts.map +1 -1
  154. package/daemon/dist/routes/config.js +19 -2
  155. package/daemon/dist/routes/config.js.map +1 -1
  156. package/daemon/dist/routes/info.d.ts +3 -0
  157. package/daemon/dist/routes/info.d.ts.map +1 -0
  158. package/daemon/dist/routes/info.js +20 -0
  159. package/daemon/dist/routes/info.js.map +1 -0
  160. package/daemon/dist/routes/queue.d.ts +18 -0
  161. package/daemon/dist/routes/queue.d.ts.map +1 -1
  162. package/daemon/dist/routes/queue.js +97 -5
  163. package/daemon/dist/routes/queue.js.map +1 -1
  164. package/daemon/dist/routes/rig-policy.d.ts +9 -0
  165. package/daemon/dist/routes/rig-policy.d.ts.map +1 -0
  166. package/daemon/dist/routes/rig-policy.js +174 -0
  167. package/daemon/dist/routes/rig-policy.js.map +1 -0
  168. package/daemon/dist/routes/rigs.d.ts.map +1 -1
  169. package/daemon/dist/routes/rigs.js +10 -2
  170. package/daemon/dist/routes/rigs.js.map +1 -1
  171. package/daemon/dist/routes/sessions.d.ts.map +1 -1
  172. package/daemon/dist/routes/sessions.js +26 -3
  173. package/daemon/dist/routes/sessions.js.map +1 -1
  174. package/daemon/dist/routes/up.d.ts +25 -0
  175. package/daemon/dist/routes/up.d.ts.map +1 -1
  176. package/daemon/dist/routes/up.js +70 -3
  177. package/daemon/dist/routes/up.js.map +1 -1
  178. package/daemon/dist/routes/workspace.d.ts.map +1 -1
  179. package/daemon/dist/routes/workspace.js +80 -1
  180. package/daemon/dist/routes/workspace.js.map +1 -1
  181. package/daemon/dist/server.d.ts +10 -0
  182. package/daemon/dist/server.d.ts.map +1 -1
  183. package/daemon/dist/server.js +11 -0
  184. package/daemon/dist/server.js.map +1 -1
  185. package/daemon/dist/startup.d.ts.map +1 -1
  186. package/daemon/dist/startup.js +45 -4
  187. package/daemon/dist/startup.js.map +1 -1
  188. package/daemon/specs/agents/orchestration/orchestrator/guidance/role.md +28 -4
  189. package/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md +285 -8
  190. package/daemon/specs/agents/shared/skills/core/rig-lifecycle/SKILL.md +2 -2
  191. package/daemon/specs/rigs/launch/conveyor/rig.yaml +1 -1
  192. package/dist/client.d.ts +3 -0
  193. package/dist/client.d.ts.map +1 -1
  194. package/dist/client.js +13 -1
  195. package/dist/client.js.map +1 -1
  196. package/dist/commands/bundle.d.ts.map +1 -1
  197. package/dist/commands/bundle.js +123 -2
  198. package/dist/commands/bundle.js.map +1 -1
  199. package/dist/commands/config-init-workspace.d.ts.map +1 -1
  200. package/dist/commands/config-init-workspace.js +40 -5
  201. package/dist/commands/config-init-workspace.js.map +1 -1
  202. package/dist/commands/ps.d.ts.map +1 -1
  203. package/dist/commands/ps.js +51 -8
  204. package/dist/commands/ps.js.map +1 -1
  205. package/dist/commands/queue.d.ts +6 -0
  206. package/dist/commands/queue.d.ts.map +1 -1
  207. package/dist/commands/queue.js +103 -5
  208. package/dist/commands/queue.js.map +1 -1
  209. package/dist/commands/rig-policy.d.ts +48 -0
  210. package/dist/commands/rig-policy.d.ts.map +1 -0
  211. package/dist/commands/rig-policy.js +429 -0
  212. package/dist/commands/rig-policy.js.map +1 -0
  213. package/dist/commands/scope.d.ts +6 -0
  214. package/dist/commands/scope.d.ts.map +1 -0
  215. package/dist/commands/scope.js +659 -0
  216. package/dist/commands/scope.js.map +1 -0
  217. package/dist/commands/up.d.ts.map +1 -1
  218. package/dist/commands/up.js +47 -0
  219. package/dist/commands/up.js.map +1 -1
  220. package/dist/commands/workflow.d.ts +9 -0
  221. package/dist/commands/workflow.d.ts.map +1 -1
  222. package/dist/commands/workflow.js +139 -1
  223. package/dist/commands/workflow.js.map +1 -1
  224. package/dist/commands/workspace.d.ts +20 -0
  225. package/dist/commands/workspace.d.ts.map +1 -1
  226. package/dist/commands/workspace.js +138 -7
  227. package/dist/commands/workspace.js.map +1 -1
  228. package/dist/daemon-lifecycle.d.ts +25 -1
  229. package/dist/daemon-lifecycle.d.ts.map +1 -1
  230. package/dist/daemon-lifecycle.js +33 -2
  231. package/dist/daemon-lifecycle.js.map +1 -1
  232. package/dist/index.d.ts +2 -0
  233. package/dist/index.d.ts.map +1 -1
  234. package/dist/index.js +5 -0
  235. package/dist/index.js.map +1 -1
  236. package/dist/lib/scope/dot-id.d.ts +43 -0
  237. package/dist/lib/scope/dot-id.d.ts.map +1 -0
  238. package/dist/lib/scope/dot-id.js +165 -0
  239. package/dist/lib/scope/dot-id.js.map +1 -0
  240. package/dist/lib/scope/scope-fs.d.ts +79 -0
  241. package/dist/lib/scope/scope-fs.d.ts.map +1 -0
  242. package/dist/lib/scope/scope-fs.js +468 -0
  243. package/dist/lib/scope/scope-fs.js.map +1 -0
  244. package/dist/lib/scope/templates.d.ts +38 -0
  245. package/dist/lib/scope/templates.d.ts.map +1 -0
  246. package/dist/lib/scope/templates.js +109 -0
  247. package/dist/lib/scope/templates.js.map +1 -0
  248. package/dist/lib/scope/types.d.ts +61 -0
  249. package/dist/lib/scope/types.d.ts.map +1 -0
  250. package/dist/lib/scope/types.js +38 -0
  251. package/dist/lib/scope/types.js.map +1 -0
  252. package/dist/lib/scope-templates/backlog-deprecation.md +26 -0
  253. package/dist/lib/scope-templates/backlog-tech-debt.md +22 -0
  254. package/dist/lib/scope-templates/bug-fix.md +30 -0
  255. package/dist/lib/scope-templates/mission-notes.md +97 -0
  256. package/dist/lib/scope-templates/mission-placeholder.md +20 -0
  257. package/dist/lib/scope-templates/mission-release.md +25 -0
  258. package/dist/lib/scope-templates/placeholder.md +30 -0
  259. package/dist/lib/scope-templates/release-feature.md +26 -0
  260. package/dist/lib/scope-templates/research.md +22 -0
  261. package/package.json +2 -2
  262. package/ui/dist/assets/index-GjTxfi9S.js +547 -0
  263. package/ui/dist/index.html +1 -1
  264. package/ui/dist/assets/index-Rl3ZBrUg.js +0 -547
@@ -5,18 +5,218 @@ import { Hono } from "hono";
5
5
  import { LegacyBundleAssembler as BundleAssembler } from "../domain/bundle-assembler.js";
6
6
  import { PodBundleAssembler } from "../domain/pod-bundle-assembler.js";
7
7
  import { computeIntegrity, writeIntegrity, verifyIntegrity } from "../domain/bundle-integrity.js";
8
- import { pack, verifyArchiveDigest } from "../domain/bundle-archive.js";
8
+ import { pack, unpack, verifyArchiveDigest } from "../domain/bundle-archive.js";
9
9
  import { resolvePackage } from "../domain/package-resolve-helper.js";
10
10
  import { LegacyRigSpecSchema } from "../domain/rigspec-schema.js";
11
11
  import { RigSpecCodec } from "../domain/rigspec-codec.js";
12
12
  import { RigSpecSchema } from "../domain/rigspec-schema.js";
13
- import { parseLegacyBundleManifest as parseBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, serializePodBundleManifest, parsePodBundleManifest, validatePodBundleManifest } from "../domain/bundle-types.js";
13
+ import { parseLegacyBundleManifest as parseBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, serializePodBundleManifest, parsePodBundleManifest, validatePodBundleManifest, validateLegacyBundleManifest, normalizeProvenanceBlock, normalizeCompatibilityBlock, isRelativeSafePath } from "../domain/bundle-types.js";
14
+ import { fileURLToPath } from "node:url";
15
+ import { detectBundleConflicts } from "../domain/bundle-conflict-detector.js";
16
+ import { BundleAuditReader, BundleAuditWriter } from "../domain/bundle-audit.js";
17
+ import { getDefaultOpenRigPath } from "../openrig-compat.js";
18
+ import { routeSkills } from "../domain/bundle-skills-router.js";
19
+ import { routePlugins } from "../domain/bundle-plugins-router.js";
20
+ import { routeWorkflowSpecs } from "../domain/bundle-workflow-specs-router.js";
21
+ import { routeContextPacks } from "../domain/bundle-context-packs-router.js";
22
+ import { routeAgentImages } from "../domain/bundle-agent-images-router.js";
23
+ import { SettingsStore as ContextPackSettingsStore } from "../domain/user-settings/settings-store.js";
24
+ /**
25
+ * Read the daemon's own package.json version at call time (Item 1 / slice-05).
26
+ * Function-level read on purpose: module-level constants would mask test
27
+ * isolation per the audit-every-layer discipline. The read is cheap and
28
+ * only happens on bundle create.
29
+ */
30
+ function getDaemonVersion() {
31
+ try {
32
+ const here = fileURLToPath(import.meta.url);
33
+ const pkgPath = nodePath.join(nodePath.dirname(here), "..", "..", "package.json");
34
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
35
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
36
+ }
37
+ catch {
38
+ return "unknown";
39
+ }
40
+ }
41
+ /**
42
+ * Compare two dotted numeric version strings (semver-ish). Returns -1 if
43
+ * a < b, 0 if equal, 1 if a > b. Non-numeric segments coerce to 0. Adequate
44
+ * for the 0.x.y / 1.x.y range; pre-release / build metadata not interpreted.
45
+ * Item-2 install-time version check (Checkpoint 3.3).
46
+ */
47
+ function compareVersions(a, b) {
48
+ const partsA = a.split(".").map((p) => parseInt(p, 10) || 0);
49
+ const partsB = b.split(".").map((p) => parseInt(p, 10) || 0);
50
+ const maxLen = Math.max(partsA.length, partsB.length);
51
+ for (let i = 0; i < maxLen; i++) {
52
+ const va = partsA[i] ?? 0;
53
+ const vb = partsB[i] ?? 0;
54
+ if (va !== vb)
55
+ return va < vb ? -1 : 1;
56
+ }
57
+ return 0;
58
+ }
59
+ /**
60
+ * Run the install-time compatibility check (Item 2 Checkpoint 3.3). Returns
61
+ * an array of failures (one per kind), or null if all checks pass. Missing
62
+ * fields in compat are no-ops (daemon-only check when min_cli_version absent;
63
+ * full pass when both absent). cliVersion undefined skips the CLI check
64
+ * silently (pre-Item-2 CLIs don't send it; honest backward compat).
65
+ */
66
+ function checkBundleCompatibility(compat, daemonVersion, cliVersion) {
67
+ if (!compat)
68
+ return null;
69
+ const failures = [];
70
+ if (compat.minDaemonVersion && compareVersions(daemonVersion, compat.minDaemonVersion) < 0) {
71
+ failures.push({
72
+ reason: "daemon_version_mismatch",
73
+ required: compat.minDaemonVersion,
74
+ actual: daemonVersion,
75
+ description: `bundle requires daemon >= ${compat.minDaemonVersion}, current daemon is ${daemonVersion}`,
76
+ });
77
+ }
78
+ if (compat.minCliVersion && cliVersion && compareVersions(cliVersion, compat.minCliVersion) < 0) {
79
+ failures.push({
80
+ reason: "cli_version_mismatch",
81
+ required: compat.minCliVersion,
82
+ actual: cliVersion,
83
+ description: `bundle requires CLI >= ${compat.minCliVersion}, current CLI is ${cliVersion}`,
84
+ });
85
+ }
86
+ return failures.length > 0 ? failures : null;
87
+ }
88
+ /**
89
+ * Extract bundle.yaml manifest from a .rigbundle archive via the canonical
90
+ * safe-extraction path (unpack from domain/bundle-archive). unpack performs
91
+ * verifyArchiveDigest, then tar.list pre-scan rejecting symlinks / hardlinks
92
+ * / absolute paths / dot-dot traversal, then extracts, then verifies content
93
+ * integrity. Using unpack here keeps the install-time compat check inside the
94
+ * existing trust boundary — raw tar.extract on an untrusted archive would
95
+ * bypass the safety prescan (B1 regression fixed). Throws on archive / safety
96
+ * / parse failures; the caller converts these into a 3-part 400 response.
97
+ */
98
+ async function extractManifestForCompatCheck(bundlePath) {
99
+ const meta = await extractInstallTimeMetadata(bundlePath);
100
+ return meta.bundleManifest;
101
+ }
102
+ /**
103
+ * Extract both the bundle.yaml manifest AND the rig name from the bundle's
104
+ * rig.yaml in one safe pass (Item 3 / slice-05 Checkpoint 4.2). The /install
105
+ * handler uses bundleManifest for the compat check (Item 2) and rigName for
106
+ * the conflict check (Item 3). Reuses unpack — single trust boundary.
107
+ */
108
+ async function extractInstallTimeMetadata(bundlePath) {
109
+ const tmpDir = fs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-meta-"));
110
+ try {
111
+ await unpack(bundlePath, tmpDir);
112
+ const manifestPath = nodePath.join(tmpDir, "bundle.yaml");
113
+ if (!fs.existsSync(manifestPath))
114
+ throw new Error("Bundle missing bundle.yaml");
115
+ const manifestYaml = fs.readFileSync(manifestPath, "utf-8");
116
+ const bundleManifest = parsePodBundleManifest(manifestYaml);
117
+ // B1 safety repair (slice-05 Checkpoint 4.2 / qitem-20260518204906): validate
118
+ // the parsed manifest BEFORE trusting any of its fields. The validators
119
+ // reject unsafe rig_spec values (isRelativeSafePath: no absolute, no ../,
120
+ // no backslash, no empty segments). Schema-version-aware: v2 uses pod-aware
121
+ // validator; everything else falls back to the v1 legacy validator. This is
122
+ // the same trust-boundary reuse as the unpack/B1 fix in Item 2.
123
+ const schemaVersion = bundleManifest["schema_version"];
124
+ if (schemaVersion === 2) {
125
+ const v2Validation = validatePodBundleManifest(bundleManifest);
126
+ if (!v2Validation.valid) {
127
+ throw new Error(`Invalid v2 bundle manifest: ${v2Validation.errors.join("; ")}`);
128
+ }
129
+ }
130
+ else {
131
+ const v1Validation = validateLegacyBundleManifest(bundleManifest, { requireIntegrity: false });
132
+ if (!v1Validation.valid) {
133
+ throw new Error(`Invalid v1 bundle manifest: ${v1Validation.errors.join("; ")}`);
134
+ }
135
+ }
136
+ // Rig name lives in the bundle's rig.yaml (path referenced by bundle.yaml's
137
+ // rig_spec field; defaults to rig.yaml for legacy bundles). Read + parse;
138
+ // missing-or-malformed rig name leaves rigName undefined which the detector
139
+ // fail-opens on (no rig name to compare).
140
+ //
141
+ // B1 safety repair (defense-in-depth alongside the validator above): resolve
142
+ // rig_spec inside tmpDir and require the result to stay inside tmpDir
143
+ // before reading, mirroring bundle-source-resolver.ts:60-63. The validator
144
+ // should have already rejected unsafe rig_spec; this is the second line.
145
+ let rigName;
146
+ const rigSpecRel = typeof bundleManifest["rig_spec"] === "string" ? bundleManifest["rig_spec"] : "rig.yaml";
147
+ const rigSpecPath = nodePath.resolve(tmpDir, rigSpecRel);
148
+ const tmpDirResolved = nodePath.resolve(tmpDir);
149
+ if (rigSpecPath !== tmpDirResolved && !rigSpecPath.startsWith(tmpDirResolved + nodePath.sep)) {
150
+ throw new Error(`Rig spec path '${rigSpecRel}' escapes bundle workspace`);
151
+ }
152
+ if (fs.existsSync(rigSpecPath)) {
153
+ try {
154
+ const rigYaml = fs.readFileSync(rigSpecPath, "utf-8");
155
+ const rigParsed = parsePodBundleManifest(rigYaml);
156
+ if (typeof rigParsed["name"] === "string" && rigParsed["name"].length > 0) {
157
+ rigName = rigParsed["name"];
158
+ }
159
+ }
160
+ catch {
161
+ // rig.yaml malformed — leave rigName undefined; conflict check skips
162
+ }
163
+ }
164
+ return { bundleManifest, rigName };
165
+ }
166
+ finally {
167
+ fs.rmSync(tmpDir, { recursive: true, force: true });
168
+ }
169
+ }
170
+ /**
171
+ * Sanitize raw compatibility from request body into a BundleCompatibility
172
+ * object. Only known typed fields accepted; unknown fields dropped silently.
173
+ * Returns undefined if input is missing or has no usable fields.
174
+ */
175
+ function compatibilityFromRequestBody(raw) {
176
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
177
+ return undefined;
178
+ const c = raw;
179
+ const result = {};
180
+ if (typeof c["minDaemonVersion"] === "string")
181
+ result.minDaemonVersion = c["minDaemonVersion"];
182
+ if (typeof c["minCliVersion"] === "string")
183
+ result.minCliVersion = c["minCliVersion"];
184
+ if (typeof c["schemaVersion"] === "number")
185
+ result.schemaVersion = c["schemaVersion"];
186
+ return Object.keys(result).length > 0 ? result : undefined;
187
+ }
188
+ /**
189
+ * Normalize raw provenance from request body into a BundleProvenance object.
190
+ * Only string fields are accepted; unknown/non-string fields are dropped
191
+ * silently. Returns undefined if the input is missing or has no usable
192
+ * fields. Daemon-side daemonVersion injection is the caller's responsibility.
193
+ */
194
+ function provenanceFromRequestBody(raw) {
195
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
196
+ return undefined;
197
+ const p = raw;
198
+ const result = {};
199
+ if (typeof p["sourceHost"] === "string")
200
+ result.sourceHost = p["sourceHost"];
201
+ if (typeof p["authorSession"] === "string")
202
+ result.authorSession = p["authorSession"];
203
+ if (typeof p["sourceRigId"] === "string")
204
+ result.sourceRigId = p["sourceRigId"];
205
+ if (typeof p["sourceRigName"] === "string")
206
+ result.sourceRigName = p["sourceRigName"];
207
+ if (typeof p["cliVersion"] === "string")
208
+ result.cliVersion = p["cliVersion"];
209
+ if (typeof p["notes"] === "string")
210
+ result.notes = p["notes"];
211
+ return Object.keys(result).length > 0 ? result : undefined;
212
+ }
14
213
  export const bundleRoutes = new Hono();
15
214
  function getDeps(c) {
16
215
  return {
17
216
  eventBus: c.get("eventBus"),
18
217
  bootstrapOrchestrator: c.get("bootstrapOrchestrator"),
19
218
  bootstrapRepo: c.get("bootstrapRepo"),
219
+ rigRepo: c.get("rigRepo"),
20
220
  };
21
221
  }
22
222
  function realFsOps() {
@@ -64,6 +264,535 @@ function podAssemblerFsOps() {
64
264
  listFiles: (dir) => realFsOps().listFiles(dir),
65
265
  };
66
266
  }
267
+ /** Item 6 / slice-05 Checkpoint 7.3d: real PluginsRouterFsOps backed by node:fs. */
268
+ function pluginsRouterFsOps() {
269
+ return {
270
+ exists: (p) => fs.existsSync(p),
271
+ isDirectory: (p) => { try {
272
+ return fs.statSync(p).isDirectory();
273
+ }
274
+ catch {
275
+ return false;
276
+ } },
277
+ mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
278
+ copyDir: (s, d) => fs.cpSync(s, d, { recursive: true }),
279
+ };
280
+ }
281
+ /** Item 6 / slice-05 Checkpoint 7.3e step 3: real WorkflowSpecsRouterFsOps backed by node:fs. */
282
+ function workflowSpecsRouterFsOps() {
283
+ return {
284
+ exists: (p) => fs.existsSync(p),
285
+ readFile: (p) => fs.readFileSync(p, "utf-8"),
286
+ writeFile: (p, c) => fs.writeFileSync(p, c, "utf-8"),
287
+ mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
288
+ };
289
+ }
290
+ /** Item 6 / slice-05 Checkpoint 7.3f step 3: real ContextPacksRouterFsOps backed by node:fs. */
291
+ function contextPacksRouterFsOps() {
292
+ return {
293
+ exists: (p) => fs.existsSync(p),
294
+ isDirectory: (p) => { try {
295
+ return fs.statSync(p).isDirectory();
296
+ }
297
+ catch {
298
+ return false;
299
+ } },
300
+ mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
301
+ copyDir: (s, d) => fs.cpSync(s, d, { recursive: true }),
302
+ };
303
+ }
304
+ /** Item 6 / slice-05 Checkpoint 7.3g step 3: real AgentImagesRouterFsOps backed by node:fs. */
305
+ function agentImagesRouterFsOps() {
306
+ return {
307
+ exists: (p) => fs.existsSync(p),
308
+ isDirectory: (p) => { try {
309
+ return fs.statSync(p).isDirectory();
310
+ }
311
+ catch {
312
+ return false;
313
+ } },
314
+ mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
315
+ copyDir: (s, d) => fs.cpSync(s, d, { recursive: true }),
316
+ };
317
+ }
318
+ /**
319
+ * Item 6 / slice-05 Checkpoint 7.3g step 3: extract the bundle safely
320
+ * (banked unpack trust boundary) and route any declared agent_images to
321
+ * the operator agent-images library. Returns null when bundle has no
322
+ * agent_images[] (no-op).
323
+ *
324
+ * Target resolution: <openrigHome>/agent-images — per startup.ts:523
325
+ * (the user-file root the live AgentImageLibraryService is constructed
326
+ * against). No SettingsStore complexity; canonical path is OPENRIG_HOME-
327
+ * rooted per agent-image-types.ts:9-10.
328
+ *
329
+ * Per the e7a0b253 PRD-coherent contract: agent_images entries are paths
330
+ * to image DIRECTORIES (not manifest paths — distinct shape from
331
+ * context_packs). The router enforces sourceAbs isDirectory + manifest.yaml
332
+ * inside the dir is a file before copying the whole image dir.
333
+ *
334
+ * Mirror of routeContextPacksAfterBootstrap pattern with the dir-path
335
+ * contract adaptation. bundlePath-only signature per 5f410eee B1 lesson.
336
+ */
337
+ async function routeAgentImagesAfterBootstrap(bundlePath) {
338
+ const targetAgentImagesDir = getDefaultOpenRigPath("agent-images");
339
+ const tmpDir = fs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-agent-images-route-"));
340
+ try {
341
+ await unpack(bundlePath, tmpDir);
342
+ const manifestPath = nodePath.join(tmpDir, "bundle.yaml");
343
+ if (!fs.existsSync(manifestPath))
344
+ return null;
345
+ const manifestYaml = fs.readFileSync(manifestPath, "utf-8");
346
+ const manifest = parsePodBundleManifest(manifestYaml);
347
+ const rawAgentImages = manifest["agent_images"];
348
+ if (!Array.isArray(rawAgentImages) || rawAgentImages.length === 0)
349
+ return null;
350
+ const declaredAgentImages = rawAgentImages.filter((s) => typeof s === "string" && s.length > 0);
351
+ if (declaredAgentImages.length === 0)
352
+ return null;
353
+ return routeAgentImages({
354
+ bundleRoot: tmpDir,
355
+ declaredAgentImages,
356
+ targetAgentImagesDir,
357
+ }, agentImagesRouterFsOps());
358
+ }
359
+ finally {
360
+ fs.rmSync(tmpDir, { recursive: true, force: true });
361
+ }
362
+ }
363
+ /**
364
+ * Item 6 / slice-05 Checkpoint 7.3f step 3: extract the bundle safely
365
+ * (banked unpack trust boundary) and route any declared context_packs to
366
+ * the operator context-packs library. Returns null when bundle has no
367
+ * context_packs[] (no-op).
368
+ *
369
+ * Target resolution: <openrigHome>/context-packs — per startup.ts:496
370
+ * (the user-file root the live ContextPackLibraryService is constructed
371
+ * against). No SettingsStore complexity (unlike workflow_specs);
372
+ * context-packs canonical path is OPENRIG_HOME-rooted per
373
+ * context-pack-types.ts:9-10.
374
+ *
375
+ * Mirror of routeSkillsAfterBootstrap / routePluginsAfterBootstrap /
376
+ * routeWorkflowSpecsAfterBootstrap pattern: takes bundlePath only
377
+ * (decoupled from installMeta per the 5f410eee B1 lesson; routing must
378
+ * fire on the dual-override path too).
379
+ */
380
+ async function routeContextPacksAfterBootstrap(bundlePath) {
381
+ const targetContextPacksDir = getDefaultOpenRigPath("context-packs");
382
+ const tmpDir = fs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-context-packs-route-"));
383
+ try {
384
+ await unpack(bundlePath, tmpDir);
385
+ const manifestPath = nodePath.join(tmpDir, "bundle.yaml");
386
+ if (!fs.existsSync(manifestPath))
387
+ return null;
388
+ const manifestYaml = fs.readFileSync(manifestPath, "utf-8");
389
+ const manifest = parsePodBundleManifest(manifestYaml);
390
+ const rawContextPacks = manifest["context_packs"];
391
+ if (!Array.isArray(rawContextPacks) || rawContextPacks.length === 0)
392
+ return null;
393
+ const declaredContextPacks = rawContextPacks.filter((s) => typeof s === "string" && s.length > 0);
394
+ if (declaredContextPacks.length === 0)
395
+ return null;
396
+ return routeContextPacks({
397
+ bundleRoot: tmpDir,
398
+ declaredContextPacks,
399
+ targetContextPacksDir,
400
+ }, contextPacksRouterFsOps());
401
+ }
402
+ finally {
403
+ fs.rmSync(tmpDir, { recursive: true, force: true });
404
+ }
405
+ }
406
+ /**
407
+ * Item 6 / slice-05 Checkpoint 7.3e step 3: extract the bundle safely
408
+ * (banked unpack trust boundary) and route any declared workflow_specs to
409
+ * the operator workflow-specs library. Returns null when bundle has no
410
+ * workflow_specs[] (no-op), no manifest, or workspaceSpecsRoot unresolved.
411
+ *
412
+ * Target resolution per the CALLER CONTRACT in
413
+ * bundle-workflow-specs-router.ts: SettingsStore is the sole authority.
414
+ * Resolved as nodePath.join(workspaceSpecsRoot, "workflows") — the path
415
+ * that spec-library-workflow-scanner actually reads (startup.ts:903-916).
416
+ * If SettingsStore cannot resolve workspaceSpecsRoot (settings not yet
417
+ * initialized / config error), we return null and the install lifecycle
418
+ * proceeds without workflow_specs routing — mirror of startup.ts:910-916
419
+ * try/catch posture.
420
+ *
421
+ * Mirror of routeSkillsAfterBootstrap / routePluginsAfterBootstrap pattern:
422
+ * takes bundlePath only (decoupled from installMeta per the 5f410eee B1
423
+ * lesson; routing must fire on the dual-override path too).
424
+ */
425
+ async function routeWorkflowSpecsAfterBootstrap(bundlePath) {
426
+ let workspaceSpecsRoot;
427
+ try {
428
+ const settingsStore = new ContextPackSettingsStore();
429
+ workspaceSpecsRoot = settingsStore.resolveConfig().workspaceSpecsRoot;
430
+ }
431
+ catch {
432
+ return null; // settings unresolvable; no-op routing
433
+ }
434
+ if (!workspaceSpecsRoot)
435
+ return null;
436
+ const targetWorkflowSpecsDir = nodePath.join(workspaceSpecsRoot, "workflows");
437
+ const tmpDir = fs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-workflow-specs-route-"));
438
+ try {
439
+ await unpack(bundlePath, tmpDir);
440
+ const manifestPath = nodePath.join(tmpDir, "bundle.yaml");
441
+ if (!fs.existsSync(manifestPath))
442
+ return null;
443
+ const manifestYaml = fs.readFileSync(manifestPath, "utf-8");
444
+ const manifest = parsePodBundleManifest(manifestYaml);
445
+ const rawWorkflowSpecs = manifest["workflow_specs"];
446
+ if (!Array.isArray(rawWorkflowSpecs) || rawWorkflowSpecs.length === 0)
447
+ return null;
448
+ const declaredWorkflowSpecs = rawWorkflowSpecs.filter((s) => typeof s === "string" && s.length > 0);
449
+ if (declaredWorkflowSpecs.length === 0)
450
+ return null;
451
+ return routeWorkflowSpecs({
452
+ bundleRoot: tmpDir,
453
+ declaredWorkflowSpecs,
454
+ targetWorkflowSpecsDir,
455
+ }, workflowSpecsRouterFsOps());
456
+ }
457
+ finally {
458
+ fs.rmSync(tmpDir, { recursive: true, force: true });
459
+ }
460
+ }
461
+ /**
462
+ * Item 6 / slice-05 Checkpoint 7.3d: extract the bundle safely (banked unpack
463
+ * trust boundary) and route any declared plugin references to the operator
464
+ * plugins library (<OPENRIG_HOME>/plugins/<id>/). Returns null when bundle
465
+ * has no plugins[] (no-op). Mirror of routeSkillsAfterBootstrap pattern —
466
+ * takes bundlePath only (decoupled from installMeta per the 5f410eee B1
467
+ * lesson; routing must fire on the dual-override path too).
468
+ */
469
+ async function routePluginsAfterBootstrap(bundlePath) {
470
+ const tmpDir = fs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-plugins-route-"));
471
+ try {
472
+ await unpack(bundlePath, tmpDir);
473
+ const manifestPath = nodePath.join(tmpDir, "bundle.yaml");
474
+ if (!fs.existsSync(manifestPath))
475
+ return null;
476
+ const manifestYaml = fs.readFileSync(manifestPath, "utf-8");
477
+ const manifest = parsePodBundleManifest(manifestYaml);
478
+ const rawPlugins = manifest["plugins"];
479
+ if (!Array.isArray(rawPlugins) || rawPlugins.length === 0)
480
+ return null;
481
+ const declaredPlugins = [];
482
+ for (const entry of rawPlugins) {
483
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
484
+ continue;
485
+ const p = entry;
486
+ const s = p["source"];
487
+ if (typeof p["id"] !== "string" || !p["id"])
488
+ continue;
489
+ if (!s || typeof s !== "object" || Array.isArray(s))
490
+ continue;
491
+ const src = s;
492
+ if (src["kind"] !== "local" || typeof src["path"] !== "string" || !src["path"])
493
+ continue;
494
+ declaredPlugins.push({ id: p["id"], source: { kind: "local", path: src["path"] } });
495
+ }
496
+ if (declaredPlugins.length === 0)
497
+ return null;
498
+ return routePlugins({
499
+ bundleRoot: tmpDir,
500
+ declaredPlugins,
501
+ targetPluginsDir: getDefaultOpenRigPath("plugins"),
502
+ }, pluginsRouterFsOps());
503
+ }
504
+ finally {
505
+ fs.rmSync(tmpDir, { recursive: true, force: true });
506
+ }
507
+ }
508
+ /** Item 6 / slice-05 Checkpoint 7.3: real SkillsRouterFsOps backed by node:fs. */
509
+ function skillsRouterFsOps() {
510
+ return {
511
+ exists: (p) => fs.existsSync(p),
512
+ readFile: (p) => fs.readFileSync(p, "utf-8"),
513
+ writeFile: (p, c) => fs.writeFileSync(p, c, "utf-8"),
514
+ mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
515
+ };
516
+ }
517
+ /**
518
+ * Item 6 / slice-05 Checkpoint 7.3: extract the bundle safely (banked unpack
519
+ * trust boundary) and route any declared skills to the operator skills
520
+ * library. Returns null when bundle has no skills[] (no-op).
521
+ *
522
+ * B1 repair (qitem-20260518220247-22f5257a): takes bundlePath only and does
523
+ * its own safe unpack + parse. Previously coupled to installMeta from the
524
+ * pre-check extraction, which is null when operator passes --skip-version-
525
+ * check AND --force together; that incorrectly suppressed post-install
526
+ * skills routing on the dual-override path. Skills routing is independent
527
+ * of the pre-check decisions and should fire whenever an install completes
528
+ * successfully with a bundle that declares skills.
529
+ *
530
+ * Best-effort: returns null on any extract/parse failure (caller has the
531
+ * outer try/catch). Single unpack call per invocation; the manifest re-parse
532
+ * is cheap vs. the unpack cost which is required either way to access the
533
+ * skill source files for routing.
534
+ */
535
+ async function routeSkillsAfterBootstrap(bundlePath) {
536
+ const tmpDir = fs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-skills-route-"));
537
+ try {
538
+ await unpack(bundlePath, tmpDir);
539
+ const manifestPath = nodePath.join(tmpDir, "bundle.yaml");
540
+ if (!fs.existsSync(manifestPath))
541
+ return null;
542
+ const manifestYaml = fs.readFileSync(manifestPath, "utf-8");
543
+ const manifest = parsePodBundleManifest(manifestYaml);
544
+ const rawSkills = manifest["skills"];
545
+ if (!Array.isArray(rawSkills) || rawSkills.length === 0)
546
+ return null;
547
+ const declaredSkills = rawSkills.filter((s) => typeof s === "string" && s.length > 0);
548
+ if (declaredSkills.length === 0)
549
+ return null;
550
+ return routeSkills({
551
+ bundleRoot: tmpDir,
552
+ declaredSkills,
553
+ targetSkillsDir: getDefaultOpenRigPath("skills"),
554
+ }, skillsRouterFsOps());
555
+ }
556
+ finally {
557
+ fs.rmSync(tmpDir, { recursive: true, force: true });
558
+ }
559
+ }
560
+ /** Item 4 / slice-05 Checkpoint 5.2: real BundleAuditFsOps backed by node:fs. */
561
+ function auditFsOps() {
562
+ return {
563
+ appendFile: (p, c) => fs.appendFileSync(p, c, "utf-8"),
564
+ readFile: (p) => fs.readFileSync(p, "utf-8"),
565
+ exists: (p) => fs.existsSync(p),
566
+ mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
567
+ };
568
+ }
569
+ /**
570
+ * Audit file path resolved at call time via getDefaultOpenRigPath
571
+ * ("bundle-audit.jsonl"). Function-level read (no module-level constant)
572
+ * so OPENRIG_HOME env changes between requests / tests are honored.
573
+ */
574
+ function bundleAuditPath() {
575
+ return getDefaultOpenRigPath("bundle-audit.jsonl");
576
+ }
577
+ /**
578
+ * Item 4 / slice-05 Checkpoint 5.3: append a bundle install audit record.
579
+ * Best-effort — audit failures are logged via the eventBus shape but never
580
+ * fail the install response (the install already happened or failed; the
581
+ * audit is a side-channel record). bundleManifest is optional; when present,
582
+ * provenance.source_host is mirrored into the record.
583
+ */
584
+ function writeInstallAudit(opts) {
585
+ try {
586
+ const writer = new BundleAuditWriter({
587
+ opts: { auditPath: bundleAuditPath() },
588
+ fsOps: auditFsOps(),
589
+ });
590
+ const provenance = normalizeProvenanceBlock(opts.bundleManifest?.["provenance"]);
591
+ const record = {
592
+ installedAt: new Date().toISOString(),
593
+ bundlePath: opts.bundlePath,
594
+ outcome: opts.outcome,
595
+ };
596
+ if (opts.targetRigId)
597
+ record.targetRigId = opts.targetRigId;
598
+ if (opts.targetRigName)
599
+ record.targetRigName = opts.targetRigName;
600
+ if (opts.cliVersion)
601
+ record.cliVersion = opts.cliVersion;
602
+ record.daemonVersion = getDaemonVersion();
603
+ if (provenance?.sourceHost)
604
+ record.sourceHost = provenance.sourceHost;
605
+ writer.append(record);
606
+ }
607
+ catch {
608
+ // Audit-write failure is side-channel; never fail the install response.
609
+ // Future enhancement: surface via eventBus (out of scope this commit).
610
+ }
611
+ }
612
+ function consumeAuthorBundleYaml(sourceRoot, staging) {
613
+ const authorBundlePath = nodePath.join(sourceRoot, "bundle.yaml");
614
+ if (!fs.existsSync(authorBundlePath))
615
+ return {};
616
+ // Canonicalize sourceRoot before vendoring (banked 79a89d40 B1 guard
617
+ // catch: lexical containment + dereference allows symlinks under
618
+ // sourceRoot to escape). realpath the root once; every declared path's
619
+ // realpath must stay within this boundary.
620
+ const sourceRootReal = fs.realpathSync(sourceRoot);
621
+ const stagingResolved = nodePath.resolve(staging);
622
+ /** Realpath-contain check: the actual file/dir behind `absPath` (after
623
+ * symlink resolution) must live under sourceRootReal. Catches symlinks
624
+ * whose targets escape the source tree even when the lexical path is
625
+ * inside sourceRoot. Throws on escape. */
626
+ const assertSourceRealContained = (absPath, kindLabel, declared) => {
627
+ let realPath;
628
+ try {
629
+ realPath = fs.realpathSync(absPath);
630
+ }
631
+ catch (err) {
632
+ throw new Error(`author bundle ${kindLabel} '${declared}' does not exist in source: ${err.message}`);
633
+ }
634
+ if (realPath !== sourceRootReal && !realPath.startsWith(sourceRootReal + nodePath.sep)) {
635
+ throw new Error(`author bundle ${kindLabel} '${declared}' resolves outside bundle source root (symlink escape); rejected`);
636
+ }
637
+ return realPath;
638
+ };
639
+ /** Pre-walk a directory tree with lstat; for every symlink encountered,
640
+ * realpath-check containment under sourceRootReal. Regular files and
641
+ * dirs need no special check (they're inherently contained — the
642
+ * problem class is symlinks escaping). Throws on any escape. */
643
+ const assertNoSymlinkEscapeInTree = (dirAbs, kindLabel, declared) => {
644
+ const stack = [dirAbs];
645
+ while (stack.length > 0) {
646
+ const cur = stack.pop();
647
+ let entries;
648
+ try {
649
+ entries = fs.readdirSync(cur, { withFileTypes: true });
650
+ }
651
+ catch {
652
+ // Unreadable dir — skip; cpSync will surface the failure if material
653
+ continue;
654
+ }
655
+ for (const entry of entries) {
656
+ const entryAbs = nodePath.join(cur, entry.name);
657
+ if (entry.isSymbolicLink()) {
658
+ // Realpath the symlink target; reject if escapes sourceRootReal
659
+ let entryReal;
660
+ try {
661
+ entryReal = fs.realpathSync(entryAbs);
662
+ }
663
+ catch (err) {
664
+ throw new Error(`author bundle ${kindLabel} '${declared}' has unreadable symlink at '${nodePath.relative(sourceRootReal, entryAbs)}': ${err.message}`);
665
+ }
666
+ if (entryReal !== sourceRootReal && !entryReal.startsWith(sourceRootReal + nodePath.sep)) {
667
+ throw new Error(`author bundle ${kindLabel} '${declared}' contains nested symlink at '${nodePath.relative(sourceRootReal, entryAbs)}' escaping bundle source root; rejected`);
668
+ }
669
+ // If the symlink target is a directory under sourceRoot, walk it
670
+ // (cpSync with dereference:true will follow it; we need to
671
+ // validate any nested symlinks too)
672
+ try {
673
+ const st = fs.statSync(entryReal);
674
+ if (st.isDirectory())
675
+ stack.push(entryReal);
676
+ }
677
+ catch { /* unreadable — skip */ }
678
+ }
679
+ else if (entry.isDirectory()) {
680
+ stack.push(entryAbs);
681
+ }
682
+ }
683
+ }
684
+ };
685
+ const authorYaml = fs.readFileSync(authorBundlePath, "utf-8");
686
+ const authorParsed = parsePodBundleManifest(authorYaml);
687
+ const result = {};
688
+ const vendorFile = (declared, kindLabel) => {
689
+ if (!isRelativeSafePath(declared))
690
+ throw new Error(`author bundle ${kindLabel} path '${declared}' is not safe`);
691
+ const sourceAbs = nodePath.resolve(sourceRootReal, declared);
692
+ // Lexical containment + realpath containment (banked 79a89d40 B1)
693
+ if (sourceAbs !== sourceRootReal && !sourceAbs.startsWith(sourceRootReal + nodePath.sep)) {
694
+ throw new Error(`author bundle ${kindLabel} path '${declared}' escapes bundle source root`);
695
+ }
696
+ if (!fs.existsSync(sourceAbs))
697
+ throw new Error(`author bundle ${kindLabel} '${declared}' does not exist in source`);
698
+ assertSourceRealContained(sourceAbs, kindLabel, declared);
699
+ const targetAbs = nodePath.resolve(staging, declared);
700
+ if (!targetAbs.startsWith(stagingResolved + nodePath.sep)) {
701
+ throw new Error(`author bundle ${kindLabel} target '${declared}' escapes staging`);
702
+ }
703
+ fs.mkdirSync(nodePath.dirname(targetAbs), { recursive: true });
704
+ // readFileSync follows the symlink; we already validated its realpath
705
+ // stays in sourceRootReal. Write as regular file (tar safety).
706
+ const content = fs.readFileSync(sourceAbs);
707
+ fs.writeFileSync(targetAbs, content);
708
+ };
709
+ const vendorDir = (declared, kindLabel) => {
710
+ if (!isRelativeSafePath(declared))
711
+ throw new Error(`author bundle ${kindLabel} path '${declared}' is not safe`);
712
+ const sourceAbs = nodePath.resolve(sourceRootReal, declared);
713
+ if (sourceAbs !== sourceRootReal && !sourceAbs.startsWith(sourceRootReal + nodePath.sep)) {
714
+ throw new Error(`author bundle ${kindLabel} path '${declared}' escapes bundle source root`);
715
+ }
716
+ if (!fs.existsSync(sourceAbs))
717
+ throw new Error(`author bundle ${kindLabel} '${declared}' does not exist in source`);
718
+ // Realpath-validate the declared dir itself (catches symlink-to-outside-dir)
719
+ const sourceReal = assertSourceRealContained(sourceAbs, kindLabel, declared);
720
+ // Pre-walk the realpath-resolved dir to catch nested symlink escapes
721
+ // before cpSync dereferences anything
722
+ assertNoSymlinkEscapeInTree(sourceReal, kindLabel, declared);
723
+ const targetAbs = nodePath.resolve(staging, declared);
724
+ if (!targetAbs.startsWith(stagingResolved + nodePath.sep)) {
725
+ throw new Error(`author bundle ${kindLabel} target '${declared}' escapes staging`);
726
+ }
727
+ fs.mkdirSync(nodePath.dirname(targetAbs), { recursive: true });
728
+ // dereference: true → symlinks followed (now validated safe) and
729
+ // written as regular files for tar safety
730
+ fs.cpSync(sourceAbs, targetAbs, { recursive: true, dereference: true });
731
+ };
732
+ // skills[] — file paths
733
+ const rawSkills = authorParsed["skills"];
734
+ if (Array.isArray(rawSkills) && rawSkills.length > 0) {
735
+ const skills = rawSkills.filter((s) => typeof s === "string" && s.length > 0);
736
+ for (const declared of skills)
737
+ vendorFile(declared, "skill");
738
+ if (skills.length > 0)
739
+ result.skills = skills;
740
+ }
741
+ // plugins[] — dir paths via source.path
742
+ const rawPlugins = authorParsed["plugins"];
743
+ if (Array.isArray(rawPlugins) && rawPlugins.length > 0) {
744
+ const plugins = [];
745
+ for (const entry of rawPlugins) {
746
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
747
+ continue;
748
+ const p = entry;
749
+ const s = p["source"];
750
+ if (typeof p["id"] !== "string" || !p["id"])
751
+ continue;
752
+ if (!s || typeof s !== "object" || Array.isArray(s))
753
+ continue;
754
+ const src = s;
755
+ if (src["kind"] !== "local" || typeof src["path"] !== "string" || !src["path"])
756
+ continue;
757
+ vendorDir(src["path"], `plugin '${p["id"]}'`);
758
+ plugins.push({ id: p["id"], source: { kind: "local", path: src["path"] } });
759
+ }
760
+ if (plugins.length > 0)
761
+ result.plugins = plugins;
762
+ }
763
+ // workflow_specs[] — file paths
764
+ const rawWorkflowSpecs = authorParsed["workflow_specs"];
765
+ if (Array.isArray(rawWorkflowSpecs) && rawWorkflowSpecs.length > 0) {
766
+ const workflowSpecs = rawWorkflowSpecs.filter((s) => typeof s === "string" && s.length > 0);
767
+ for (const declared of workflowSpecs)
768
+ vendorFile(declared, "workflow_spec");
769
+ if (workflowSpecs.length > 0)
770
+ result.workflowSpecs = workflowSpecs;
771
+ }
772
+ // context_packs[] — manifest.yaml paths; vendor the parent dir
773
+ const rawContextPacks = authorParsed["context_packs"];
774
+ if (Array.isArray(rawContextPacks) && rawContextPacks.length > 0) {
775
+ const contextPacks = rawContextPacks.filter((s) => typeof s === "string" && s.length > 0);
776
+ for (const declared of contextPacks) {
777
+ const parentRel = nodePath.dirname(declared);
778
+ if (parentRel === ".")
779
+ continue; // declared at root; nothing meaningful to vendor
780
+ vendorDir(parentRel, `context_pack parent of '${declared}'`);
781
+ }
782
+ if (contextPacks.length > 0)
783
+ result.contextPacks = contextPacks;
784
+ }
785
+ // agent_images[] — dir paths
786
+ const rawAgentImages = authorParsed["agent_images"];
787
+ if (Array.isArray(rawAgentImages) && rawAgentImages.length > 0) {
788
+ const agentImages = rawAgentImages.filter((s) => typeof s === "string" && s.length > 0);
789
+ for (const declared of agentImages)
790
+ vendorDir(declared, "agent_image");
791
+ if (agentImages.length > 0)
792
+ result.agentImages = agentImages;
793
+ }
794
+ return result;
795
+ }
67
796
  // POST /api/bundles/create
68
797
  bundleRoutes.post("/create", async (c) => {
69
798
  const { eventBus } = getDeps(c);
@@ -74,6 +803,13 @@ bundleRoutes.post("/create", async (c) => {
74
803
  const outputPath = typeof body["outputPath"] === "string" ? body["outputPath"] : "";
75
804
  const rigRoot = typeof body["rigRoot"] === "string" ? body["rigRoot"] : undefined;
76
805
  const includePackages = Array.isArray(body["includePackages"]) ? body["includePackages"] : undefined;
806
+ // Item 1 / slice-05: build provenance from request body + inject daemonVersion server-side
807
+ const clientProvenance = provenanceFromRequestBody(body["provenance"]);
808
+ const provenance = clientProvenance
809
+ ? { ...clientProvenance, daemonVersion: getDaemonVersion() }
810
+ : undefined;
811
+ // Item 2 / slice-05: build compatibility from request body (no server-side fields)
812
+ const compatibility = compatibilityFromRequestBody(body["compatibility"]);
77
813
  if (!specPath || !bundleName || !bundleVersion || !outputPath) {
78
814
  return c.json({ error: "specPath, bundleName, bundleVersion, and outputPath are required" }, 400);
79
815
  }
@@ -91,7 +827,22 @@ bundleRoutes.post("/create", async (c) => {
91
827
  const tmpStaging = fs.mkdtempSync(nodePath.join(os.tmpdir(), "pod-bundle-create-"));
92
828
  try {
93
829
  const assembler = new PodBundleAssembler({ fsOps: podAssemblerFsOps() });
94
- const result = assembler.assemble({ rigRoot: effectiveRigRoot, rigSpecPath: nodePath.resolve(specPath), outputDir: tmpStaging, bundleName, bundleVersion });
830
+ const result = assembler.assemble({ rigRoot: effectiveRigRoot, rigSpecPath: nodePath.resolve(specPath), outputDir: tmpStaging, bundleName, bundleVersion, provenance, compatibility });
831
+ // Item 6 / Checkpoint 7.5 (QA-20260601 A2 repair): auto-detect
832
+ // author bundle.yaml at the rig source root; vendor declared
833
+ // cross-primitive content into staging + carry the fields onto
834
+ // the manifest. computeIntegrity below covers the vendored content.
835
+ const authorPrimitives = consumeAuthorBundleYaml(effectiveRigRoot, tmpStaging);
836
+ if (authorPrimitives.skills)
837
+ result.manifest.skills = authorPrimitives.skills;
838
+ if (authorPrimitives.plugins)
839
+ result.manifest.plugins = authorPrimitives.plugins;
840
+ if (authorPrimitives.workflowSpecs)
841
+ result.manifest.workflowSpecs = authorPrimitives.workflowSpecs;
842
+ if (authorPrimitives.contextPacks)
843
+ result.manifest.contextPacks = authorPrimitives.contextPacks;
844
+ if (authorPrimitives.agentImages)
845
+ result.manifest.agentImages = authorPrimitives.agentImages;
95
846
  const integrity = computeIntegrity(tmpStaging, integrityFsOps());
96
847
  result.manifest.integrity = integrity;
97
848
  fs.writeFileSync(nodePath.join(tmpStaging, "bundle.yaml"), serializePodBundleManifest(result.manifest), "utf-8");
@@ -144,8 +895,29 @@ bundleRoutes.post("/create", async (c) => {
144
895
  try {
145
896
  const assembler = new BundleAssembler({ fsOps: assemblerFsOps() });
146
897
  const manifest = assembler.assemble({
147
- specPath: nodePath.resolve(specPath), packages, outputDir: tmpStaging, bundleName, bundleVersion,
898
+ specPath: nodePath.resolve(specPath), packages, outputDir: tmpStaging, bundleName, bundleVersion, provenance, compatibility,
148
899
  });
900
+ // Item 6 / Checkpoint 7.5 (QA-20260601 A2 repair, legacy path mirror):
901
+ // auto-detect author bundle.yaml in source dir; vendor + carry
902
+ // cross-primitive fields before integrity. Re-serialize bundle.yaml
903
+ // since the assembler already wrote one without these fields.
904
+ const legacyAuthorPrimitives = consumeAuthorBundleYaml(specDir, tmpStaging);
905
+ const hasLegacyPrimitives = legacyAuthorPrimitives.skills || legacyAuthorPrimitives.plugins ||
906
+ legacyAuthorPrimitives.workflowSpecs || legacyAuthorPrimitives.contextPacks || legacyAuthorPrimitives.agentImages;
907
+ if (hasLegacyPrimitives) {
908
+ if (legacyAuthorPrimitives.skills)
909
+ manifest.skills = legacyAuthorPrimitives.skills;
910
+ if (legacyAuthorPrimitives.plugins)
911
+ manifest.plugins = legacyAuthorPrimitives.plugins;
912
+ if (legacyAuthorPrimitives.workflowSpecs)
913
+ manifest.workflowSpecs = legacyAuthorPrimitives.workflowSpecs;
914
+ if (legacyAuthorPrimitives.contextPacks)
915
+ manifest.contextPacks = legacyAuthorPrimitives.contextPacks;
916
+ if (legacyAuthorPrimitives.agentImages)
917
+ manifest.agentImages = legacyAuthorPrimitives.agentImages;
918
+ const { serializeLegacyBundleManifest } = await import("../domain/bundle-types.js");
919
+ fs.writeFileSync(nodePath.join(tmpStaging, "bundle.yaml"), serializeLegacyBundleManifest(manifest), "utf-8");
920
+ }
149
921
  const integrity = computeIntegrity(tmpStaging, integrityFsOps());
150
922
  writeIntegrity(tmpStaging, integrity, integrityFsOps());
151
923
  const archiveHash = await pack(tmpStaging, nodePath.resolve(outputPath));
@@ -224,6 +996,35 @@ bundleRoutes.post("/inspect", async (c) => {
224
996
  algorithm: integritySection.algorithm ?? "sha256",
225
997
  files: integritySection.files ?? {},
226
998
  } : undefined,
999
+ // Item 1 / slice-05: surface provenance in normalized camelCase so the
1000
+ // /api/bundles/inspect contract is one shape regardless of v1 vs v2
1001
+ // (v1 path normalizes through normalizeLegacyBundleManifest below).
1002
+ // Field is optional; undefined when bundle has no provenance.
1003
+ provenance: normalizeProvenanceBlock(rawParsed["provenance"]),
1004
+ // Item 2 / slice-05: surface compatibility normalized to camelCase
1005
+ // (same single-contract reason as provenance above). v1 already
1006
+ // surfaces via the normalizer at the end of this handler.
1007
+ compatibility: normalizeCompatibilityBlock(rawParsed["compatibility"]),
1008
+ // Item 6 / Checkpoint 7.5 / QA-20260601 C1 repair: surface the 5
1009
+ // cross-primitive blocks normalized to camelCase so /inspect's
1010
+ // contract carries the same shape v1's normalizer already
1011
+ // surfaces. Raw YAML keys are snake_case; expose camelCase to
1012
+ // match the rest of the v2 inspect contract.
1013
+ skills: Array.isArray(rawParsed["skills"])
1014
+ ? rawParsed["skills"].filter((s) => typeof s === "string")
1015
+ : undefined,
1016
+ plugins: Array.isArray(rawParsed["plugins"])
1017
+ ? rawParsed["plugins"].filter((p) => p && typeof p === "object")
1018
+ : undefined,
1019
+ workflowSpecs: Array.isArray(rawParsed["workflow_specs"])
1020
+ ? rawParsed["workflow_specs"].filter((s) => typeof s === "string")
1021
+ : undefined,
1022
+ contextPacks: Array.isArray(rawParsed["context_packs"])
1023
+ ? rawParsed["context_packs"].filter((s) => typeof s === "string")
1024
+ : undefined,
1025
+ agentImages: Array.isArray(rawParsed["agent_images"])
1026
+ ? rawParsed["agent_images"].filter((s) => typeof s === "string")
1027
+ : undefined,
227
1028
  };
228
1029
  const integrityCompat = integritySection ? {
229
1030
  schemaVersion: 2,
@@ -252,23 +1053,110 @@ bundleRoutes.post("/inspect", async (c) => {
252
1053
  fs.rmSync(tmpDir, { recursive: true, force: true });
253
1054
  }
254
1055
  });
1056
+ // GET /api/bundles/history — Item 4 / slice-05 Checkpoint 5.2
1057
+ // Returns the install audit JSONL records (optionally filtered by rig name
1058
+ // and / or since timestamp). Read-only — no audit-write side effects.
1059
+ // Empty audit file returns []. Reader fails-open on malformed JSONL lines
1060
+ // (forward-compat with future record-shape evolutions).
1061
+ bundleRoutes.get("/history", async (c) => {
1062
+ const rig = c.req.query("rig");
1063
+ const since = c.req.query("since");
1064
+ const reader = new BundleAuditReader({
1065
+ opts: { auditPath: bundleAuditPath() },
1066
+ fsOps: auditFsOps(),
1067
+ });
1068
+ const records = reader.list({
1069
+ rig: typeof rig === "string" && rig.length > 0 ? rig : undefined,
1070
+ since: typeof since === "string" && since.length > 0 ? since : undefined,
1071
+ });
1072
+ return c.json({ records, total: records.length }, 200);
1073
+ });
255
1074
  // POST /api/bundles/install — reuses full bootstrap lifecycle
256
1075
  bundleRoutes.post("/install", async (c) => {
257
- const { bootstrapOrchestrator, bootstrapRepo, eventBus } = getDeps(c);
1076
+ const { bootstrapOrchestrator, bootstrapRepo, eventBus, rigRepo } = getDeps(c);
258
1077
  const body = await c.req.json().catch(() => ({}));
259
1078
  const bundlePath = typeof body["bundlePath"] === "string" ? body["bundlePath"] : "";
260
1079
  const plan = body["plan"] === true;
261
1080
  const autoApprove = body["autoApprove"] === true;
262
1081
  const targetRoot = typeof body["targetRoot"] === "string" ? body["targetRoot"] : undefined;
1082
+ // Item 2 / slice-05 Checkpoint 3.3: install-time compatibility check inputs
1083
+ const skipVersionCheck = body["skipVersionCheck"] === true;
1084
+ const clientCliVersion = typeof body["cliVersion"] === "string" ? body["cliVersion"] : undefined;
1085
+ // Item 3 / slice-05 Checkpoint 4.2: install-time conflict check inputs
1086
+ const force = body["force"] === true;
263
1087
  if (!bundlePath)
264
1088
  return c.json({ error: "bundlePath is required" }, 400);
265
1089
  if (!plan && !targetRoot)
266
1090
  return c.json({ error: "targetRoot is required for apply mode" }, 400);
267
- // Concurrency lock
1091
+ // Concurrency lock — runs BEFORE compat check so the existing 409
1092
+ // semantic (concurrent install detection) is preserved verbatim.
268
1093
  if (!bootstrapOrchestrator.tryAcquire(bundlePath)) {
269
1094
  return c.json({ error: "Bundle install already in progress", code: "conflict" }, 409);
270
1095
  }
271
1096
  try {
1097
+ // Item 2 / slice-05 Checkpoint 3.3: install-time compatibility check
1098
+ // Runs AFTER the lock + BEFORE bootstrap delegation. Mismatch returns a
1099
+ // 3-part error and exits the lifecycle (lock releases via the outer
1100
+ // finally). Operator override via --skip-version-check (request body
1101
+ // skipVersionCheck=true).
1102
+ // Item 2 + Item 3 / slice-05: single safe extract pass yields both the
1103
+ // bundle manifest (for compat check) and the rig name (for conflict check).
1104
+ // Caller can skip the compat check via skipVersionCheck; the conflict check
1105
+ // also runs from this same extract pass unless --force bypasses it.
1106
+ let installMeta = null;
1107
+ if (!skipVersionCheck || !force) {
1108
+ try {
1109
+ installMeta = await extractInstallTimeMetadata(bundlePath);
1110
+ }
1111
+ catch (err) {
1112
+ return c.json({
1113
+ error: "Bundle install pre-check could not run (extraction failed)",
1114
+ detail: err.message,
1115
+ resolutions: [
1116
+ "confirm the bundle path is correct and the archive is readable",
1117
+ "pass --skip-version-check and --force to bypass both pre-checks (NOT recommended unless intentional)",
1118
+ ],
1119
+ }, 400);
1120
+ }
1121
+ }
1122
+ if (!skipVersionCheck && installMeta) {
1123
+ const compatibility = normalizeCompatibilityBlock(installMeta.bundleManifest["compatibility"]);
1124
+ const failures = checkBundleCompatibility(compatibility, getDaemonVersion(), clientCliVersion);
1125
+ if (failures) {
1126
+ return c.json({
1127
+ error: "Bundle compatibility check failed",
1128
+ failures,
1129
+ resolutions: [
1130
+ "upgrade the affected runtime to the required version (recommended)",
1131
+ "use a bundle with relaxed minimum requirements",
1132
+ "pass --skip-version-check to bypass for an operator-explicit override (NOT recommended for routine use)",
1133
+ ],
1134
+ }, 400);
1135
+ }
1136
+ }
1137
+ // Item 3 / slice-05 Checkpoint 4.2: install-time conflict check
1138
+ // Runs AFTER the compat check + BEFORE bootstrap delegation. Mismatch
1139
+ // returns a 400 with the 3-part error shape (error + conflicts[] +
1140
+ // resolutions[]). Operator override via --force (request body force=true).
1141
+ // The check fails CLOSED on extraction failure (handled above) and
1142
+ // fail-OPEN on missing rig name in the bundle (no rig name to compare).
1143
+ if (!force && installMeta && rigRepo) {
1144
+ const runningRigs = rigRepo.listRigs().map((r) => ({ rigId: r.id, name: r.name }));
1145
+ const report = detectBundleConflicts({
1146
+ bundleRigName: installMeta.rigName ?? "",
1147
+ runningRigs,
1148
+ });
1149
+ if (report.hasConflicts) {
1150
+ return c.json({
1151
+ error: "Bundle install conflict check failed",
1152
+ conflicts: report.conflicts,
1153
+ resolutions: [
1154
+ "stop the conflicting running rig and re-attempt install",
1155
+ "use --force to bypass for an operator-explicit override (NOT recommended for routine use; conflicts may produce partial install state)",
1156
+ ],
1157
+ }, 400);
1158
+ }
1159
+ }
272
1160
  if (plan) {
273
1161
  // Plan mode: no run lifecycle
274
1162
  try {
@@ -309,21 +1197,92 @@ bundleRoutes.post("/install", async (c) => {
309
1197
  });
310
1198
  if (result.status === "completed") {
311
1199
  eventBus.emit({ type: "bootstrap.completed", runId: result.runId, rigId: result.rigId, sourceRef: bundlePath });
312
- return c.json(result, 201);
1200
+ writeInstallAudit({
1201
+ bundlePath, outcome: "success", targetRigId: result.rigId,
1202
+ targetRigName: installMeta?.rigName, cliVersion: clientCliVersion,
1203
+ bundleManifest: installMeta?.bundleManifest,
1204
+ });
1205
+ // Item 6 / Checkpoint 7.3: route any declared skills after successful
1206
+ // install. Best-effort: a routing failure does NOT fail the install
1207
+ // response (the install already succeeded; skills routing is a side-
1208
+ // channel post-install step). Routing result included in response body
1209
+ // so operators see what landed.
1210
+ let skillsRouting = null;
1211
+ let pluginsRouting = null;
1212
+ let workflowSpecsRouting = null;
1213
+ let contextPacksRouting = null;
1214
+ let agentImagesRouting = null;
1215
+ try {
1216
+ skillsRouting = await routeSkillsAfterBootstrap(bundlePath);
1217
+ }
1218
+ catch {
1219
+ // Side-channel failure; install already succeeded
1220
+ }
1221
+ try {
1222
+ pluginsRouting = await routePluginsAfterBootstrap(bundlePath);
1223
+ }
1224
+ catch {
1225
+ // Side-channel failure; install already succeeded
1226
+ }
1227
+ try {
1228
+ workflowSpecsRouting = await routeWorkflowSpecsAfterBootstrap(bundlePath);
1229
+ }
1230
+ catch {
1231
+ // Side-channel failure; install already succeeded
1232
+ }
1233
+ try {
1234
+ contextPacksRouting = await routeContextPacksAfterBootstrap(bundlePath);
1235
+ }
1236
+ catch {
1237
+ // Side-channel failure; install already succeeded
1238
+ }
1239
+ try {
1240
+ agentImagesRouting = await routeAgentImagesAfterBootstrap(bundlePath);
1241
+ }
1242
+ catch {
1243
+ // Side-channel failure; install already succeeded
1244
+ }
1245
+ const extras = {};
1246
+ if (skillsRouting)
1247
+ extras.skillsRouting = skillsRouting;
1248
+ if (pluginsRouting)
1249
+ extras.pluginsRouting = pluginsRouting;
1250
+ if (workflowSpecsRouting)
1251
+ extras.workflowSpecsRouting = workflowSpecsRouting;
1252
+ if (contextPacksRouting)
1253
+ extras.contextPacksRouting = contextPacksRouting;
1254
+ if (agentImagesRouting)
1255
+ extras.agentImagesRouting = agentImagesRouting;
1256
+ return c.json(Object.keys(extras).length > 0 ? { ...result, ...extras } : result, 201);
313
1257
  }
314
1258
  if (result.status === "partial") {
315
1259
  const ok = result.stages.filter((s) => s.status === "ok").length;
316
1260
  const fail = result.stages.filter((s) => s.status === "failed" || s.status === "blocked").length;
317
1261
  eventBus.emit({ type: "bootstrap.partial", runId: result.runId, sourceRef: bundlePath, rigId: result.rigId, completed: ok, failed: fail });
1262
+ writeInstallAudit({
1263
+ bundlePath, outcome: "partial", targetRigId: result.rigId,
1264
+ targetRigName: installMeta?.rigName, cliVersion: clientCliVersion,
1265
+ bundleManifest: installMeta?.bundleManifest,
1266
+ });
318
1267
  return c.json(result, 200);
319
1268
  }
320
1269
  eventBus.emit({ type: "bootstrap.failed", runId: result.runId, sourceRef: bundlePath, error: result.errors[0] ?? "failed" });
1270
+ writeInstallAudit({
1271
+ bundlePath, outcome: "failed",
1272
+ targetRigName: installMeta?.rigName, cliVersion: clientCliVersion,
1273
+ bundleManifest: installMeta?.bundleManifest,
1274
+ });
321
1275
  const hasBlocked = result.stages.some((s) => s.status === "blocked");
322
1276
  return c.json(result, hasBlocked ? 409 : 500);
323
1277
  }
324
1278
  catch (err) {
325
1279
  bootstrapRepo.updateRunStatus(run.id, "failed");
326
1280
  eventBus.emit({ type: "bootstrap.failed", runId: run.id, sourceRef: bundlePath, error: err.message });
1281
+ writeInstallAudit({
1282
+ bundlePath, outcome: "failed",
1283
+ targetRigName: installMeta?.rigName, cliVersion: clientCliVersion,
1284
+ bundleManifest: installMeta?.bundleManifest,
1285
+ });
327
1286
  return c.json({ runId: run.id, status: "failed", error: err.message }, 500);
328
1287
  }
329
1288
  }