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,724 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WP-8 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-11/D-12/D-13, §6.2, §6.5, the WP-8 row
|
|
5
|
+
* AC-8a..AC-8d, P-5 "never overwrite the other side silently"): the ONE-SKILL transfers, and the
|
|
6
|
+
* per-machine plugin exclusion list.
|
|
7
|
+
*
|
|
8
|
+
* WP-7 answers "what does the box have that the hub does not?" and classifies it; this module is what
|
|
9
|
+
* happens when a human acts on that answer. Two directions, one file, because they are one exchange:
|
|
10
|
+
*
|
|
11
|
+
* BOX `exportBoxSkill({ homeDir, env, agent, name })` the bytes of ONE item folder, hashed with
|
|
12
|
+
* build.js's own walk so the hash IS a hub item hash. `retireBoxSkill({ …, expectHash })`
|
|
13
|
+
* MOVES that folder under `<data dir>/agent-bundles/retired/<agent>/<name>-<ts>/` — D-13's
|
|
14
|
+
* "moves, never deletes" — after re-hashing it and refusing a copy that changed since review.
|
|
15
|
+
* HUB `importBoxSkillToHub({ … })` fetches that export, verifies every byte, and writes the folder
|
|
16
|
+
* into the hub's OWN skill root via stage-inside-root + rename (D-12), backing up any copy that
|
|
17
|
+
* was already there FIRST (P-5), then asks the box to retire its shadowing copy.
|
|
18
|
+
*
|
|
19
|
+
* The two rules that shape every line here:
|
|
20
|
+
* P-5 the hub NEVER writes over a folder it did not just verify a backup of, and the box NEVER
|
|
21
|
+
* deletes. A failure anywhere leaves both sides exactly as they were.
|
|
22
|
+
* Identity is a NAME, held to the store's own id shape. A name that could escape a root (a slash, a
|
|
23
|
+
* `..`, a NUL) is refused before a path is ever built from it, on both sides.
|
|
24
|
+
*
|
|
25
|
+
* Everything filesystem-touching takes an injectable `homeDir` / `env` / `dataDir` / `now`, so a test
|
|
26
|
+
* never reads this machine's real `~/.claude`, `~/.agents` or CTM data dir, and never sleeps.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const crypto = require('crypto');
|
|
30
|
+
const fsp = require('fs/promises');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
|
|
34
|
+
const { atomicWriteFile } = require('../../atomic-write');
|
|
35
|
+
const {
|
|
36
|
+
computeTreeHash, itemHashOfDir, walkItem, listClaudeSkillItems, listCodexSkillItems,
|
|
37
|
+
} = require('./build');
|
|
38
|
+
const inventory = require('./inventory');
|
|
39
|
+
const store = require('./store');
|
|
40
|
+
const codexInstall = require('./codex-install');
|
|
41
|
+
const installer = require('../coding-cli-skill-installer');
|
|
42
|
+
|
|
43
|
+
const { MANAGED_SKILLS } = installer;
|
|
44
|
+
const { readMarker } = installer._internals;
|
|
45
|
+
|
|
46
|
+
/** store.js's ID_RE — a name that is also a directory name, and nothing else. */
|
|
47
|
+
const NAME_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
48
|
+
// WP-12 (AC-12c): `dsh` (the DeepSeek Harness) is a transfer target like Claude/Codex; its hub root is
|
|
49
|
+
// $DSH_HOME/skills (default ~/.dsh/skills) and its box roots are $DSH_HOME/skills plus ~/.agents/skills.
|
|
50
|
+
const AGENTS = Object.freeze(['claude', 'codex', 'dsh']);
|
|
51
|
+
/** §6.2: the whole export must fit in one response body. */
|
|
52
|
+
const MAX_EXPORT_BYTES = 25 * 1024 * 1024;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* WP-9 (§9 S-9, AC-9b): the DIFF SOURCE. A browser cannot call the box's own
|
|
56
|
+
* `GET /api/agent-skills/export` (peer-bearer auth), so the hub relays one preview per side.
|
|
57
|
+
*
|
|
58
|
+
* The two caps are different questions. TEXT is per FILE: past 256 KiB a file is reported as binary
|
|
59
|
+
* (the dialog never renders a wall of diff lines), and the SAME rule covers a file that is not valid
|
|
60
|
+
* UTF-8 — `text: null, binary: true` is one answer for "not lines we can diff". TOTAL is per
|
|
61
|
+
* RESPONSE: the hub refuses (413) rather than streaming megabytes through one JSON body.
|
|
62
|
+
*/
|
|
63
|
+
const MAX_PREVIEW_TEXT_BYTES = 256 * 1024;
|
|
64
|
+
const MAX_PREVIEW_BYTES = 2 * 1024 * 1024;
|
|
65
|
+
/** codex-install.js's marker `source` (an ON-DISK value, restated as data — see inventory.js). */
|
|
66
|
+
const AGENT_BUNDLE_SOURCE = 'agent-bundle';
|
|
67
|
+
|
|
68
|
+
/** The per-machine plugin exclusion store (§6.5). A DB column is out of scope for this WP: see below. */
|
|
69
|
+
const EXCLUDE_FILE = 'agent-bundle-exclude-plugins.json';
|
|
70
|
+
const EXCLUDE_SCHEMA = 1;
|
|
71
|
+
const MAX_EXCLUDE_ENTRIES = 200;
|
|
72
|
+
const MAX_EXCLUDE_ID_CHARS = 200;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One typed failure, carrying the wire status. Both route modules map it by `status`/`code` alone, so
|
|
76
|
+
* the box's answer and the hub's relay of it are the same shape for the same fault.
|
|
77
|
+
*/
|
|
78
|
+
class AgentSkillError extends Error {
|
|
79
|
+
constructor(code, status, detail) {
|
|
80
|
+
super(code);
|
|
81
|
+
this.name = 'AgentSkillError';
|
|
82
|
+
this.code = code;
|
|
83
|
+
this.status = Number(status) || 500;
|
|
84
|
+
if (detail != null) this.detail = String(detail).slice(0, 200);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sha256Hex(buffer) {
|
|
89
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _safeDetail(value) {
|
|
93
|
+
const text = value instanceof Error
|
|
94
|
+
? String(value.message || value.name || 'error')
|
|
95
|
+
: String(value == null ? '' : value);
|
|
96
|
+
// Defensive: a transport message is caller-supplied text and the only secret on this path is the
|
|
97
|
+
// peer bearer, so redact the shape a bearer takes before it can escape into a response.
|
|
98
|
+
return text.replace(/bearer\s+[^\s,;]+/gi, 'bearer <redacted>').replace(/[\r\n\t]+/g, ' ').trim().slice(0, 200);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _resolvedHome(homeDir) {
|
|
102
|
+
return String(homeDir == null ? '' : homeDir).replace(/\/+$/, '') || '/';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _string(value, max = 1000) {
|
|
106
|
+
return String(value == null ? '' : value).trim().slice(0, max);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `20261026T120000000Z` — sortable, path-safe, and derived from an INJECTED clock. */
|
|
110
|
+
function _compactTs(ms) {
|
|
111
|
+
return new Date(Number(ms) || Date.now()).toISOString().replace(/[-:.]/g, '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function _statOrNull(target) {
|
|
115
|
+
try {
|
|
116
|
+
return await fsp.stat(target);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return null;
|
|
119
|
+
throw error; // RETHROW: a real stat fault is the caller's to REPORT
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function _realOrPlain(target) {
|
|
124
|
+
try {
|
|
125
|
+
return await fsp.realpath(target);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) return target;
|
|
128
|
+
throw error; // RETHROW: a real resolution fault is the caller's to REPORT
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The two ref fields, validated. `agent` is one of exactly two CLIs and `name` is a directory name
|
|
134
|
+
* (never a path): everything downstream builds a path out of `name`, so this is the one gate.
|
|
135
|
+
*/
|
|
136
|
+
function validateSkillRef(agent, name) {
|
|
137
|
+
const resolvedAgent = _string(agent, 40);
|
|
138
|
+
const resolvedName = String(name == null ? '' : name).trim();
|
|
139
|
+
if (!AGENTS.includes(resolvedAgent) || !NAME_RE.test(resolvedName)) {
|
|
140
|
+
throw new AgentSkillError('agent_skill_invalid', 422);
|
|
141
|
+
}
|
|
142
|
+
// NAME_RE alone still admits the two directory self-names, and every path below is built by joining
|
|
143
|
+
// this name onto a root — so `.`/`..` would escape the root rather than name an item (`readdir` never
|
|
144
|
+
// returns either, so no real item is refused by this).
|
|
145
|
+
if (resolvedName === '.' || resolvedName === '..') throw new AgentSkillError('agent_skill_invalid', 422);
|
|
146
|
+
return { agent: resolvedAgent, name: resolvedName };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ------------------------------------------------------------------------------------------------
|
|
150
|
+
* BOX side — locate / export / retire
|
|
151
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The item folder for `(agent, name)` on THIS machine, or null. Deliberately the SAME enumeration
|
|
155
|
+
* `readBoxInventory` runs (build.js's listers, in build.js's order), so "the same skill" means the
|
|
156
|
+
* same folder for the inventory, the export and the retire — including Codex's first-root-wins rule.
|
|
157
|
+
*/
|
|
158
|
+
async function _locateBoxItem(agent, name, homeDir, env) {
|
|
159
|
+
const resolvedHome = _resolvedHome(homeDir);
|
|
160
|
+
const items = agent === 'claude'
|
|
161
|
+
? await listClaudeSkillItems(inventory.claudeConfigDir(resolvedHome, env))
|
|
162
|
+
: agent === 'dsh'
|
|
163
|
+
// WP-12: dsh's box roots are <home>/.agents/skills AND $DSH_HOME/skills (default ~/.dsh/skills) --
|
|
164
|
+
// the same pair the codex lister walks, with $DSH_HOME standing in for $CODEX_HOME, so a copy
|
|
165
|
+
// under either dsh root is exportable.
|
|
166
|
+
? await listCodexSkillItems(resolvedHome, { ...env, CODEX_HOME: env.DSH_HOME || path.join(resolvedHome, '.dsh') })
|
|
167
|
+
: await listCodexSkillItems(resolvedHome, env);
|
|
168
|
+
return items.find((item) => item.name === name) || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* §6.2 `GET /api/agent-skills/export`: the bytes of ONE box-owned skill.
|
|
173
|
+
*
|
|
174
|
+
* `managed: 'ctm-managed'` is a 404 (`agent_skill_not_found`) rather than a 409: this release's own
|
|
175
|
+
* `delegate` copy is simply not an item that can be proposed for import, and saying "not found" keeps
|
|
176
|
+
* the hub from ever rendering it as one.
|
|
177
|
+
*
|
|
178
|
+
* @returns {Promise<{agent: string, name: string, itemHash: string, files: Array<object>}>}
|
|
179
|
+
*/
|
|
180
|
+
async function exportBoxSkill({ homeDir = os.homedir(), env = process.env, agent, name } = {}) {
|
|
181
|
+
const ref = validateSkillRef(agent, name);
|
|
182
|
+
const item = await _locateBoxItem(ref.agent, ref.name, homeDir, env);
|
|
183
|
+
if (!item) throw new AgentSkillError('agent_skill_not_found', 404);
|
|
184
|
+
if (await inventory.managedStateOf(item) === 'ctm-managed') {
|
|
185
|
+
throw new AgentSkillError('agent_skill_not_found', 404);
|
|
186
|
+
}
|
|
187
|
+
const realRoot = await _realOrPlain(item.rootDir);
|
|
188
|
+
const walked = await walkItem(realRoot);
|
|
189
|
+
if (walked.error) throw new AgentSkillError('agent_skill_not_found', 404);
|
|
190
|
+
const files = walked.files;
|
|
191
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
192
|
+
if (total > MAX_EXPORT_BYTES) throw new AgentSkillError('agent_skill_too_large', 413);
|
|
193
|
+
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const file of files) {
|
|
196
|
+
const buffer = await fsp.readFile(file.abs);
|
|
197
|
+
out.push({
|
|
198
|
+
path: file.rel,
|
|
199
|
+
mode: file.mode,
|
|
200
|
+
size: buffer.length,
|
|
201
|
+
sha256: sha256Hex(buffer),
|
|
202
|
+
contentBase64: buffer.toString('base64'),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// The item hash is the canonical entry list build.js hashes — computed from the SAME bytes this
|
|
206
|
+
// response carries, so the hub's recomputation can only disagree if a byte changed in flight.
|
|
207
|
+
return { agent: ref.agent, name: ref.name, itemHash: computeTreeHash(out.map(_entryOf)), files: out };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The `{path, mode, sha256, size}` projection computeTreeHash folds (one shape, one place). */
|
|
211
|
+
function _entryOf(file) {
|
|
212
|
+
return { path: file.path, mode: file.mode, sha256: file.sha256, size: file.size };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** rename, or (across filesystems) copy-then-remove. NEVER a plain delete of the source. */
|
|
216
|
+
async function _moveDir(src, dest) {
|
|
217
|
+
try {
|
|
218
|
+
await fsp.rename(src, dest);
|
|
219
|
+
return;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (!error || error.code !== 'EXDEV') throw error; // RETHROW: only EXDEV has a fallback
|
|
222
|
+
}
|
|
223
|
+
await fsp.cp(src, dest, { recursive: true, force: true, verbatimSymlinks: true });
|
|
224
|
+
await fsp.rm(src, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* §6.2/§6.5 `POST /api/agent-skills/retire` — D-13's "moves, never deletes".
|
|
229
|
+
*
|
|
230
|
+
* The copy is re-hashed NOW and must still be the one the user reviewed (`expectHash`); a managed
|
|
231
|
+
* (`ctm-managed`) copy is refused outright. The folder is then MOVED under
|
|
232
|
+
* `<data dir>/agent-bundles/retired/<agent>/<name>-<compactTs>/` — never removed — so the box user can
|
|
233
|
+
* always recover it by hand.
|
|
234
|
+
*
|
|
235
|
+
* A folder carrying an `agent-bundle` marker (a WP-6 Codex install the box then edited) ALSO drops its
|
|
236
|
+
* root-record entry in `<homeDir>/.agents/skills`: without that, the hub's next push would read the
|
|
237
|
+
* missing target as a USER DELETION (`removed`) and never re-install the bundle copy, which is exactly
|
|
238
|
+
* the shadowing D-13 exists to end.
|
|
239
|
+
*
|
|
240
|
+
* @returns {Promise<{retiredTo: string}>}
|
|
241
|
+
*/
|
|
242
|
+
async function retireBoxSkill({
|
|
243
|
+
homeDir = os.homedir(), env = process.env, dataDir = null, agent, name, expectHash = null, now = Date.now,
|
|
244
|
+
} = {}) {
|
|
245
|
+
const ref = validateSkillRef(agent, name);
|
|
246
|
+
if (!dataDir) throw new Error('retireBoxSkill: a data dir is required');
|
|
247
|
+
const item = await _locateBoxItem(ref.agent, ref.name, homeDir, env);
|
|
248
|
+
if (!item) throw new AgentSkillError('agent_skill_not_found', 404);
|
|
249
|
+
if (await inventory.managedStateOf(item) === 'ctm-managed') {
|
|
250
|
+
throw new AgentSkillError('agent_skill_managed', 409);
|
|
251
|
+
}
|
|
252
|
+
const itemHash = await itemHashOfDir(item.rootDir);
|
|
253
|
+
if (itemHash === null) throw new AgentSkillError('agent_skill_not_found', 404);
|
|
254
|
+
const expected = String(expectHash == null ? '' : expectHash).trim();
|
|
255
|
+
if (!expected || itemHash !== expected) {
|
|
256
|
+
throw new AgentSkillError('agent_skill_changed_since_review', 409);
|
|
257
|
+
}
|
|
258
|
+
// Read the marker BEFORE the move: after it, the folder is at the retired path.
|
|
259
|
+
const marker = await readMarker(item.rootDir, ref.name);
|
|
260
|
+
const retiredTo = path.join(store.storeRoot(dataDir), 'retired', ref.agent, `${ref.name}-${_compactTs(now)}`);
|
|
261
|
+
await fsp.mkdir(path.dirname(retiredTo), { recursive: true });
|
|
262
|
+
await _moveDir(item.rootDir, retiredTo);
|
|
263
|
+
if (marker && marker.source === AGENT_BUNDLE_SOURCE) {
|
|
264
|
+
await codexInstall.dropRecordEntry(codexInstall.skillsRoot(_resolvedHome(homeDir)), ref.name);
|
|
265
|
+
}
|
|
266
|
+
return { retiredTo };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* ------------------------------------------------------------------------------------------------
|
|
270
|
+
* HUB side — the hub's own skill root, the export verification, the backup + stage + rename write
|
|
271
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
272
|
+
|
|
273
|
+
/** The hub's skill root for one agent: `<CLAUDE_CONFIG_DIR|~/.claude>/skills`, `~/.agents/skills`, or
|
|
274
|
+
* (WP-12) `$DSH_HOME/skills` (default `~/.dsh/skills`) for dsh. */
|
|
275
|
+
async function resolveHubSkillsRoot(agent, homeDir = os.homedir(), env = process.env) {
|
|
276
|
+
const resolvedHome = _resolvedHome(homeDir);
|
|
277
|
+
const plain = agent === 'claude'
|
|
278
|
+
? path.join(inventory.claudeConfigDir(resolvedHome, env), 'skills')
|
|
279
|
+
: agent === 'dsh'
|
|
280
|
+
? path.join(env.DSH_HOME ? _resolvedHome(env.DSH_HOME) : path.join(resolvedHome, '.dsh'), 'skills')
|
|
281
|
+
: path.join(resolvedHome, '.agents', 'skills');
|
|
282
|
+
// AC-8a: a Dropbox/`~/.claude`-symlinked root is the observed real shape, so the write target is the
|
|
283
|
+
// RESOLVED directory — writing through the symlink would still work, but the backup and the replace
|
|
284
|
+
// would then straddle two paths for one folder.
|
|
285
|
+
return _realOrPlain(plain);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* §6.2/§6.5 step 2: every file's `sha256` is recomputed from its decoded bytes, and the item hash is
|
|
290
|
+
* recomputed from those bytes — an export that disagrees with ITSELF (`agent_skill_export_corrupt`,
|
|
291
|
+
* 502) is a different fault from an export that disagrees with what the user reviewed
|
|
292
|
+
* (`agent_skill_changed_since_review`, 409, AC-8c).
|
|
293
|
+
*
|
|
294
|
+
* @returns {Array<{path: string, mode: number, sha256: string, size: number, contentBase64: string}>}
|
|
295
|
+
*/
|
|
296
|
+
function verifyExport(body, agent, name) {
|
|
297
|
+
const corrupt = () => new AgentSkillError('agent_skill_export_corrupt', 502);
|
|
298
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) throw corrupt();
|
|
299
|
+
if (String(body.agent) !== agent || String(body.name) !== name) throw corrupt();
|
|
300
|
+
if (!Array.isArray(body.files) || typeof body.itemHash !== 'string') throw corrupt();
|
|
301
|
+
const files = [];
|
|
302
|
+
for (const raw of body.files) {
|
|
303
|
+
if (!raw || typeof raw !== 'object') throw corrupt();
|
|
304
|
+
const rel = String(raw.path == null ? '' : raw.path);
|
|
305
|
+
if (!rel || rel.startsWith('/') || rel.includes('\\') || rel.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
306
|
+
throw corrupt();
|
|
307
|
+
}
|
|
308
|
+
const sha = String(raw.sha256 == null ? '' : raw.sha256);
|
|
309
|
+
if (!/^[0-9a-f]{64}$/.test(sha)) throw corrupt();
|
|
310
|
+
const mode = Number(raw.mode);
|
|
311
|
+
if (mode !== 0o444 && mode !== 0o555) throw corrupt();
|
|
312
|
+
const size = Number(raw.size);
|
|
313
|
+
if (!Number.isInteger(size) || size < 0) throw corrupt();
|
|
314
|
+
const contentBase64 = String(raw.contentBase64 == null ? '' : raw.contentBase64);
|
|
315
|
+
const buffer = Buffer.from(contentBase64, 'base64');
|
|
316
|
+
if (buffer.length !== size || sha256Hex(buffer) !== sha) throw corrupt();
|
|
317
|
+
files.push({ path: rel, mode, sha256: sha, size, contentBase64 });
|
|
318
|
+
}
|
|
319
|
+
if (_itemHashOfFiles(files) !== body.itemHash) throw corrupt();
|
|
320
|
+
return files;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The item hash of a verified file list — build.js's canonical list, byte for byte. */
|
|
324
|
+
function _itemHashOfFiles(files) {
|
|
325
|
+
return computeTreeHash(files.map(_entryOf));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ------------------------------------------------------------------------------------------------
|
|
329
|
+
* WP-9 PREVIEW (§9 S-9, AC-9b) — the read-only diff source both sides of the Review dialog use
|
|
330
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* One preview entry from the bytes on disk. `text` is the file's lines only when the bytes are valid
|
|
334
|
+
* UTF-8 AND at most MAX_PREVIEW_TEXT_BYTES; everything else is `{text: null, binary: true}` with the
|
|
335
|
+
* real size, so the dialog can say "binary file, <size>" without ever guessing.
|
|
336
|
+
*
|
|
337
|
+
* The validity test is the round trip plus the encoded form's NUL scan: a decoder that substituted
|
|
338
|
+
* U+FFFD lost information (the file is not UTF-8 text), and a NUL is the classic payload signature.
|
|
339
|
+
*/
|
|
340
|
+
function _previewEntryOf(filePath, buffer) {
|
|
341
|
+
const size = buffer.length;
|
|
342
|
+
const binary = size > MAX_PREVIEW_TEXT_BYTES || buffer.includes(0);
|
|
343
|
+
let text = null;
|
|
344
|
+
if (!binary) {
|
|
345
|
+
const decoded = buffer.toString('utf8');
|
|
346
|
+
text = (decoded.includes('�') || Buffer.byteLength(decoded, 'utf8') !== size) ? null : decoded;
|
|
347
|
+
}
|
|
348
|
+
return { path: filePath, text: text === null ? null : text, binary: text === null, size };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The HUB's own copy of `(agent, name)` — the folder `_writeIntoHubRoot` writes to, read with async
|
|
353
|
+
* fs. `null` when the hub holds no such folder (the "new on <machine>" case: there is nothing to diff
|
|
354
|
+
* against). An unreadable folder is also `null`: a preview is a read, never a failure the user acts on.
|
|
355
|
+
*
|
|
356
|
+
* @returns {Promise<{files: Array<{path: string, text: string|null, binary: boolean, size: number}>}|null>}
|
|
357
|
+
*/
|
|
358
|
+
async function readHubSkillFiles({ agent, name, hubHomeDir = os.homedir(), hubEnv = process.env } = {}) {
|
|
359
|
+
const ref = validateSkillRef(agent, name);
|
|
360
|
+
const root = await resolveHubSkillsRoot(ref.agent, hubHomeDir, hubEnv);
|
|
361
|
+
const dir = path.join(root, ref.name);
|
|
362
|
+
const stat = await _statOrNull(dir);
|
|
363
|
+
if (!stat || !stat.isDirectory()) return null;
|
|
364
|
+
const walked = await walkItem(await _realOrPlain(dir));
|
|
365
|
+
if (walked.error) return null;
|
|
366
|
+
const files = [];
|
|
367
|
+
for (const file of walked.files) {
|
|
368
|
+
let buffer;
|
|
369
|
+
try {
|
|
370
|
+
buffer = await fsp.readFile(file.abs);
|
|
371
|
+
} catch {
|
|
372
|
+
// HANDLE: one unreadable file is reported as a binary entry — the preview still shows the rest.
|
|
373
|
+
files.push({ path: file.rel, text: null, binary: true, size: Number(file.size) || 0 });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
files.push(_previewEntryOf(file.rel, buffer));
|
|
377
|
+
}
|
|
378
|
+
return { files };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* WP-9 (§9 S-9, AC-9b/AC-9c): `GET /api/machines/:id/skills/preview?agent=&name=`.
|
|
383
|
+
*
|
|
384
|
+
* The box's half rides the SAME fetch + verification as an import (validateSkillRef, `verifyExport`,
|
|
385
|
+
* the item hash recomputed from the bytes) — so a preview can never show a folder the import would
|
|
386
|
+
* then refuse. The hub's half is its own copy, or null. NOTHING is written on either side, and no
|
|
387
|
+
* backup/retire leg runs: this is the read the Review dialog renders diffs from.
|
|
388
|
+
*
|
|
389
|
+
* @returns {Promise<{agent: string, name: string, boxHash: string,
|
|
390
|
+
* box: {files: Array<object>}, hub: {files: Array<object>}|null}>}
|
|
391
|
+
*/
|
|
392
|
+
async function previewBoxSkill({ agent, name, hubHomeDir = os.homedir(), hubEnv = process.env, fetchExport } = {}) {
|
|
393
|
+
const ref = validateSkillRef(agent, name);
|
|
394
|
+
if (typeof fetchExport !== 'function') throw new AgentSkillError('agent_skill_box_unreachable', 502);
|
|
395
|
+
let response;
|
|
396
|
+
try {
|
|
397
|
+
response = await fetchExport({ agent: ref.agent, name: ref.name });
|
|
398
|
+
} catch (error) {
|
|
399
|
+
throw new AgentSkillError('agent_skill_box_unreachable', 502, _safeDetail(error));
|
|
400
|
+
}
|
|
401
|
+
const status = Number(response && response.status);
|
|
402
|
+
if (!Number.isFinite(status) || status === 0) throw new AgentSkillError('agent_skill_box_unreachable', 502, 'the box did not answer');
|
|
403
|
+
if (status === 404) throw new AgentSkillError('agent_skill_not_found', 404);
|
|
404
|
+
if (status !== 200 || !response.body) {
|
|
405
|
+
throw new AgentSkillError('agent_skill_box_unreachable', 502, `the box answered ${status}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Same verification the import runs, so "what the dialog showed" and "what an import would write"
|
|
409
|
+
// are the same bytes (and the same hash the Import button sends back as `expectBoxHash`).
|
|
410
|
+
const files = verifyExport(response.body, ref.agent, ref.name);
|
|
411
|
+
const boxHash = String(response.body.itemHash);
|
|
412
|
+
const box = {
|
|
413
|
+
files: files.map((file) => _previewEntryOf(file.path, Buffer.from(file.contentBase64, 'base64'))),
|
|
414
|
+
};
|
|
415
|
+
const hub = await readHubSkillFiles({
|
|
416
|
+
agent: ref.agent, name: ref.name, hubHomeDir, hubEnv,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const totalBytes = box.files.reduce((sum, file) => sum + file.size, 0)
|
|
420
|
+
+ (hub ? hub.files.reduce((sum, file) => sum + file.size, 0) : 0);
|
|
421
|
+
if (totalBytes > MAX_PREVIEW_BYTES) throw new AgentSkillError('agent_skill_preview_too_large', 413);
|
|
422
|
+
|
|
423
|
+
return { agent: ref.agent, name: ref.name, boxHash, box, hub };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Recursive copy that preserves the exec bit and reproduces neither the tree's nor the source's
|
|
427
|
+
* read-only dir modes (the hub's skill root is user-editable, P-5's whole point). */
|
|
428
|
+
async function _copyDirTree(srcDir, destDir) {
|
|
429
|
+
await fsp.mkdir(destDir, { recursive: true });
|
|
430
|
+
await fsp.chmod(destDir, 0o755);
|
|
431
|
+
for (const dirent of await fsp.readdir(srcDir, { withFileTypes: true })) {
|
|
432
|
+
const src = path.join(srcDir, dirent.name);
|
|
433
|
+
const dest = path.join(destDir, dirent.name);
|
|
434
|
+
if (dirent.isDirectory()) {
|
|
435
|
+
await _copyDirTree(src, dest);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (!dirent.isFile()) continue; // symlinks are never followed out of the folder
|
|
439
|
+
const stat = await fsp.lstat(src);
|
|
440
|
+
await fsp.copyFile(src, dest);
|
|
441
|
+
await fsp.chmod(dest, (stat.mode & 0o111) !== 0 ? 0o755 : 0o644);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Write the verified files into `destDir` (staging): files 0644, exec bit preserved as 0755, dirs
|
|
446
|
+
* 0755 — the user edits these copies, so no 0444 can survive the import. */
|
|
447
|
+
async function _writeFiles(destDir, files) {
|
|
448
|
+
await fsp.mkdir(destDir, { recursive: true });
|
|
449
|
+
await fsp.chmod(destDir, 0o755);
|
|
450
|
+
const dirs = new Set([destDir]);
|
|
451
|
+
for (const file of files) {
|
|
452
|
+
const target = path.join(destDir, ...file.path.split('/'));
|
|
453
|
+
const dir = path.dirname(target);
|
|
454
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
455
|
+
dirs.add(dir);
|
|
456
|
+
await fsp.writeFile(target, Buffer.from(file.contentBase64, 'base64'));
|
|
457
|
+
await fsp.chmod(target, (file.mode & 0o111) !== 0 ? 0o755 : 0o644);
|
|
458
|
+
}
|
|
459
|
+
for (const dir of dirs) await fsp.chmod(dir, 0o755);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* D-12: back up any existing hub copy FIRST, then stage inside the root and rename.
|
|
464
|
+
*
|
|
465
|
+
* ORDER IS LOAD-BEARING (P-5): the backup is written and VERIFIED (same item hash as the folder it
|
|
466
|
+
* came from) before a single byte of the live copy is touched, so the only way to lose the old copy is
|
|
467
|
+
* to fail while the new one is already in place — and even then the replaced folder is still on disk
|
|
468
|
+
* until the swap is complete.
|
|
469
|
+
*
|
|
470
|
+
* @returns {Promise<{importedTo: string, backupDir: string|null}>}
|
|
471
|
+
*/
|
|
472
|
+
async function _writeIntoHubRoot({ agent, name, files, dataDir, hubHomeDir, hubEnv, now }) {
|
|
473
|
+
const root = await resolveHubSkillsRoot(agent, hubHomeDir, hubEnv);
|
|
474
|
+
await fsp.mkdir(root, { recursive: true });
|
|
475
|
+
const targetDir = path.join(root, name);
|
|
476
|
+
const existing = await _statOrNull(targetDir);
|
|
477
|
+
|
|
478
|
+
let backupDir = null;
|
|
479
|
+
if (existing && existing.isDirectory()) {
|
|
480
|
+
backupDir = path.join(String(dataDir), 'skill-import-backups', agent, `${name}-${_compactTs(now)}`);
|
|
481
|
+
await fsp.mkdir(path.dirname(backupDir), { recursive: true });
|
|
482
|
+
await _copyDirTree(targetDir, backupDir);
|
|
483
|
+
const [beforeHash, backupHash] = await Promise.all([itemHashOfDir(targetDir), itemHashOfDir(backupDir)]);
|
|
484
|
+
if (beforeHash === null || beforeHash !== backupHash) {
|
|
485
|
+
throw new AgentSkillError('agent_skill_backup_failed', 500);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const rand = crypto.randomBytes(6).toString('hex');
|
|
490
|
+
const staging = path.join(root, `.${name}.ctm-import-${rand}`);
|
|
491
|
+
const replaced = path.join(root, `.${name}.ctm-replaced-${rand}`);
|
|
492
|
+
try {
|
|
493
|
+
await _writeFiles(staging, files);
|
|
494
|
+
let moved = false;
|
|
495
|
+
if (existing) {
|
|
496
|
+
await fsp.rename(targetDir, replaced);
|
|
497
|
+
moved = true;
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
await fsp.rename(staging, targetDir);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
// RETHROW: the swap failed, so put the original back before the staging dir is cleaned up.
|
|
503
|
+
if (moved) await fsp.rename(replaced, targetDir);
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
if (moved) await fsp.rm(replaced, { recursive: true, force: true });
|
|
507
|
+
} catch (error) {
|
|
508
|
+
await fsp.rm(staging, { recursive: true, force: true });
|
|
509
|
+
throw error; // RETHROW: the caller maps it; the original folder is still in place
|
|
510
|
+
}
|
|
511
|
+
return { importedTo: targetDir, backupDir };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* §6.5 `POST /api/machines/:instanceId/skills/import`.
|
|
516
|
+
*
|
|
517
|
+
* `fetchExport` / `retire` are the caller's transport (lib/api/machines.js builds them from the peer
|
|
518
|
+
* bearer + the row's dial list), so this module owns the policy and the route owns the socket. A
|
|
519
|
+
* retire failure NEVER undoes the import (the bytes are already the hub's), so the result carries
|
|
520
|
+
* `retired: false` plus the code instead of throwing.
|
|
521
|
+
*
|
|
522
|
+
* @returns {Promise<{importedTo: string, backupDir: string|null, retired: boolean,
|
|
523
|
+
* retiredTo: string|null, retireError: string|null}>}
|
|
524
|
+
*/
|
|
525
|
+
async function importBoxSkillToHub({
|
|
526
|
+
agent, name, expectBoxHash = null, dataDir,
|
|
527
|
+
hubHomeDir = os.homedir(), hubEnv = process.env,
|
|
528
|
+
fetchExport, retire = null, now = Date.now,
|
|
529
|
+
} = {}) {
|
|
530
|
+
const ref = validateSkillRef(agent, name);
|
|
531
|
+
if (!dataDir) throw new Error('importBoxSkillToHub: a data dir is required');
|
|
532
|
+
if (Object.hasOwn(MANAGED_SKILLS, ref.name)) {
|
|
533
|
+
throw new AgentSkillError('agent_skill_managed', 409);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
let response;
|
|
537
|
+
try {
|
|
538
|
+
response = await fetchExport({ agent: ref.agent, name: ref.name });
|
|
539
|
+
} catch (error) {
|
|
540
|
+
throw new AgentSkillError('agent_skill_box_unreachable', 502, _safeDetail(error));
|
|
541
|
+
}
|
|
542
|
+
const status = Number(response && response.status);
|
|
543
|
+
if (!Number.isFinite(status) || status === 0) {
|
|
544
|
+
throw new AgentSkillError('agent_skill_box_unreachable', 502, 'the box did not answer');
|
|
545
|
+
}
|
|
546
|
+
if (status === 404) throw new AgentSkillError('agent_skill_not_found', 404);
|
|
547
|
+
if (status !== 200 || !response.body) {
|
|
548
|
+
throw new AgentSkillError('agent_skill_box_unreachable', 502, `the box answered ${status}`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const files = verifyExport(response.body, ref.agent, ref.name);
|
|
552
|
+
const boxHash = String(response.body.itemHash);
|
|
553
|
+
const expected = String(expectBoxHash == null ? '' : expectBoxHash).trim();
|
|
554
|
+
if (boxHash !== expected) throw new AgentSkillError('agent_skill_changed_since_review', 409);
|
|
555
|
+
|
|
556
|
+
const written = await _writeIntoHubRoot({
|
|
557
|
+
agent: ref.agent, name: ref.name, files, dataDir, hubHomeDir, hubEnv, now,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
let retired = false;
|
|
561
|
+
let retiredTo = null;
|
|
562
|
+
let retireError = null;
|
|
563
|
+
if (typeof retire === 'function') {
|
|
564
|
+
try {
|
|
565
|
+
const answer = await retire({ agent: ref.agent, name: ref.name, expectHash: boxHash });
|
|
566
|
+
const retireStatus = Number(answer && answer.status);
|
|
567
|
+
if (retireStatus === 200 && answer.body && typeof answer.body.retiredTo === 'string') {
|
|
568
|
+
retired = true;
|
|
569
|
+
retiredTo = answer.body.retiredTo;
|
|
570
|
+
} else {
|
|
571
|
+
retireError = _string(answer && answer.body && answer.body.code, 80)
|
|
572
|
+
|| (retireStatus ? `agent_skill_box_http_${retireStatus}` : 'agent_skill_box_unreachable');
|
|
573
|
+
}
|
|
574
|
+
} catch (error) {
|
|
575
|
+
// REPORT: the import already succeeded; a dead box on the retire leg is a partial outcome the
|
|
576
|
+
// caller surfaces, never a rollback of bytes that are already correct.
|
|
577
|
+
retireError = 'agent_skill_box_unreachable';
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
retireError = 'agent_skill_retire_skipped';
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return { importedTo: written.importedTo, backupDir: written.backupDir, retired, retiredTo, retireError };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* §6.5 `POST /api/machines/:instanceId/skills/retire` — "use the laptop's copy", or a redundant `same`
|
|
588
|
+
* copy: the box's folder moves aside, and NOTHING is written on the hub. Same error mapping as the
|
|
589
|
+
* import leg, minus the write.
|
|
590
|
+
*
|
|
591
|
+
* @returns {Promise<{retiredTo: string}>}
|
|
592
|
+
*/
|
|
593
|
+
async function retireBoxSkillFromHub({ agent, name, expectBoxHash = null, retire } = {}) {
|
|
594
|
+
const ref = validateSkillRef(agent, name);
|
|
595
|
+
if (typeof retire !== 'function') throw new AgentSkillError('agent_skill_box_unreachable', 502);
|
|
596
|
+
let response;
|
|
597
|
+
try {
|
|
598
|
+
response = await retire({ agent: ref.agent, name: ref.name, expectHash: expectBoxHash });
|
|
599
|
+
} catch (error) {
|
|
600
|
+
throw new AgentSkillError('agent_skill_box_unreachable', 502, _safeDetail(error));
|
|
601
|
+
}
|
|
602
|
+
const status = Number(response && response.status);
|
|
603
|
+
const code = _string(response && response.body && response.body.code, 80);
|
|
604
|
+
if (!Number.isFinite(status) || status === 0) throw new AgentSkillError('agent_skill_box_unreachable', 502);
|
|
605
|
+
if (status === 200 && response.body && typeof response.body.retiredTo === 'string') {
|
|
606
|
+
return { retiredTo: response.body.retiredTo };
|
|
607
|
+
}
|
|
608
|
+
if (status === 404) throw new AgentSkillError('agent_skill_not_found', 404);
|
|
609
|
+
if (status === 409) throw new AgentSkillError(code || 'agent_skill_changed_since_review', 409);
|
|
610
|
+
if (status === 422) throw new AgentSkillError(code || 'agent_skill_invalid', 422);
|
|
611
|
+
throw new AgentSkillError('agent_skill_box_unreachable', 502, `the box answered ${status}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* ------------------------------------------------------------------------------------------------
|
|
615
|
+
* Per-machine plugin exclusions (§6.5)
|
|
616
|
+
*
|
|
617
|
+
* ONE hub-side JSON file, `<hub data dir>/agent-bundle-exclude-plugins.json`, shaped
|
|
618
|
+
* `{ schema: 1, machines: { <instanceId>: [<plugin id>…] } }`. A DB column is deliberately OUT of
|
|
619
|
+
* scope for this WP (it would mean a migration plus a db.js writer for a list that is read on the
|
|
620
|
+
* create path and written from one settings row); the file is atomic and the read below is cached in
|
|
621
|
+
* memory, re-read on write.
|
|
622
|
+
* ---------------------------------------------------------------------------------------------- */
|
|
623
|
+
|
|
624
|
+
let _excludeCache = null; // { dataDir, machines }
|
|
625
|
+
|
|
626
|
+
function _excludePath(dataDir) {
|
|
627
|
+
return path.join(String(dataDir == null ? '' : dataDir), EXCLUDE_FILE);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Load the store for `dataDir` into the in-memory cache (async — no sync fs on the main thread). A
|
|
632
|
+
* missing/garbled file is an empty list. Callers that need the list (the Machines list handler, the
|
|
633
|
+
* push path) await this first; `excludePluginsFor` then reads the cache synchronously.
|
|
634
|
+
*/
|
|
635
|
+
async function loadExcludePlugins(dataDir) {
|
|
636
|
+
const dir = String(dataDir == null ? '' : dataDir);
|
|
637
|
+
if (_excludeCache && _excludeCache.dataDir === dir) return _excludeCache.machines;
|
|
638
|
+
let machines = {};
|
|
639
|
+
try {
|
|
640
|
+
const parsed = JSON.parse(await fsp.readFile(path.join(dir, EXCLUDE_FILE), 'utf8'));
|
|
641
|
+
if (parsed && typeof parsed === 'object' && parsed.machines && typeof parsed.machines === 'object'
|
|
642
|
+
&& !Array.isArray(parsed.machines)) {
|
|
643
|
+
machines = parsed.machines;
|
|
644
|
+
}
|
|
645
|
+
} catch {
|
|
646
|
+
/* expected: no file yet, or a torn copy — both mean "nothing excluded", never a create failure */
|
|
647
|
+
}
|
|
648
|
+
_excludeCache = { dataDir: dir, machines };
|
|
649
|
+
return machines;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** The plugin ids excluded for ONE machine, or `[]`, from the cache `loadExcludePlugins` filled
|
|
653
|
+
* (a store never loaded for this dir reads as nothing excluded). */
|
|
654
|
+
function excludePluginsFor(dataDir, instanceId) {
|
|
655
|
+
const id = _string(instanceId, 160);
|
|
656
|
+
if (!id) return [];
|
|
657
|
+
const dir = String(dataDir == null ? '' : dataDir);
|
|
658
|
+
const machines = _excludeCache && _excludeCache.dataDir === dir ? _excludeCache.machines : {};
|
|
659
|
+
const list = machines[id];
|
|
660
|
+
return Array.isArray(list) ? list.slice() : [];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* §6.5 `PUT /api/machines/:instanceId/skills/exclude-plugins`: replace one machine's list. Ids are
|
|
665
|
+
* strings of at most 200 chars, deduped and sorted; at most 200 of them (the plugin count a hub can
|
|
666
|
+
* plausibly carry, and the bound that keeps the file small).
|
|
667
|
+
*
|
|
668
|
+
* @returns {Promise<string[]>} the stored list
|
|
669
|
+
*/
|
|
670
|
+
async function setExcludePlugins(dataDir, instanceId, plugins) {
|
|
671
|
+
const id = _string(instanceId, 160);
|
|
672
|
+
if (!id) throw new AgentSkillError('agent_skill_invalid', 422);
|
|
673
|
+
if (!Array.isArray(plugins)) throw new AgentSkillError('exclude_plugins_invalid', 422);
|
|
674
|
+
const unique = new Set();
|
|
675
|
+
for (const value of plugins) {
|
|
676
|
+
if (typeof value !== 'string') throw new AgentSkillError('exclude_plugins_invalid', 422);
|
|
677
|
+
const pluginId = value.trim();
|
|
678
|
+
if (!pluginId || pluginId.length > MAX_EXCLUDE_ID_CHARS) throw new AgentSkillError('exclude_plugins_invalid', 422);
|
|
679
|
+
unique.add(pluginId);
|
|
680
|
+
}
|
|
681
|
+
if (unique.size > MAX_EXCLUDE_ENTRIES) throw new AgentSkillError('exclude_plugins_invalid', 422);
|
|
682
|
+
const list = [...unique].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
683
|
+
const dir = String(dataDir == null ? '' : dataDir);
|
|
684
|
+
const machines = { ...(await loadExcludePlugins(dir)), [id]: list };
|
|
685
|
+
const next = { schema: EXCLUDE_SCHEMA, machines };
|
|
686
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
687
|
+
await atomicWriteFile(_excludePath(dir), JSON.stringify(next, null, 2) + '\n');
|
|
688
|
+
// Re-read on write: the cache IS the file's parsed content, so a write refreshes it in place.
|
|
689
|
+
_excludeCache = { dataDir: dir, machines };
|
|
690
|
+
return list;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Test-only: drop the in-memory cache so each test observes its own file. Module state, like every
|
|
695
|
+
* other cache in this tree.
|
|
696
|
+
*/
|
|
697
|
+
function _resetExcludePluginsCacheForTests() {
|
|
698
|
+
_excludeCache = null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
module.exports = {
|
|
702
|
+
AgentSkillError,
|
|
703
|
+
// box
|
|
704
|
+
validateSkillRef,
|
|
705
|
+
exportBoxSkill,
|
|
706
|
+
retireBoxSkill,
|
|
707
|
+
// hub
|
|
708
|
+
resolveHubSkillsRoot,
|
|
709
|
+
verifyExport,
|
|
710
|
+
importBoxSkillToHub,
|
|
711
|
+
retireBoxSkillFromHub,
|
|
712
|
+
// WP-9: the review dialog's diff source.
|
|
713
|
+
readHubSkillFiles,
|
|
714
|
+
previewBoxSkill,
|
|
715
|
+
// per-machine plugin exclusions
|
|
716
|
+
loadExcludePlugins,
|
|
717
|
+
excludePluginsFor,
|
|
718
|
+
setExcludePlugins,
|
|
719
|
+
_resetExcludePluginsCacheForTests,
|
|
720
|
+
EXCLUDE_FILE,
|
|
721
|
+
MAX_EXPORT_BYTES,
|
|
722
|
+
MAX_PREVIEW_BYTES,
|
|
723
|
+
MAX_PREVIEW_TEXT_BYTES,
|
|
724
|
+
};
|