@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.
- package/daemon/assets/plugins/openrig-core/skills/forming-an-openrig-mental-model/SKILL.md +1 -1
- package/daemon/dist/adapters/codex-runtime-adapter.d.ts.map +1 -1
- package/daemon/dist/adapters/codex-runtime-adapter.js +2 -1
- package/daemon/dist/adapters/codex-runtime-adapter.js.map +1 -1
- package/daemon/dist/adapters/tmux.d.ts +36 -0
- package/daemon/dist/adapters/tmux.d.ts.map +1 -1
- package/daemon/dist/adapters/tmux.js +66 -0
- package/daemon/dist/adapters/tmux.js.map +1 -1
- package/daemon/dist/db/migrations/037_mission_control_actions.d.ts +5 -1
- package/daemon/dist/db/migrations/037_mission_control_actions.d.ts.map +1 -1
- package/daemon/dist/db/migrations/037_mission_control_actions.js +5 -1
- package/daemon/dist/db/migrations/037_mission_control_actions.js.map +1 -1
- package/daemon/dist/db/migrations/041_rig_policy.d.ts +32 -0
- package/daemon/dist/db/migrations/041_rig_policy.d.ts.map +1 -0
- package/daemon/dist/db/migrations/041_rig_policy.js +47 -0
- package/daemon/dist/db/migrations/041_rig_policy.js.map +1 -0
- package/daemon/dist/domain/agent-images/resume-token-discovery.js +1 -1
- package/daemon/dist/domain/agent-images/resume-token-discovery.js.map +1 -1
- package/daemon/dist/domain/agent-images/snapshot-capturer.d.ts.map +1 -1
- package/daemon/dist/domain/agent-images/snapshot-capturer.js +2 -1
- package/daemon/dist/domain/agent-images/snapshot-capturer.js.map +1 -1
- package/daemon/dist/domain/agent-manifest.js +27 -0
- package/daemon/dist/domain/agent-manifest.js.map +1 -1
- package/daemon/dist/domain/bootstrap-orchestrator.d.ts.map +1 -1
- package/daemon/dist/domain/bootstrap-orchestrator.js +86 -6
- package/daemon/dist/domain/bootstrap-orchestrator.js.map +1 -1
- package/daemon/dist/domain/bundle-agent-images-router.d.ts +95 -0
- package/daemon/dist/domain/bundle-agent-images-router.d.ts.map +1 -0
- package/daemon/dist/domain/bundle-agent-images-router.js +140 -0
- package/daemon/dist/domain/bundle-agent-images-router.js.map +1 -0
- package/daemon/dist/domain/bundle-assembler.d.ts +18 -1
- package/daemon/dist/domain/bundle-assembler.d.ts.map +1 -1
- package/daemon/dist/domain/bundle-assembler.js +11 -1
- package/daemon/dist/domain/bundle-assembler.js.map +1 -1
- package/daemon/dist/domain/bundle-audit.d.ts +89 -0
- package/daemon/dist/domain/bundle-audit.d.ts.map +1 -0
- package/daemon/dist/domain/bundle-audit.js +87 -0
- package/daemon/dist/domain/bundle-audit.js.map +1 -0
- package/daemon/dist/domain/bundle-conflict-detector.d.ts +57 -0
- package/daemon/dist/domain/bundle-conflict-detector.d.ts.map +1 -0
- package/daemon/dist/domain/bundle-conflict-detector.js +49 -0
- package/daemon/dist/domain/bundle-conflict-detector.js.map +1 -0
- package/daemon/dist/domain/bundle-context-packs-router.d.ts +100 -0
- package/daemon/dist/domain/bundle-context-packs-router.d.ts.map +1 -0
- package/daemon/dist/domain/bundle-context-packs-router.js +158 -0
- package/daemon/dist/domain/bundle-context-packs-router.js.map +1 -0
- package/daemon/dist/domain/bundle-plugins-router.d.ts +69 -0
- package/daemon/dist/domain/bundle-plugins-router.d.ts.map +1 -0
- package/daemon/dist/domain/bundle-plugins-router.js +87 -0
- package/daemon/dist/domain/bundle-plugins-router.js.map +1 -0
- package/daemon/dist/domain/bundle-skills-router.d.ts +62 -0
- package/daemon/dist/domain/bundle-skills-router.d.ts.map +1 -0
- package/daemon/dist/domain/bundle-skills-router.js +90 -0
- package/daemon/dist/domain/bundle-skills-router.js.map +1 -0
- package/daemon/dist/domain/bundle-types.d.ts +106 -0
- package/daemon/dist/domain/bundle-types.d.ts.map +1 -1
- package/daemon/dist/domain/bundle-types.js +375 -0
- package/daemon/dist/domain/bundle-types.js.map +1 -1
- package/daemon/dist/domain/bundle-workflow-specs-router.d.ts +94 -0
- package/daemon/dist/domain/bundle-workflow-specs-router.d.ts.map +1 -0
- package/daemon/dist/domain/bundle-workflow-specs-router.js +144 -0
- package/daemon/dist/domain/bundle-workflow-specs-router.js.map +1 -0
- package/daemon/dist/domain/context-monitor.d.ts +5 -1
- package/daemon/dist/domain/context-monitor.d.ts.map +1 -1
- package/daemon/dist/domain/context-monitor.js +28 -15
- package/daemon/dist/domain/context-monitor.js.map +1 -1
- package/daemon/dist/domain/graph-projection.d.ts +6 -0
- package/daemon/dist/domain/graph-projection.d.ts.map +1 -1
- package/daemon/dist/domain/graph-projection.js +6 -0
- package/daemon/dist/domain/graph-projection.js.map +1 -1
- package/daemon/dist/domain/native-resume-probe.js +7 -7
- package/daemon/dist/domain/native-resume-probe.js.map +1 -1
- package/daemon/dist/domain/node-inventory.d.ts +18 -0
- package/daemon/dist/domain/node-inventory.d.ts.map +1 -1
- package/daemon/dist/domain/node-inventory.js +98 -0
- package/daemon/dist/domain/node-inventory.js.map +1 -1
- package/daemon/dist/domain/node-launcher.d.ts +12 -0
- package/daemon/dist/domain/node-launcher.d.ts.map +1 -1
- package/daemon/dist/domain/node-launcher.js +25 -1
- package/daemon/dist/domain/node-launcher.js.map +1 -1
- package/daemon/dist/domain/pod-bundle-assembler.d.ts +16 -1
- package/daemon/dist/domain/pod-bundle-assembler.d.ts.map +1 -1
- package/daemon/dist/domain/pod-bundle-assembler.js +11 -1
- package/daemon/dist/domain/pod-bundle-assembler.js.map +1 -1
- package/daemon/dist/domain/profile-resolver.d.ts +10 -0
- package/daemon/dist/domain/profile-resolver.d.ts.map +1 -1
- package/daemon/dist/domain/profile-resolver.js +4 -0
- package/daemon/dist/domain/profile-resolver.js.map +1 -1
- package/daemon/dist/domain/ps-projection.d.ts +22 -0
- package/daemon/dist/domain/ps-projection.d.ts.map +1 -1
- package/daemon/dist/domain/ps-projection.js +57 -0
- package/daemon/dist/domain/ps-projection.js.map +1 -1
- package/daemon/dist/domain/queue-repository.d.ts +37 -1
- package/daemon/dist/domain/queue-repository.d.ts.map +1 -1
- package/daemon/dist/domain/queue-repository.js +99 -2
- package/daemon/dist/domain/queue-repository.js.map +1 -1
- package/daemon/dist/domain/restore-check-service.d.ts +4 -0
- package/daemon/dist/domain/restore-check-service.d.ts.map +1 -1
- package/daemon/dist/domain/restore-check-service.js +16 -7
- package/daemon/dist/domain/restore-check-service.js.map +1 -1
- package/daemon/dist/domain/rig-policy/rig-policy-defaults.d.ts +48 -0
- package/daemon/dist/domain/rig-policy/rig-policy-defaults.d.ts.map +1 -0
- package/daemon/dist/domain/rig-policy/rig-policy-defaults.js +96 -0
- package/daemon/dist/domain/rig-policy/rig-policy-defaults.js.map +1 -0
- package/daemon/dist/domain/rig-policy/rig-policy-store.d.ts +46 -0
- package/daemon/dist/domain/rig-policy/rig-policy-store.d.ts.map +1 -0
- package/daemon/dist/domain/rig-policy/rig-policy-store.js +171 -0
- package/daemon/dist/domain/rig-policy/rig-policy-store.js.map +1 -0
- package/daemon/dist/domain/rig-policy/rig-policy-types.d.ts +145 -0
- package/daemon/dist/domain/rig-policy/rig-policy-types.d.ts.map +1 -0
- package/daemon/dist/domain/rig-policy/rig-policy-types.js +52 -0
- package/daemon/dist/domain/rig-policy/rig-policy-types.js.map +1 -0
- package/daemon/dist/domain/rig-policy/rig-policy-validator.d.ts +58 -0
- package/daemon/dist/domain/rig-policy/rig-policy-validator.d.ts.map +1 -0
- package/daemon/dist/domain/rig-policy/rig-policy-validator.js +211 -0
- package/daemon/dist/domain/rig-policy/rig-policy-validator.js.map +1 -0
- package/daemon/dist/domain/rigspec-instantiator.d.ts +1 -1
- package/daemon/dist/domain/rigspec-instantiator.d.ts.map +1 -1
- package/daemon/dist/domain/rigspec-instantiator.js +109 -18
- package/daemon/dist/domain/rigspec-instantiator.js.map +1 -1
- package/daemon/dist/domain/seat-activity-service.d.ts +79 -0
- package/daemon/dist/domain/seat-activity-service.d.ts.map +1 -0
- package/daemon/dist/domain/seat-activity-service.js +123 -0
- package/daemon/dist/domain/seat-activity-service.js.map +1 -0
- package/daemon/dist/domain/slices/slice-indexer.d.ts +12 -0
- package/daemon/dist/domain/slices/slice-indexer.d.ts.map +1 -1
- package/daemon/dist/domain/slices/slice-indexer.js +23 -0
- package/daemon/dist/domain/slices/slice-indexer.js.map +1 -1
- package/daemon/dist/domain/spec-library-service.d.ts.map +1 -1
- package/daemon/dist/domain/spec-library-service.js +19 -0
- package/daemon/dist/domain/spec-library-service.js.map +1 -1
- package/daemon/dist/domain/types.d.ts +79 -3
- package/daemon/dist/domain/types.d.ts.map +1 -1
- package/daemon/dist/domain/types.js.map +1 -1
- package/daemon/dist/domain/workflow-spec-cache.d.ts +22 -0
- package/daemon/dist/domain/workflow-spec-cache.d.ts.map +1 -1
- package/daemon/dist/domain/workflow-spec-cache.js +37 -0
- package/daemon/dist/domain/workflow-spec-cache.js.map +1 -1
- package/daemon/dist/domain/workspace/default-workspace-scaffold.d.ts +7 -0
- package/daemon/dist/domain/workspace/default-workspace-scaffold.d.ts.map +1 -1
- package/daemon/dist/domain/workspace/default-workspace-scaffold.js +156 -3
- package/daemon/dist/domain/workspace/default-workspace-scaffold.js.map +1 -1
- package/daemon/dist/domain/workspace/workspace-doctor.d.ts +197 -0
- package/daemon/dist/domain/workspace/workspace-doctor.d.ts.map +1 -0
- package/daemon/dist/domain/workspace/workspace-doctor.js +448 -0
- package/daemon/dist/domain/workspace/workspace-doctor.js.map +1 -0
- package/daemon/dist/index.d.ts.map +1 -1
- package/daemon/dist/index.js +11 -0
- package/daemon/dist/index.js.map +1 -1
- package/daemon/dist/routes/bundles.d.ts.map +1 -1
- package/daemon/dist/routes/bundles.js +966 -7
- package/daemon/dist/routes/bundles.js.map +1 -1
- package/daemon/dist/routes/config.d.ts.map +1 -1
- package/daemon/dist/routes/config.js +19 -2
- package/daemon/dist/routes/config.js.map +1 -1
- package/daemon/dist/routes/info.d.ts +3 -0
- package/daemon/dist/routes/info.d.ts.map +1 -0
- package/daemon/dist/routes/info.js +20 -0
- package/daemon/dist/routes/info.js.map +1 -0
- package/daemon/dist/routes/queue.d.ts +18 -0
- package/daemon/dist/routes/queue.d.ts.map +1 -1
- package/daemon/dist/routes/queue.js +97 -5
- package/daemon/dist/routes/queue.js.map +1 -1
- package/daemon/dist/routes/rig-policy.d.ts +9 -0
- package/daemon/dist/routes/rig-policy.d.ts.map +1 -0
- package/daemon/dist/routes/rig-policy.js +174 -0
- package/daemon/dist/routes/rig-policy.js.map +1 -0
- package/daemon/dist/routes/rigs.d.ts.map +1 -1
- package/daemon/dist/routes/rigs.js +10 -2
- package/daemon/dist/routes/rigs.js.map +1 -1
- package/daemon/dist/routes/sessions.d.ts.map +1 -1
- package/daemon/dist/routes/sessions.js +26 -3
- package/daemon/dist/routes/sessions.js.map +1 -1
- package/daemon/dist/routes/up.d.ts +25 -0
- package/daemon/dist/routes/up.d.ts.map +1 -1
- package/daemon/dist/routes/up.js +70 -3
- package/daemon/dist/routes/up.js.map +1 -1
- package/daemon/dist/routes/workspace.d.ts.map +1 -1
- package/daemon/dist/routes/workspace.js +80 -1
- package/daemon/dist/routes/workspace.js.map +1 -1
- package/daemon/dist/server.d.ts +10 -0
- package/daemon/dist/server.d.ts.map +1 -1
- package/daemon/dist/server.js +11 -0
- package/daemon/dist/server.js.map +1 -1
- package/daemon/dist/startup.d.ts.map +1 -1
- package/daemon/dist/startup.js +45 -4
- package/daemon/dist/startup.js.map +1 -1
- package/daemon/specs/agents/orchestration/orchestrator/guidance/role.md +28 -4
- package/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md +285 -8
- package/daemon/specs/agents/shared/skills/core/rig-lifecycle/SKILL.md +2 -2
- package/daemon/specs/rigs/launch/conveyor/rig.yaml +1 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -1
- package/dist/client.js.map +1 -1
- package/dist/commands/bundle.d.ts.map +1 -1
- package/dist/commands/bundle.js +123 -2
- package/dist/commands/bundle.js.map +1 -1
- package/dist/commands/config-init-workspace.d.ts.map +1 -1
- package/dist/commands/config-init-workspace.js +40 -5
- package/dist/commands/config-init-workspace.js.map +1 -1
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +51 -8
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/queue.d.ts +6 -0
- package/dist/commands/queue.d.ts.map +1 -1
- package/dist/commands/queue.js +103 -5
- package/dist/commands/queue.js.map +1 -1
- package/dist/commands/rig-policy.d.ts +48 -0
- package/dist/commands/rig-policy.d.ts.map +1 -0
- package/dist/commands/rig-policy.js +429 -0
- package/dist/commands/rig-policy.js.map +1 -0
- package/dist/commands/scope.d.ts +6 -0
- package/dist/commands/scope.d.ts.map +1 -0
- package/dist/commands/scope.js +659 -0
- package/dist/commands/scope.js.map +1 -0
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +47 -0
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/workflow.d.ts +9 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +139 -1
- package/dist/commands/workflow.js.map +1 -1
- package/dist/commands/workspace.d.ts +20 -0
- package/dist/commands/workspace.d.ts.map +1 -1
- package/dist/commands/workspace.js +138 -7
- package/dist/commands/workspace.js.map +1 -1
- package/dist/daemon-lifecycle.d.ts +25 -1
- package/dist/daemon-lifecycle.d.ts.map +1 -1
- package/dist/daemon-lifecycle.js +33 -2
- package/dist/daemon-lifecycle.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/scope/dot-id.d.ts +43 -0
- package/dist/lib/scope/dot-id.d.ts.map +1 -0
- package/dist/lib/scope/dot-id.js +165 -0
- package/dist/lib/scope/dot-id.js.map +1 -0
- package/dist/lib/scope/scope-fs.d.ts +79 -0
- package/dist/lib/scope/scope-fs.d.ts.map +1 -0
- package/dist/lib/scope/scope-fs.js +468 -0
- package/dist/lib/scope/scope-fs.js.map +1 -0
- package/dist/lib/scope/templates.d.ts +38 -0
- package/dist/lib/scope/templates.d.ts.map +1 -0
- package/dist/lib/scope/templates.js +109 -0
- package/dist/lib/scope/templates.js.map +1 -0
- package/dist/lib/scope/types.d.ts +61 -0
- package/dist/lib/scope/types.d.ts.map +1 -0
- package/dist/lib/scope/types.js +38 -0
- package/dist/lib/scope/types.js.map +1 -0
- package/dist/lib/scope-templates/backlog-deprecation.md +26 -0
- package/dist/lib/scope-templates/backlog-tech-debt.md +22 -0
- package/dist/lib/scope-templates/bug-fix.md +30 -0
- package/dist/lib/scope-templates/mission-notes.md +97 -0
- package/dist/lib/scope-templates/mission-placeholder.md +20 -0
- package/dist/lib/scope-templates/mission-release.md +25 -0
- package/dist/lib/scope-templates/placeholder.md +30 -0
- package/dist/lib/scope-templates/release-feature.md +26 -0
- package/dist/lib/scope-templates/research.md +22 -0
- package/package.json +2 -2
- package/ui/dist/assets/index-GjTxfi9S.js +547 -0
- package/ui/dist/index.html +1 -1
- 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
|
-
|
|
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
|
}
|