create-walle 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/lib/release-schema.json +2 -2
- package/lib/runtime-closure-policy.json +8 -7
- package/package.json +1 -1
- package/template/coding-task-manager/api-prompts.js +45 -7
- package/template/coding-task-manager/bin/claude-hook.js +55 -1
- package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
- package/template/coding-task-manager/bin/land.js +2069 -0
- package/template/coding-task-manager/bin/memory-census.js +4 -2
- package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
- package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
- package/template/coding-task-manager/db.js +156 -32
- package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
- package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
- package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
- package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
- package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
- package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
- package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
- package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
- package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
- package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
- package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
- package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
- package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
- package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
- package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
- package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
- package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
- package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
- package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
- package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
- package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
- package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
- package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
- package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
- package/template/coding-task-manager/lib/agent-definitions.js +19 -0
- package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
- package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
- package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
- package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
- package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
- package/template/coding-task-manager/lib/agent-presets.js +84 -1
- package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
- package/template/coding-task-manager/lib/agent-version-service.js +74 -11
- package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
- package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
- package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
- package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
- package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
- package/template/coding-task-manager/lib/api/fleet.js +118 -0
- package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
- package/template/coding-task-manager/lib/api/ingress.js +9 -1
- package/template/coding-task-manager/lib/api/landing.js +403 -0
- package/template/coding-task-manager/lib/api/machines.js +691 -2
- package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
- package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
- package/template/coding-task-manager/lib/api/session-attach.js +623 -0
- package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
- package/template/coding-task-manager/lib/api/sessions.js +77 -51
- package/template/coding-task-manager/lib/api/setup-network.js +544 -16
- package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
- package/template/coding-task-manager/lib/api/setup.js +829 -10
- package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
- package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
- package/template/coding-task-manager/lib/auth-rules.js +292 -21
- package/template/coding-task-manager/lib/background-stop.js +369 -0
- package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
- package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
- package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
- package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
- package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
- package/template/coding-task-manager/lib/build-info.js +4 -2
- package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
- package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
- package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
- package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
- package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
- package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
- package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
- package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
- package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
- package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
- package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
- package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
- package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
- package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
- package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
- package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
- package/template/coding-task-manager/lib/conversation-sync.js +355 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
- package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
- package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
- package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
- package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
- package/template/coding-task-manager/lib/db-domains.js +1 -0
- package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
- package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
- package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
- package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
- package/template/coding-task-manager/lib/feature-manifest.js +4 -0
- package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
- package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
- package/template/coding-task-manager/lib/fleet-hub.js +762 -0
- package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
- package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
- package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
- package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
- package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
- package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
- package/template/coding-task-manager/lib/fleet-update.js +798 -0
- package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
- package/template/coding-task-manager/lib/fs-watcher.js +58 -42
- package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
- package/template/coding-task-manager/lib/headless-term-service.js +84 -8
- package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
- package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
- package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
- package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
- package/template/coding-task-manager/lib/host-handoff.js +16 -19
- package/template/coding-task-manager/lib/host-stats.js +150 -3
- package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
- package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
- package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
- package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
- package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
- package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
- package/template/coding-task-manager/lib/hub-bridge.js +398 -0
- package/template/coding-task-manager/lib/idle-notify.js +491 -0
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
- package/template/coding-task-manager/lib/instance-manifest.js +5 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
- package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
- package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
- package/template/coding-task-manager/lib/landing/config.js +536 -0
- package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
- package/template/coding-task-manager/lib/landing/gate.js +360 -0
- package/template/coding-task-manager/lib/landing/git.js +344 -0
- package/template/coding-task-manager/lib/landing/lease.js +247 -0
- package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
- package/template/coding-task-manager/lib/landing/proof.js +223 -0
- package/template/coding-task-manager/lib/landing/queue.js +998 -0
- package/template/coding-task-manager/lib/landing/radar.js +489 -0
- package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
- package/template/coding-task-manager/lib/landing/state.js +263 -0
- package/template/coding-task-manager/lib/landing/stop.js +49 -0
- package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
- package/template/coding-task-manager/lib/landing/sync.js +456 -0
- package/template/coding-task-manager/lib/landing/train.js +885 -0
- package/template/coding-task-manager/lib/landing/window.js +57 -0
- package/template/coding-task-manager/lib/launch-environment.js +121 -0
- package/template/coding-task-manager/lib/launch-model-override.js +36 -10
- package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
- package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
- package/template/coding-task-manager/lib/lead-model-options.js +160 -3
- package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
- package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
- package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
- package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
- package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
- package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
- package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
- package/template/coding-task-manager/lib/machine-registry.js +750 -5
- package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
- package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
- package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
- package/template/coding-task-manager/lib/memory-join.js +337 -0
- package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
- package/template/coding-task-manager/lib/memory-sync.js +565 -0
- package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
- package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
- package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
- package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
- package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
- package/template/coding-task-manager/lib/model-availability.js +727 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
- package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
- package/template/coding-task-manager/lib/model-id-rule.js +19 -0
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
- package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
- package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
- package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
- package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
- package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
- package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
- package/template/coding-task-manager/lib/native-history-control.js +6 -4
- package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
- package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
- package/template/coding-task-manager/lib/opencode-cli.js +480 -0
- package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
- package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
- package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
- package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
- package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
- package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
- package/template/coding-task-manager/lib/perf-summary.js +7 -0
- package/template/coding-task-manager/lib/perf-tracker.js +63 -2
- package/template/coding-task-manager/lib/project-config.js +7 -0
- package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
- package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
- package/template/coding-task-manager/lib/read-pool-client.js +89 -2
- package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
- package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
- package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
- package/template/coding-task-manager/lib/relay-client.js +57 -3
- package/template/coding-task-manager/lib/relink-audit.js +34 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
- package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
- package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
- package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
- package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
- package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
- package/template/coding-task-manager/lib/restart-guard.js +18 -6
- package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
- package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
- package/template/coding-task-manager/lib/resume-admission.js +17 -3
- package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
- package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
- package/template/coding-task-manager/lib/runtime-contract.js +3 -1
- package/template/coding-task-manager/lib/schema-indexes.js +1 -0
- package/template/coding-task-manager/lib/session-context-locator.js +742 -0
- package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
- package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
- package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
- package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
- package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
- package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
- package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
- package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
- package/template/coding-task-manager/lib/session-create/response.js +7 -7
- package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
- package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
- package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
- package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
- package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
- package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
- package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
- package/template/coding-task-manager/lib/session-empty.js +13 -10
- package/template/coding-task-manager/lib/session-host-manager.js +60 -0
- package/template/coding-task-manager/lib/session-jobs.js +3 -1
- package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
- package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
- package/template/coding-task-manager/lib/session-park.js +738 -0
- package/template/coding-task-manager/lib/session-path-grant.js +13 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
- package/template/coding-task-manager/lib/session-transport.js +17 -0
- package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
- package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
- package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
- package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
- package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
- package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
- package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
- package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
- package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
- package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
- package/template/coding-task-manager/lib/submission-ack.js +90 -2
- package/template/coding-task-manager/lib/systemd-scope.js +115 -0
- package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
- package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
- package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
- package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
- package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
- package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
- package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
- package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
- package/template/coding-task-manager/lib/transcript-source.js +36 -4
- package/template/coding-task-manager/lib/transcript-store.js +9 -0
- package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
- package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
- package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
- package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
- package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
- package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
- package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
- package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
- package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
- package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
- package/template/coding-task-manager/lib/walle/turn.js +1 -1
- package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
- package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
- package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
- package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
- package/template/coding-task-manager/lib/walle-row-history.js +495 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
- package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
- package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
- package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
- package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
- package/template/coding-task-manager/lib/ws/misc.js +1 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
- package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
- package/template/coding-task-manager/oauth-proxy.js +112 -115
- package/template/coding-task-manager/package-lock.json +10 -94
- package/template/coding-task-manager/package.json +4 -3
- package/template/coding-task-manager/protocol/message-table.json +10 -0
- package/template/coding-task-manager/protocol/messages.js +10 -0
- package/template/coding-task-manager/protocol/ws-schema.json +20 -0
- package/template/coding-task-manager/protocol/ws-schema.md +2 -0
- package/template/coding-task-manager/public/css/app.css +170 -0
- package/template/coding-task-manager/public/css/walle-session.css +4 -0
- package/template/coding-task-manager/public/index.html +473 -243
- package/template/coding-task-manager/public/js/agent-command.js +86 -0
- package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
- package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
- package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
- package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
- package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
- package/template/coding-task-manager/public/js/background-work.js +196 -27
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
- package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
- package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
- package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
- package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
- package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
- package/template/coding-task-manager/public/js/document-review-links.js +9 -1
- package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
- package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
- package/template/coding-task-manager/public/js/message-renderer.js +4 -3
- package/template/coding-task-manager/public/js/model-picker.js +3 -1
- package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
- package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
- package/template/coding-task-manager/public/js/pasted-content.js +236 -0
- package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
- package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
- package/template/coding-task-manager/public/js/prompts.js +11 -10
- package/template/coding-task-manager/public/js/protocol.js +10 -0
- package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
- package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
- package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
- package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
- package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
- package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
- package/template/coding-task-manager/public/js/stream-view.js +6 -5
- package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
- package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
- package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
- package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
- package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
- package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
- package/template/coding-task-manager/public/js/walle.js +6 -0
- package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
- package/template/coding-task-manager/public/m/app.css +140 -0
- package/template/coding-task-manager/public/m/app.js +507 -68
- package/template/coding-task-manager/public/m/index.html +17 -0
- package/template/coding-task-manager/public/m/machines.js +838 -4
- package/template/coding-task-manager/public/m/private-api.js +31 -1
- package/template/coding-task-manager/public/usage.html +1 -0
- package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
- package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
- package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
- package/template/coding-task-manager/server.js +1950 -11679
- package/template/coding-task-manager/session-integrity.js +64 -4
- package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
- package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
- package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
- package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
- package/template/coding-task-manager/workers/session-host-process.js +5 -5
- package/template/coding-task-manager/workers/watch-worker.js +105 -0
- package/template/coding-task-manager/workers/worktree-worker.js +168 -33
- package/template/ctm-relay/lib/protocol.js +14 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/shared/backup-retention.js +82 -0
- package/template/shared/conversation-admission.js +2 -0
- package/template/shared/conversation-provenance.js +3 -7
- package/template/shared/conversation-storage-v2-contract.js +5 -0
- package/template/shared/linked-worktree.js +51 -0
- package/template/shared/macos-fda-target.js +57 -0
- package/template/shared/orphan-parents.js +55 -0
- package/template/shared/release-schema.json +2 -2
- package/template/shared/storage/contract.js +3 -0
- package/template/wall-e/adapters/imessage.js +12 -0
- package/template/wall-e/agent.js +71 -4
- package/template/wall-e/api-walle.js +21 -1
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
- package/template/wall-e/brain/brain-join.js +859 -0
- package/template/wall-e/brain/brain-sync-membership.js +20 -0
- package/template/wall-e/brain/brain-sync-store.js +131 -77
- package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
- package/template/wall-e/brain/brain-sync-v2.js +578 -0
- package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
- package/template/wall-e/brain/deterministic-id.js +51 -0
- package/template/wall-e/brain/memories.js +2 -2
- package/template/wall-e/brain/migrations.js +40 -6
- package/template/wall-e/brain/model-identity-migration.js +1 -1
- package/template/wall-e/brain/owner-call-whitelist.js +23 -7
- package/template/wall-e/brain/schema.js +45 -85
- package/template/wall-e/brain.js +171 -35
- package/template/wall-e/chat.js +3 -3
- package/template/wall-e/coding/agent-shell-env.js +7 -2
- package/template/wall-e/coding/prompt-bundle.js +66 -21
- package/template/wall-e/config/main-loop-exemptions.json +2 -2
- package/template/wall-e/http/admin-api.js +37 -0
- package/template/wall-e/http/gws-api.js +4 -0
- package/template/wall-e/http/helper-rules-api.js +7 -1
- package/template/wall-e/http/model-admin.js +8 -1
- package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
- package/template/wall-e/http/model-proxy-providers.js +174 -1
- package/template/wall-e/http/model-route-health.js +157 -0
- package/template/wall-e/http/sync-api.js +278 -464
- package/template/wall-e/jobs/job-runner.js +28 -1
- package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
- package/template/wall-e/lib/ctm-data-dir.js +26 -0
- package/template/wall-e/lib/derivation-home.js +147 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
- package/template/wall-e/lib/helper-rules/grade.js +51 -8
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
- package/template/wall-e/lib/helper-rules/store.js +4 -2
- package/template/wall-e/lib/landing-mcp-tools.js +170 -0
- package/template/wall-e/lib/mcp-client-config.js +133 -14
- package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
- package/template/wall-e/lib/mcp-integration.js +58 -13
- package/template/wall-e/lib/parent-death.js +7 -1
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
- package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
- package/template/wall-e/llm/delegate-vendor-default.js +12 -5
- package/template/wall-e/llm/model-roles.js +1 -1
- package/template/wall-e/llm/native-delegate-selection.js +56 -8
- package/template/wall-e/llm/worker-delegate-args.js +72 -7
- package/template/wall-e/llm/worker-mcp-routing.js +66 -17
- package/template/wall-e/loops/brain-custom-writes.js +19 -0
- package/template/wall-e/loops/ingest.js +17 -1
- package/template/wall-e/loops/think.js +19 -4
- package/template/wall-e/mcp-contract.js +222 -6
- package/template/wall-e/mcp-protocol-contract.js +40 -5
- package/template/wall-e/mcp-server.js +128 -29
- package/template/wall-e/mcp-worker-schemas.js +5 -5
- package/template/wall-e/memory/ctm-context-client.js +85 -0
- package/template/wall-e/memory/ctm-session-context.js +9 -3
- package/template/wall-e/memory/hub-bridge-client.js +388 -0
- package/template/wall-e/memory/replica-origin.js +56 -0
- package/template/wall-e/memory/session-pool.js +135 -0
- package/template/wall-e/runtime/live-capabilities.js +8 -3
- package/template/wall-e/server.js +4 -2
- package/template/wall-e/shared/release-schema.json +2 -2
- package/template/wall-e/shared/sqlite-file-set.js +39 -0
- package/template/wall-e/shared/storage/contract.js +3 -0
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
- package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
- package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
- package/template/wall-e/tools/chrome-automation.js +6 -0
- package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
- package/template/wall-e/workers/brain-owner-worker.js +18 -4
- package/template/wall-e/workers/mcp-gateway-process.js +5 -5
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WP-4 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-6/D-7/D-8, §6.4): the BOX side of "the
|
|
5
|
+
* session spawns with the hub's bundle".
|
|
6
|
+
*
|
|
7
|
+
* A hub-created session has a `sessions/<id>.json` record (WP-3) and the hub's bundle is materialized
|
|
8
|
+
* in the store (WP-2). A box with EXACTLY ONE hub pointer also applies that hub's bundle to a session
|
|
9
|
+
* with no record at all — the pre-fix hub-created sessions (and box-local creates on a one-hub box)
|
|
10
|
+
* cannot otherwise be told apart from a local create (rev 3 D-10). This module turns the store's
|
|
11
|
+
* resolution of that bundle into argv/env for ONE spawn. Nothing here is persisted: the `--add-dir`
|
|
12
|
+
* pair and CLAUDE_CODE_PLUGIN_DIRS live only in the argv/env handed to `_spawnSessionPty`, so a
|
|
13
|
+
* restore re-resolves the hub's CURRENT tree instead of replaying a stale path (D-7, AC-4c/AC-4d).
|
|
14
|
+
*
|
|
15
|
+
* Measured on the box (design §5 decision table, 2026-10-25):
|
|
16
|
+
* `claude --add-dir <treeDir>/claude` loads `<treeDir>/claude/.claude/skills/*` bare-named
|
|
17
|
+
* `CLAUDE_CODE_PLUGIN_DIRS=<treeDir>/plugins` loads every plugin folder under it (parent form)
|
|
18
|
+
* A missing `<treeDir>/plugins` is harmless to Claude (measured), so nothing here stats the
|
|
19
|
+
* filesystem — the transform stays PURE and unit-testable.
|
|
20
|
+
*
|
|
21
|
+
* D-8 (argv placement): `--add-dir` is VARIADIC — it swallows every following positional until the
|
|
22
|
+
* next option (tests/worker-launch-argv.test.js:206-208,231). Inserted before a positional prompt it
|
|
23
|
+
* would eat that prompt, so the pair goes immediately before the FIRST option token of the argv (and
|
|
24
|
+
* at the end when the argv has no option token at all).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
|
|
29
|
+
/** Everything from the first `-`-prefixed token on is "option territory" as far as D-8 is concerned. */
|
|
30
|
+
function _isOptionToken(value) {
|
|
31
|
+
return typeof value === 'string' && value.startsWith('-');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* D-8: `launchArgs` with `['--add-dir', dir]` inserted before its first option token (appended when
|
|
36
|
+
* there is none). Every original token keeps its relative order; a fresh array is always returned.
|
|
37
|
+
*
|
|
38
|
+
* @param {string[]} launchArgs
|
|
39
|
+
* @param {string} dir
|
|
40
|
+
* @returns {string[]}
|
|
41
|
+
*/
|
|
42
|
+
function insertAddDir(launchArgs, dir) {
|
|
43
|
+
const args = Array.isArray(launchArgs) ? launchArgs : [];
|
|
44
|
+
const tokens = ['--add-dir', dir];
|
|
45
|
+
const at = args.findIndex(_isOptionToken);
|
|
46
|
+
return at === -1 ? [...args, ...tokens] : [...args.slice(0, at), ...tokens, ...args.slice(at)];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* §6.4/D-8: the bundle's argv/env contribution for THIS spawn.
|
|
51
|
+
*
|
|
52
|
+
* PURE: the inputs are never mutated and the identity (`applied:false`, the caller's own objects) is
|
|
53
|
+
* returned for every case that is not a Claude spawn with a resolved bundle — a `null` bundle, and
|
|
54
|
+
* every non-Claude agent (Codex has no session-scoped skills lever; WP-6 installs instead, D-14).
|
|
55
|
+
*
|
|
56
|
+
* @param {{launchArgs?: string[], spawnEnv?: object, agentType?: string, bundle?: ?{treeDir: string}}} spawn
|
|
57
|
+
* @returns {{launchArgs: string[], spawnEnv: object, applied: boolean}}
|
|
58
|
+
*/
|
|
59
|
+
function applyAgentBundleToSpawn({ launchArgs, spawnEnv, agentType, bundle }) {
|
|
60
|
+
if (agentType !== 'claude') return { launchArgs, spawnEnv, applied: false };
|
|
61
|
+
const treeDir = bundle && typeof bundle === 'object' ? String(bundle.treeDir == null ? '' : bundle.treeDir) : '';
|
|
62
|
+
if (!treeDir) return { launchArgs, spawnEnv, applied: false };
|
|
63
|
+
const env = spawnEnv && typeof spawnEnv === 'object' ? spawnEnv : {};
|
|
64
|
+
const existing = String(env.CLAUDE_CODE_PLUGIN_DIRS == null ? '' : env.CLAUDE_CODE_PLUGIN_DIRS).trim();
|
|
65
|
+
const pluginDirs = path.join(treeDir, 'plugins');
|
|
66
|
+
return {
|
|
67
|
+
launchArgs: insertAddDir(launchArgs, path.join(treeDir, 'claude')),
|
|
68
|
+
// Ours FIRST: the hub's plugins win a name collision against anything the box already had (D-8).
|
|
69
|
+
spawnEnv: { ...env, CLAUDE_CODE_PLUGIN_DIRS: existing ? `${pluginDirs}:${existing}` : pluginDirs },
|
|
70
|
+
applied: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* D-6/D-7: the bundle for `sessionId`, or null. A bundle can never fail, delay or change a spawn.
|
|
76
|
+
*
|
|
77
|
+
* `options.report(event, details)` is the caller's diagnostic seam (wired to `recordSessionDiagnostic`
|
|
78
|
+
* at the spawn site) and is called with exactly one of:
|
|
79
|
+
* `agent-bundle-resolve-failed` the store could not answer (REPORT, then spawn without the overlay)
|
|
80
|
+
* `agent-bundle-missing` this session WAS created by a hub, but no tree resolves for it —
|
|
81
|
+
* deleted/GC'd, or the hub has never pushed (AC-4e)
|
|
82
|
+
* A session with no hub record at all reports nothing: there was never a bundle to miss. On a
|
|
83
|
+
* one-hub box (rev 3 D-10) such a session still RESOLVES — `store.resolveSpawnBundle` falls back to
|
|
84
|
+
* the sole hub — so this path only stays silent when the box has zero or several hub pointers. When a
|
|
85
|
+
* sole hub exists but its tree is gone, `resolveSpawnBundle` returns null and `sessionHubRecord` is
|
|
86
|
+
* null too, so nothing is reported (acceptable: with no record there is no hub to name).
|
|
87
|
+
*
|
|
88
|
+
* @param {string} sessionId
|
|
89
|
+
* @param {{store?: object, report?: (event: string, details: object) => void, dataDir?: string}} [options]
|
|
90
|
+
* @returns {Promise<null | {treeDir: string, treeHash: string, hubPeerId: string}>}
|
|
91
|
+
*/
|
|
92
|
+
async function resolveSpawnBundleForSession(sessionId, options = {}) {
|
|
93
|
+
const store = options.store || require('./store');
|
|
94
|
+
const report = typeof options.report === 'function' ? options.report : () => undefined;
|
|
95
|
+
// createProviderSession has no data dir in scope, so the canonical resolver is required lazily here
|
|
96
|
+
// rather than threaded through every caller.
|
|
97
|
+
const dataDir = options.dataDir || require('../data-dir').getDataDir();
|
|
98
|
+
let bundle = null;
|
|
99
|
+
try {
|
|
100
|
+
bundle = await store.resolveSpawnBundle(dataDir, sessionId);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// REPORT: an unreadable store is not a spawn failure — the session starts without the overlay.
|
|
103
|
+
report('agent-bundle-resolve-failed', { message: String((error && error.message) || error).slice(0, 200) });
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (bundle) return bundle;
|
|
107
|
+
let record = null;
|
|
108
|
+
try {
|
|
109
|
+
record = await store.sessionHubRecord(dataDir, sessionId);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
// REPORT: bundle bookkeeping never blocks a launch (D-6/D-7).
|
|
112
|
+
report('agent-bundle-resolve-failed', { message: String((error && error.message) || error).slice(0, 200) });
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (record) report('agent-bundle-missing', { hubPeerId: record.hubPeerId });
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { applyAgentBundleToSpawn, insertAddDir, resolveSpawnBundleForSession };
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WP-2 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-1/D-2/D-5, §6.3): the BOX side of the
|
|
5
|
+
* hub's agent-skill bundle — a content-addressed blob store plus a manifest materializer.
|
|
6
|
+
*
|
|
7
|
+
* Layout under `<dataDir>/agent-bundles/`:
|
|
8
|
+
* blobs/<hh>/<hex> content store, 0444, hh = first two hex chars
|
|
9
|
+
* trees/<treeHashHex>/{claude,plugins,codex}/… materialized read-only view, files 0444/0555,
|
|
10
|
+
* dirs 0555 (the entry paths already carry the
|
|
11
|
+
* `claude/`, `plugins/`, `codex/` prefixes)
|
|
12
|
+
* trees/<treeHashHex>.manifest.json WP-5: the manifest that tree was built from, the
|
|
13
|
+
* GC's blob-reference source (never a tree itself)
|
|
14
|
+
* hubs/<peerInstanceId>.json { treeHash, at } — the hub's CURRENT pointer
|
|
15
|
+
* sessions/<ctmSessionId>.json { hubPeerId, createdTreeHash, at }
|
|
16
|
+
*
|
|
17
|
+
* D-1: nothing is ever written into the box user's `~/.claude`; a bundle is a session overlay that
|
|
18
|
+
* lives only here. D-2: a bundle is addressed by `treeHash = computeTreeHash(entries)` (WP-1's
|
|
19
|
+
* builder), so identical content yields an identical directory on every hub. D-5: the box verifies
|
|
20
|
+
* what it is given — every blob is re-hashed on receipt, the manifest is accepted only when the
|
|
21
|
+
* recomputed tree hash matches the URL and every referenced blob is present at the declared size,
|
|
22
|
+
* and materialization is temp-dir + rename so a partially written tree is never visible.
|
|
23
|
+
*
|
|
24
|
+
* Identity rules (the reason `AgentBundleError` exists): `hex` must be 64 lowercase hex chars and
|
|
25
|
+
* `treeHash` the `sha256:<64 hex>` form (URL-encoded or raw — the URL carries it), so neither can
|
|
26
|
+
* ever name a file outside this store. `peerInstanceId` / `ctmSessionId` become file names and are
|
|
27
|
+
* held to `/^[A-Za-z0-9._-]{1,128}$/`.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const crypto = require('crypto');
|
|
31
|
+
const fsp = require('fs/promises');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
|
|
34
|
+
const { computeTreeHash } = require('./build');
|
|
35
|
+
const { atomicWriteFile } = require('../../atomic-write');
|
|
36
|
+
|
|
37
|
+
const HEX_RE = /^[0-9a-f]{64}$/;
|
|
38
|
+
const TREE_HASH_RE = /^sha256:([0-9a-f]{64})$/;
|
|
39
|
+
/** The id shape shared by peerInstanceId / ctmSessionId — they are file names, nothing more. */
|
|
40
|
+
const ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
41
|
+
/** = build.js's MAX_ENTRIES: the hub cannot produce more, so the box will not accept more. */
|
|
42
|
+
const MAX_ENTRIES = 20000;
|
|
43
|
+
/** D-5: files are one of exactly two modes; dirs are always 0555. */
|
|
44
|
+
const FILE_MODES = new Set([0o444, 0o555]);
|
|
45
|
+
const DIR_MODE = 0o555;
|
|
46
|
+
/** D-5 / WP-1: every entry path starts with one of these three roots. */
|
|
47
|
+
const PATH_PREFIXES = ['claude/.claude/skills/', 'plugins/', 'codex/'];
|
|
48
|
+
|
|
49
|
+
class AgentBundleError extends Error {
|
|
50
|
+
constructor(code, status, details = {}) {
|
|
51
|
+
super(code);
|
|
52
|
+
this.name = 'AgentBundleError';
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.status = status;
|
|
55
|
+
if (Array.isArray(details.missing)) this.missing = details.missing;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function storeRoot(dataDir) {
|
|
60
|
+
return path.join(String(dataDir == null ? '' : dataDir), 'agent-bundles');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _invalid() {
|
|
64
|
+
return new AgentBundleError('agent_bundle_path_invalid', 422);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A blob hex, or `agent_bundle_path_invalid`. Never normalizes case: the name IS the hash. */
|
|
68
|
+
function _requireHex(value) {
|
|
69
|
+
const hex = String(value == null ? '' : value).trim();
|
|
70
|
+
if (!HEX_RE.test(hex)) throw _invalid();
|
|
71
|
+
return hex;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `sha256:<64 hex>`, accepted URL-encoded (`sha256%3A…`) or raw, or `agent_bundle_path_invalid`. */
|
|
75
|
+
function _requireTreeHash(value) {
|
|
76
|
+
const raw = String(value == null ? '' : value).trim();
|
|
77
|
+
let decoded = raw;
|
|
78
|
+
if (raw.includes('%')) {
|
|
79
|
+
try {
|
|
80
|
+
decoded = decodeURIComponent(raw);
|
|
81
|
+
} catch {
|
|
82
|
+
// expected: a malformed percent-escape is just an invalid tree hash, not a server fault
|
|
83
|
+
throw _invalid();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const match = TREE_HASH_RE.exec(decoded);
|
|
87
|
+
if (!match) throw _invalid();
|
|
88
|
+
return { treeHash: `sha256:${match[1]}`, hex: match[1] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _requireId(value) {
|
|
92
|
+
const id = String(value == null ? '' : value).trim();
|
|
93
|
+
if (!ID_RE.test(id)) throw _invalid();
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _blobPath(root, hex) {
|
|
98
|
+
return path.join(root, 'blobs', hex.slice(0, 2), hex);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function _statOrNull(target) {
|
|
102
|
+
try {
|
|
103
|
+
return await fsp.stat(target);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function _exists(target) {
|
|
111
|
+
return (await _statOrNull(target)) !== null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function _unlinkQuiet(target) {
|
|
115
|
+
try {
|
|
116
|
+
await fsp.unlink(target);
|
|
117
|
+
} catch {
|
|
118
|
+
/* expected: the temp file was never created, or the rename already consumed it */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function _readJsonOrNull(target) {
|
|
123
|
+
let text;
|
|
124
|
+
try {
|
|
125
|
+
text = await fsp.readFile(target, 'utf8');
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(text);
|
|
132
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
133
|
+
} catch {
|
|
134
|
+
// expected: a torn/garbled pointer is a missing piece of information, not a reason to fail the
|
|
135
|
+
// spawn — D-7 keeps bundle work unable to block or fail a session.
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** D-5: a relative POSIX path under one of the three bundle roots, with no way to escape the tree. */
|
|
141
|
+
function _validateEntryPath(value) {
|
|
142
|
+
if (typeof value !== 'string' || value.length === 0) throw _invalid();
|
|
143
|
+
if (value.startsWith('/')) throw _invalid();
|
|
144
|
+
if (value.includes('\\')) throw _invalid();
|
|
145
|
+
if (/[\u0000-\u001f]/.test(value)) throw _invalid();
|
|
146
|
+
const segments = value.split('/');
|
|
147
|
+
for (const segment of segments) {
|
|
148
|
+
if (segment === '' || segment === '.' || segment === '..') throw _invalid();
|
|
149
|
+
}
|
|
150
|
+
if (!PATH_PREFIXES.some((prefix) => value.startsWith(prefix))) throw _invalid();
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _normalizeEntries(manifest, canonicalTreeHash) {
|
|
155
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) throw _invalid();
|
|
156
|
+
if (manifest.schema !== 1) throw _invalid();
|
|
157
|
+
if (!Array.isArray(manifest.entries)) throw _invalid();
|
|
158
|
+
if (manifest.entries.length > MAX_ENTRIES) throw _invalid();
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
const entries = [];
|
|
161
|
+
for (const raw of manifest.entries) {
|
|
162
|
+
if (!raw || typeof raw !== 'object') throw _invalid();
|
|
163
|
+
const rel = _validateEntryPath(raw.path);
|
|
164
|
+
if (seen.has(rel)) throw _invalid();
|
|
165
|
+
seen.add(rel);
|
|
166
|
+
if (!FILE_MODES.has(raw.mode)) throw _invalid();
|
|
167
|
+
if (!Number.isInteger(raw.size) || raw.size < 0) throw _invalid();
|
|
168
|
+
const sha256 = String(raw.sha256 == null ? '' : raw.sha256);
|
|
169
|
+
if (!HEX_RE.test(sha256)) throw _invalid();
|
|
170
|
+
entries.push({ path: rel, mode: raw.mode, sha256, size: raw.size });
|
|
171
|
+
}
|
|
172
|
+
if (computeTreeHash(entries) !== canonicalTreeHash) {
|
|
173
|
+
throw new AgentBundleError('agent_bundle_tree_hash_mismatch', 422);
|
|
174
|
+
}
|
|
175
|
+
return entries;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Every directory in a freshly written tree, the root included (breadth-first, unordered). */
|
|
179
|
+
async function _collectDirs(rootDir) {
|
|
180
|
+
const dirs = [rootDir];
|
|
181
|
+
for (let index = 0; index < dirs.length; index += 1) {
|
|
182
|
+
const dirents = await fsp.readdir(dirs[index], { withFileTypes: true });
|
|
183
|
+
for (const dirent of dirents) {
|
|
184
|
+
if (dirent.isDirectory()) dirs.push(path.join(dirs[index], dirent.name));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return dirs;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Recursive remove that first drops read-only dirs back to writable (trees are 0555). */
|
|
191
|
+
async function _removeTree(target) {
|
|
192
|
+
const stack = [target];
|
|
193
|
+
while (stack.length) {
|
|
194
|
+
const dir = stack.pop();
|
|
195
|
+
let dirents;
|
|
196
|
+
try {
|
|
197
|
+
dirents = await fsp.readdir(dir, { withFileTypes: true });
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error && error.code === 'ENOENT') continue;
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
await fsp.chmod(dir, 0o700);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
206
|
+
/* expected: the dir vanished between readdir and chmod — the removal below is already done */
|
|
207
|
+
}
|
|
208
|
+
for (const dirent of dirents) {
|
|
209
|
+
if (dirent.isDirectory()) stack.push(path.join(dir, dirent.name));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
await fsp.rm(target, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* §6.3: build `trees/<hex>` from the blobs. Copies (not hardlinks) so the 0444 inode is never a
|
|
217
|
+
* shared, mutable-looking object. The lock order is everything BELOW the temp root (0555,
|
|
218
|
+
* deepest-first) → `rename(tmp, treeDir)` → the published root itself (0555); see
|
|
219
|
+
* docs/agent-bundle-darwin-readonly-rename-2026-10-27.html D-1: Darwin's rename(2) needs write on
|
|
220
|
+
* `old/..`, so a 0555 temp ROOT makes the rename itself fail with EACCES on macOS ≤15, while the
|
|
221
|
+
* root stays owner-writable across the rename and is locked only once it is in its final place.
|
|
222
|
+
* A rename race (another request materialized the same tree first) is not an error: `materialized:false`.
|
|
223
|
+
*/
|
|
224
|
+
async function _materialize(root, hex, treeDir, entries) {
|
|
225
|
+
const tmp = path.join(root, 'trees', `.tmp-${hex}-${crypto.randomBytes(6).toString('hex')}`);
|
|
226
|
+
try {
|
|
227
|
+
await fsp.mkdir(tmp, { recursive: true });
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const target = path.join(tmp, ...entry.path.split('/'));
|
|
230
|
+
await fsp.mkdir(path.dirname(target), { recursive: true });
|
|
231
|
+
await fsp.copyFile(_blobPath(root, entry.sha256), target);
|
|
232
|
+
await fsp.chmod(target, entry.mode);
|
|
233
|
+
}
|
|
234
|
+
// D-5: EVERY directory in the tree is 0555, not just the entry folders — mkdir's defaults are
|
|
235
|
+
// whatever the umask says, and a writable intermediate would let a later spawn write into the
|
|
236
|
+
// store. Deepest-first so a parent is never locked while a child still needs creating.
|
|
237
|
+
// The temp ROOT is excluded (D-1, docs/agent-bundle-darwin-readonly-rename-2026-10-27.html): the
|
|
238
|
+
// rename below needs write on `old/..`, so the root is locked only after it is published.
|
|
239
|
+
const dirs = (await _collectDirs(tmp)).filter((dir) => dir !== tmp);
|
|
240
|
+
dirs.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
|
|
241
|
+
for (const dir of dirs) await fsp.chmod(dir, DIR_MODE);
|
|
242
|
+
await fsp.rename(tmp, treeDir); // D-1: the root is still owner-writable here
|
|
243
|
+
await fsp.chmod(treeDir, DIR_MODE); // D-1: a throw here propagates; D-3 re-locks on the next PUT
|
|
244
|
+
return true;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
await _removeTree(tmp);
|
|
247
|
+
if (error && (error.code === 'EEXIST' || error.code === 'ENOTEMPTY')) return false;
|
|
248
|
+
throw error; // RETHROW: a failed materialization must not be reported as "already present"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function _writeHubPointer(root, peerInstanceId, treeHash) {
|
|
253
|
+
const dir = path.join(root, 'hubs');
|
|
254
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
255
|
+
await atomicWriteFile(
|
|
256
|
+
path.join(dir, `${peerInstanceId}.json`),
|
|
257
|
+
JSON.stringify({ treeHash, at: Date.now() }),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** §6.3: the hexes from `hashes` that this box does not hold, in input order, deduped. */
|
|
262
|
+
async function missingBlobs(dataDir, hashes) {
|
|
263
|
+
const root = storeRoot(dataDir);
|
|
264
|
+
const unique = [];
|
|
265
|
+
const seen = new Set();
|
|
266
|
+
for (const value of Array.isArray(hashes) ? hashes : []) {
|
|
267
|
+
const hex = _requireHex(value);
|
|
268
|
+
if (seen.has(hex)) continue;
|
|
269
|
+
seen.add(hex);
|
|
270
|
+
unique.push(hex);
|
|
271
|
+
}
|
|
272
|
+
const missing = [];
|
|
273
|
+
for (const hex of unique) {
|
|
274
|
+
if (!(await _exists(_blobPath(root, hex)))) missing.push(hex);
|
|
275
|
+
}
|
|
276
|
+
return missing;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// gc.js's blob sweep rmdirs every shard it leaves empty, and it runs unawaited after a materializing
|
|
280
|
+
// manifest PUT -- so a shard this write just created can vanish before the temp file is opened in it.
|
|
281
|
+
// The writer owns recreating its directory (git's object writer does the same): mkdir and retry the
|
|
282
|
+
// open on ENOENT. The recursive mkdir itself can throw ENOENT too -- it answers an existing shard with
|
|
283
|
+
// EEXIST and then stats it, and the sweep can remove it in between. Once the temp file exists the
|
|
284
|
+
// shard is non-empty and the sweep's rmdir cannot win.
|
|
285
|
+
const SHARD_OPEN_ATTEMPTS = 3;
|
|
286
|
+
|
|
287
|
+
async function _openInShard(dir, tmp) {
|
|
288
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
289
|
+
try {
|
|
290
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
291
|
+
return await fsp.open(tmp, 'w', 0o600);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!error || error.code !== 'ENOENT' || attempt >= SHARD_OPEN_ATTEMPTS) throw error; // RETHROW
|
|
294
|
+
/* expected: a GC sweep removed the empty shard during mkdir or before open -- recreate it */
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* §6.2/AC-2a: store one blob. The hash is recomputed FIRST — a wrong-hash body stores nothing — and
|
|
301
|
+
* an existing blob is left untouched (`{ stored: false }`), which is what makes a re-push cheap.
|
|
302
|
+
*/
|
|
303
|
+
async function putBlob(dataDir, hex, buffer) {
|
|
304
|
+
const clean = _requireHex(hex);
|
|
305
|
+
const data = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer == null ? '' : buffer);
|
|
306
|
+
if (crypto.createHash('sha256').update(data).digest('hex') !== clean) {
|
|
307
|
+
throw new AgentBundleError('agent_bundle_blob_hash_mismatch', 422);
|
|
308
|
+
}
|
|
309
|
+
const root = storeRoot(dataDir);
|
|
310
|
+
const target = _blobPath(root, clean);
|
|
311
|
+
if (await _exists(target)) return { stored: false };
|
|
312
|
+
const dir = path.dirname(target);
|
|
313
|
+
const tmp = path.join(dir, `.${clean}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
|
|
314
|
+
try {
|
|
315
|
+
const handle = await _openInShard(dir, tmp);
|
|
316
|
+
try {
|
|
317
|
+
await handle.writeFile(data);
|
|
318
|
+
await handle.sync();
|
|
319
|
+
} finally {
|
|
320
|
+
await handle.close();
|
|
321
|
+
}
|
|
322
|
+
await fsp.chmod(tmp, 0o444);
|
|
323
|
+
await fsp.rename(tmp, target);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
await _unlinkQuiet(tmp);
|
|
326
|
+
throw error; // RETHROW: a failed write must not look like a stored blob
|
|
327
|
+
}
|
|
328
|
+
return { stored: true };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* §6.2/D-5: accept a manifest and materialize its tree, then move this hub's pointer.
|
|
333
|
+
*
|
|
334
|
+
* The peer identity is required BEFORE any write: a box keys bundles by the AUTHENTICATED hub
|
|
335
|
+
* (`req.ctmAuth.peerInstanceId`), never by a field in the body, so an unidentified caller cannot
|
|
336
|
+
* create state on this machine.
|
|
337
|
+
*
|
|
338
|
+
* @returns {Promise<{treeHash: string, materialized: boolean}>}
|
|
339
|
+
*/
|
|
340
|
+
async function putManifest(dataDir, treeHash, manifest, options = {}) {
|
|
341
|
+
const peerRaw = String(options.peerInstanceId == null ? '' : options.peerInstanceId).trim();
|
|
342
|
+
if (!peerRaw) throw new AgentBundleError('agent_bundle_peer_unidentified', 403);
|
|
343
|
+
const peerInstanceId = _requireId(peerRaw);
|
|
344
|
+
const canonical = _requireTreeHash(treeHash);
|
|
345
|
+
const root = storeRoot(dataDir);
|
|
346
|
+
const entries = _normalizeEntries(manifest, canonical.treeHash);
|
|
347
|
+
|
|
348
|
+
const missing = new Set();
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
if (!(await _exists(_blobPath(root, entry.sha256)))) missing.add(entry.sha256);
|
|
351
|
+
}
|
|
352
|
+
if (missing.size) {
|
|
353
|
+
throw new AgentBundleError('agent_bundle_blobs_missing', 409, { missing: [...missing].sort() });
|
|
354
|
+
}
|
|
355
|
+
// D-5: presence is not enough — the bytes on disk must be the size the manifest declares.
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
const stat = await _statOrNull(_blobPath(root, entry.sha256));
|
|
358
|
+
if (stat && stat.size !== entry.size) {
|
|
359
|
+
throw new AgentBundleError('agent_bundle_tree_hash_mismatch', 422);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await fsp.mkdir(path.join(root, 'trees'), { recursive: true });
|
|
364
|
+
const treeDir = path.join(root, 'trees', canonical.hex);
|
|
365
|
+
const existing = await _statOrNull(treeDir);
|
|
366
|
+
const materialized = existing && existing.isDirectory()
|
|
367
|
+
// D-3 (docs/agent-bundle-darwin-readonly-rename-2026-10-27.html): re-lock a root a crash between
|
|
368
|
+
// D-1's rename and its chmod left writable — idempotent, and a throw here is a real 500.
|
|
369
|
+
? (await fsp.chmod(treeDir, DIR_MODE), false)
|
|
370
|
+
: await _materialize(root, canonical.hex, treeDir, entries);
|
|
371
|
+
|
|
372
|
+
// WP-5: GC needs to know which blobs a tree still needs, so the manifest is copied next to the
|
|
373
|
+
// tree it names (the tree directory is the durable truth; nothing else remembers it). Written
|
|
374
|
+
// when the tree is created AND when it already exists without a copy — a tree materialized before
|
|
375
|
+
// this WP picks one up on the hub's next push of the same content.
|
|
376
|
+
const manifestCopy = path.join(root, 'trees', `${canonical.hex}.manifest.json`);
|
|
377
|
+
if (!(await _exists(manifestCopy))) {
|
|
378
|
+
await atomicWriteFile(manifestCopy, JSON.stringify({ schema: 1, treeHash: canonical.treeHash, entries }));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// AC-2e: the pointer moves even when the tree was already there — it names the hub's CURRENT
|
|
382
|
+
// bundle, not the one that happened to be materialized first.
|
|
383
|
+
await _writeHubPointer(root, peerInstanceId, canonical.treeHash);
|
|
384
|
+
return { treeHash: canonical.treeHash, materialized };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** The hub pointer file this store keys by peer id — one read site for both callers below. */
|
|
388
|
+
function _hubPointerPath(root, peerId) {
|
|
389
|
+
return path.join(root, 'hubs', `${peerId}.json`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* WP-3: the hub's CURRENT tree pointer for `peerInstanceId`, or null when that hub never pushed
|
|
394
|
+
* (or its pointer is unreadable). Used at create-record time so `sessions/<id>.json` carries the
|
|
395
|
+
* tree that was current then; `resolveSpawnBundle` re-reads the pointer at spawn regardless, so a
|
|
396
|
+
* null here costs nothing.
|
|
397
|
+
*
|
|
398
|
+
* @returns {Promise<string|null>} `sha256:<64 hex>` or null
|
|
399
|
+
*/
|
|
400
|
+
async function currentHubTree(dataDir, peerInstanceId) {
|
|
401
|
+
const peer = String(peerInstanceId == null ? '' : peerInstanceId).trim();
|
|
402
|
+
if (!ID_RE.test(peer)) return null;
|
|
403
|
+
const pointer = await _readJsonOrNull(_hubPointerPath(storeRoot(dataDir), peer));
|
|
404
|
+
if (!pointer) return null;
|
|
405
|
+
try {
|
|
406
|
+
return _requireTreeHash(pointer.treeHash).treeHash;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error instanceof AgentBundleError) return null;
|
|
409
|
+
throw error; // RETHROW: an unexpected read fault must not be reported as "no pointer"
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* §6.3: remember which hub started this session, and the tree hash as of that create.
|
|
415
|
+
*
|
|
416
|
+
* WP-3: `treeHash` may be NULL — that is the honest record for a hub that had not pushed yet at
|
|
417
|
+
* create time (D-7 reads the hub's CURRENT pointer at spawn, so the create-time value is
|
|
418
|
+
* bookkeeping). A non-null value is still validated into the canonical `sha256:<hex>` form.
|
|
419
|
+
*/
|
|
420
|
+
async function recordSessionHub(dataDir, ctmSessionId, hubPeerId, treeHash) {
|
|
421
|
+
const sessionId = _requireId(ctmSessionId);
|
|
422
|
+
const hubPeerIdClean = _requireId(hubPeerId);
|
|
423
|
+
const canonical = treeHash === null || treeHash === undefined || String(treeHash).trim() === ''
|
|
424
|
+
? null
|
|
425
|
+
: _requireTreeHash(treeHash).treeHash;
|
|
426
|
+
const dir = path.join(storeRoot(dataDir), 'sessions');
|
|
427
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
428
|
+
await atomicWriteFile(
|
|
429
|
+
path.join(dir, `${sessionId}.json`),
|
|
430
|
+
JSON.stringify({ hubPeerId: hubPeerIdClean, createdTreeHash: canonical, at: Date.now() }),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The pointer -> materialized-tree tail shared by both `resolveSpawnBundle` paths: read
|
|
436
|
+
* `hubs/<hubPeerId>.json`, canonicalize its tree hash, stat `trees/<hex>`. Returns null for every
|
|
437
|
+
* missing/unusable piece (no pointer, a torn pointer, a malformed hash, no tree directory) and
|
|
438
|
+
* rethrows an unexpected read fault. Factored out so the record path and the sole-hub path cannot
|
|
439
|
+
* drift apart (WP-5 step 4).
|
|
440
|
+
*
|
|
441
|
+
* @returns {Promise<null | {treeDir: string, treeHash: string, hubPeerId: string}>}
|
|
442
|
+
*/
|
|
443
|
+
async function _bundleForHub(root, hubPeerId) {
|
|
444
|
+
const hub = await _readJsonOrNull(_hubPointerPath(root, hubPeerId));
|
|
445
|
+
if (!hub) return null;
|
|
446
|
+
let hex;
|
|
447
|
+
try {
|
|
448
|
+
hex = _requireTreeHash(hub.treeHash).hex;
|
|
449
|
+
} catch (error) {
|
|
450
|
+
if (error instanceof AgentBundleError) return null;
|
|
451
|
+
throw error; // RETHROW: an unexpected read fault must not be reported as "no bundle"
|
|
452
|
+
}
|
|
453
|
+
const treeDir = path.join(root, 'trees', hex);
|
|
454
|
+
const stat = await _statOrNull(treeDir);
|
|
455
|
+
if (!stat || !stat.isDirectory()) return null;
|
|
456
|
+
return { treeDir, treeHash: `sha256:${hex}`, hubPeerId };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* rev 3 D-10: the peer id of the box's ONLY hub, or null. A box with exactly one `hubs/<peer>.json`
|
|
461
|
+
* pointer has exactly one hub, so a session with no `sessions/<id>.json` record (created before the
|
|
462
|
+
* hub sent its peer id, or by a box-local path) is attributed to that hub. Several pointers (or none)
|
|
463
|
+
* stay null — the box cannot guess.
|
|
464
|
+
*
|
|
465
|
+
* Only `<peer>.json` names whose basename is a valid id count; temp files (`hubs/.tmp-*`) and anything
|
|
466
|
+
* else are ignored. ENOENT (no `hubs/` dir at all) is the honest "no hub" -> null; any other read
|
|
467
|
+
* fault is RETHROWN for the caller to REPORT (spawn.js turns it into `agent-bundle-resolve-failed`).
|
|
468
|
+
*
|
|
469
|
+
* @returns {Promise<string|null>}
|
|
470
|
+
*/
|
|
471
|
+
async function _soleHubPeerId(root) {
|
|
472
|
+
let names;
|
|
473
|
+
try {
|
|
474
|
+
names = await fsp.readdir(path.join(root, 'hubs'));
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null;
|
|
477
|
+
throw error; // RETHROW: a store we cannot read is REPORTED, never silently "no hub"
|
|
478
|
+
}
|
|
479
|
+
const peers = [];
|
|
480
|
+
for (const name of names) {
|
|
481
|
+
if (!name.endsWith('.json')) continue;
|
|
482
|
+
const peer = name.slice(0, -'.json'.length);
|
|
483
|
+
if (!ID_RE.test(peer)) continue;
|
|
484
|
+
peers.push(peer);
|
|
485
|
+
}
|
|
486
|
+
return peers.length === 1 ? peers[0] : null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* §6.3: the bundle this session should spawn with, read through the hub's CURRENT pointer so a skill
|
|
491
|
+
* edit on the hub applies at the next spawn (D-7's "session overlay", not a snapshot).
|
|
492
|
+
*
|
|
493
|
+
* rev 3 D-10: a `sessions/<id>.json` record still wins when present — resolution follows THAT record's
|
|
494
|
+
* hub pointer only, with no fallback even when the pointer is gone or its tree was GC'd. With no
|
|
495
|
+
* record, the box applies its single hub's bundle when it has exactly one hub pointer
|
|
496
|
+
* (`_soleHubPeerId`); zero or several pointers stay null, so the pre-fix hub-created sessions on a
|
|
497
|
+
* one-hub box (`hubs/ctm-…-3456-1a2c9c.json`) get the hub's skills where nothing else could tell them
|
|
498
|
+
* apart from box-local creates (docs/remote-skills-hub-peer-identity-2026-10-26.html rev 3 D-10).
|
|
499
|
+
*
|
|
500
|
+
* @returns {Promise<null | {treeDir: string, treeHash: string, hubPeerId: string}>}
|
|
501
|
+
*/
|
|
502
|
+
async function resolveSpawnBundle(dataDir, ctmSessionId) {
|
|
503
|
+
const sessionId = String(ctmSessionId == null ? '' : ctmSessionId).trim();
|
|
504
|
+
if (!ID_RE.test(sessionId)) return null;
|
|
505
|
+
const root = storeRoot(dataDir);
|
|
506
|
+
const session = await _readJsonOrNull(path.join(root, 'sessions', `${sessionId}.json`));
|
|
507
|
+
if (session) {
|
|
508
|
+
const hubPeerId = String(session.hubPeerId == null ? '' : session.hubPeerId).trim();
|
|
509
|
+
if (!ID_RE.test(hubPeerId)) return null;
|
|
510
|
+
// The record wins: no fallback to a sole hub, so 'sess-orphan' / 'sess-gc' keep resolving to null.
|
|
511
|
+
return _bundleForHub(root, hubPeerId);
|
|
512
|
+
}
|
|
513
|
+
// No record (D-10): the box's only hub, if it has exactly one.
|
|
514
|
+
const sole = await _soleHubPeerId(root);
|
|
515
|
+
if (!sole) return null;
|
|
516
|
+
return _bundleForHub(root, sole);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* WP-4 (§6.4, AC-4e): the `sessions/<id>.json` record itself — "was this session created by a hub at
|
|
521
|
+
* all, and which one" — without following the hub's pointer.
|
|
522
|
+
*
|
|
523
|
+
* `resolveSpawnBundle` answers "what should this spawn load" (null covers both "no hub" and "no
|
|
524
|
+
* tree"); this answers "was there ever a hub", which is what tells a tree that vanished (report
|
|
525
|
+
* `agent-bundle-missing`) from a local create (report nothing).
|
|
526
|
+
*
|
|
527
|
+
* ENOENT → null (the honest "no record"); any other read fault is RETHROWN for the caller to REPORT —
|
|
528
|
+
* a bundle read never decides whether a session spawns (D-6/D-7).
|
|
529
|
+
*
|
|
530
|
+
* @returns {Promise<null | {hubPeerId: string, createdTreeHash: string|null}>}
|
|
531
|
+
*/
|
|
532
|
+
async function sessionHubRecord(dataDir, ctmSessionId) {
|
|
533
|
+
const sessionId = String(ctmSessionId == null ? '' : ctmSessionId).trim();
|
|
534
|
+
if (!ID_RE.test(sessionId)) return null;
|
|
535
|
+
const record = await _readJsonOrNull(path.join(storeRoot(dataDir), 'sessions', `${sessionId}.json`));
|
|
536
|
+
if (!record) return null;
|
|
537
|
+
const hubPeerId = String(record.hubPeerId == null ? '' : record.hubPeerId).trim();
|
|
538
|
+
if (!ID_RE.test(hubPeerId)) return null;
|
|
539
|
+
let createdTreeHash = null;
|
|
540
|
+
if (record.createdTreeHash !== null && record.createdTreeHash !== undefined) {
|
|
541
|
+
try {
|
|
542
|
+
createdTreeHash = _requireTreeHash(record.createdTreeHash).treeHash;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (!(error instanceof AgentBundleError)) throw error; // RETHROW: a real read fault is not "no tree"
|
|
545
|
+
createdTreeHash = null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return { hubPeerId, createdTreeHash };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
module.exports = {
|
|
552
|
+
storeRoot,
|
|
553
|
+
missingBlobs,
|
|
554
|
+
putBlob,
|
|
555
|
+
putManifest,
|
|
556
|
+
recordSessionHub,
|
|
557
|
+
// WP-3: the hub's current pointer, read at create-record time (a null record is fine — the
|
|
558
|
+
// spawn-time resolver re-reads the pointer itself).
|
|
559
|
+
currentHubTree,
|
|
560
|
+
resolveSpawnBundle,
|
|
561
|
+
// WP-4: the raw session record, for the "hub-created but no tree" diagnostic (AC-4e).
|
|
562
|
+
sessionHubRecord,
|
|
563
|
+
// WP-5: the read-only-tree remover, exported so lib/agent-bundle/gc.js removes a tree the same
|
|
564
|
+
// way materialization's own failure path does (chmod 0700 down the tree, then rm -r) instead of
|
|
565
|
+
// growing a second copy of that chmod dance.
|
|
566
|
+
removeTree: _removeTree,
|
|
567
|
+
AgentBundleError,
|
|
568
|
+
};
|