create-walle 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/lib/owner-write-safety.js +135 -0
- package/lib/public-runtime.js +13 -5
- package/lib/release-schema.json +3 -2
- package/lib/runtime-closure-policy.json +6 -4
- package/package.json +1 -1
- package/template/bin/deps-doctor.js +1 -1
- package/template/bin/node-bin.sh +11 -0
- package/template/coding-task-manager/api-prompts.js +10 -22
- package/template/coding-task-manager/bin/claude-hook.js +7 -3
- package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
- package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
- package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
- package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
- package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
- package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
- package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
- package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
- package/template/coding-task-manager/db.js +705 -286
- package/template/coding-task-manager/git-utils.js +44 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
- package/template/coding-task-manager/lib/agent-presets.js +15 -0
- package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
- package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
- package/template/coding-task-manager/lib/api/host-stats.js +98 -0
- package/template/coding-task-manager/lib/api/ingress.js +35 -12
- package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
- package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
- package/template/coding-task-manager/lib/api/machines.js +461 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
- package/template/coding-task-manager/lib/api/providers.js +53 -6
- package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
- package/template/coding-task-manager/lib/api/sessions.js +160 -8
- package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
- package/template/coding-task-manager/lib/api/worktrees.js +38 -37
- package/template/coding-task-manager/lib/approval-drift.js +16 -1
- package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
- package/template/coding-task-manager/lib/approval-scope.js +68 -4
- package/template/coding-task-manager/lib/auth-context.js +32 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
- package/template/coding-task-manager/lib/auth-rules.js +186 -45
- package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
- package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
- package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
- package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
- package/template/coding-task-manager/lib/claude-session-title.js +6 -30
- package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
- package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
- package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
- package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
- package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
- package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
- package/template/coding-task-manager/lib/codex-paths.js +2 -1
- package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
- package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
- package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
- package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
- package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
- package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
- package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
- package/template/coding-task-manager/lib/db-domains.js +21 -2
- package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
- package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
- package/template/coding-task-manager/lib/feature-manifest.js +1 -1
- package/template/coding-task-manager/lib/fs-watcher.js +3 -2
- package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
- package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
- package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
- package/template/coding-task-manager/lib/headless-term-service.js +55 -17
- package/template/coding-task-manager/lib/host-control-dial.js +167 -0
- package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
- package/template/coding-task-manager/lib/host-handoff.js +51 -13
- package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
- package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
- package/template/coding-task-manager/lib/host-stats.js +328 -0
- package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
- package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
- package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
- package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
- package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
- package/template/coding-task-manager/lib/instance-manifest.js +13 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
- package/template/coding-task-manager/lib/launch-model-override.js +160 -1
- package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
- package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
- package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
- package/template/coding-task-manager/lib/login-tripwire.js +1 -0
- package/template/coding-task-manager/lib/machine-pairing.js +238 -0
- package/template/coding-task-manager/lib/machine-pool.js +409 -0
- package/template/coding-task-manager/lib/machine-registry.js +749 -0
- package/template/coding-task-manager/lib/machine-self.js +66 -0
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
- package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
- package/template/coding-task-manager/lib/media-journal.js +84 -27
- package/template/coding-task-manager/lib/media-store-options.js +43 -15
- package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
- package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
- package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
- package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
- package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
- package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
- package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
- package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-history-control.js +2 -2
- package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
- package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
- package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
- package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
- package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
- package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
- package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
- package/template/coding-task-manager/lib/read-pool-client.js +5 -2
- package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
- package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
- package/template/coding-task-manager/lib/remote-command-context.js +12 -0
- package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
- package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
- package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
- package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
- package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
- package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
- package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
- package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
- package/template/coding-task-manager/lib/restart-preflight.js +9 -2
- package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
- package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
- package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
- package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
- package/template/coding-task-manager/lib/resume-cwd.js +13 -0
- package/template/coding-task-manager/lib/resume-from-store.js +12 -1
- package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
- package/template/coding-task-manager/lib/resume-preflight.js +21 -16
- package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
- package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
- package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
- package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
- package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
- package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
- package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
- package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
- package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
- package/template/coding-task-manager/lib/session-create/identity.js +20 -1
- package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
- package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
- package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
- package/template/coding-task-manager/lib/session-create/response.js +30 -12
- package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
- package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
- package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
- package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
- package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
- package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
- package/template/coding-task-manager/lib/session-host-manager.js +152 -206
- package/template/coding-task-manager/lib/session-jobs.js +17 -9
- package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
- package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
- package/template/coding-task-manager/lib/session-standup.js +67 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
- package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
- package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
- package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
- package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
- package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
- package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
- package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
- package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
- package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
- package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
- package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
- package/template/coding-task-manager/lib/transcript-link.js +77 -0
- package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
- package/template/coding-task-manager/lib/transcript-source.js +8 -3
- package/template/coding-task-manager/lib/transcript-store.js +40 -0
- package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
- package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
- package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
- package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
- package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
- package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
- package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
- package/template/coding-task-manager/lib/ws-router.js +68 -1
- package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
- package/template/coding-task-manager/oauth-proxy.js +99 -55
- package/template/coding-task-manager/package-lock.json +24 -0
- package/template/coding-task-manager/package.json +3 -0
- package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
- package/template/coding-task-manager/providers/codex-mcp.js +14 -5
- package/template/coding-task-manager/public/css/app.css +279 -1
- package/template/coding-task-manager/public/css/reviews.css +4 -0
- package/template/coding-task-manager/public/index.html +294 -223
- package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
- package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
- package/template/coding-task-manager/public/js/background-work.js +13 -7
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
- package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
- package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
- package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
- package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
- package/template/coding-task-manager/public/js/history-scroller.js +187 -9
- package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
- package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
- package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
- package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
- package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
- package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
- package/template/coding-task-manager/public/js/reviews.js +7 -3
- package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
- package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
- package/template/coding-task-manager/public/js/row-join.js +4 -2
- package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
- package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
- package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
- package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
- package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
- package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
- package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
- package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
- package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
- package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
- package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
- package/template/coding-task-manager/public/js/walle-session.js +30 -7
- package/template/coding-task-manager/public/m/app.css +226 -8
- package/template/coding-task-manager/public/m/app.js +502 -9
- package/template/coding-task-manager/public/m/index.html +38 -1
- package/template/coding-task-manager/public/m/machines.js +1313 -0
- package/template/coding-task-manager/public/m/pool-failover.js +391 -0
- package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
- package/template/coding-task-manager/public/m/sw.js +5 -0
- package/template/coding-task-manager/queue-engine.js +13 -0
- package/template/coding-task-manager/server/approvals.js +5 -6
- package/template/coding-task-manager/server/boot.js +136 -17
- package/template/coding-task-manager/server.js +902 -239
- package/template/coding-task-manager/session-integrity.js +7 -9
- package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
- package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
- package/template/coding-task-manager/workers/session-host-process.js +14 -3
- package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
- package/template/coding-task-manager/workers/worktree-worker.js +34 -0
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/scripts/conversation-storage-v2-migration.js +78 -11
- package/template/shared/conversation-storage-v2-contract.js +42 -6
- package/template/shared/fixtures/worker-status-payloads.json +392 -0
- package/template/shared/git-remote-key.js +83 -0
- package/template/shared/host-identity.js +98 -2
- package/template/shared/owner-write-safety.js +135 -0
- package/template/shared/public-runtime.js +13 -5
- package/template/shared/release-schema.json +3 -2
- package/template/shared/report-citations.js +14 -0
- package/template/shared/sqlite-write-lock.js +361 -223
- package/template/shared/storage/contract.js +35 -3
- package/template/wall-e/agent.js +21 -0
- package/template/wall-e/api-walle.js +5 -0
- package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
- package/template/wall-e/brain/helper-rules-schema.js +129 -0
- package/template/wall-e/brain/migrations.js +51 -22
- package/template/wall-e/brain/model-deployment-facts.js +31 -1
- package/template/wall-e/brain/owner-call-whitelist.js +16 -0
- package/template/wall-e/brain/schema-ensures.js +22 -0
- package/template/wall-e/brain.js +1 -1
- package/template/wall-e/coding/journal-dispatcher.js +51 -0
- package/template/wall-e/coding/session-journal.js +28 -40
- package/template/wall-e/coding-orchestrator.js +23 -26
- package/template/wall-e/http/auth.js +18 -1
- package/template/wall-e/http/blog-views-api.js +318 -0
- package/template/wall-e/http/helper-rules-api.js +321 -0
- package/template/wall-e/http/model-proxy-providers.js +3 -1
- package/template/wall-e/http/telemetry-api.js +12 -0
- package/template/wall-e/lib/blog-view-policy.js +88 -0
- package/template/wall-e/lib/blog-view-store.js +169 -0
- package/template/wall-e/lib/client-config-lock.js +2 -1
- package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
- package/template/wall-e/lib/error-kinds.js +10 -1
- package/template/wall-e/lib/feature-manifest.js +1 -1
- package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
- package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
- package/template/wall-e/lib/helper-rules/grade.js +277 -0
- package/template/wall-e/lib/helper-rules/label.js +247 -0
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
- package/template/wall-e/lib/helper-rules/store.js +667 -0
- package/template/wall-e/lib/mcp-integration.js +5 -6
- package/template/wall-e/lib/runtime-worker-pool.js +1 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
- package/template/wall-e/lib/telemetry-db-schema.js +37 -0
- package/template/wall-e/lib/telemetry-process-client.js +76 -1
- package/template/wall-e/llm/default-fallback.js +4 -0
- package/template/wall-e/llm/model-catalog.js +13 -4
- package/template/wall-e/llm/model-roles.js +6 -0
- package/template/wall-e/llm/portkey-http.js +150 -0
- package/template/wall-e/llm/portkey-sync.js +15 -145
- package/template/wall-e/llm/worker-mcp-routing.js +61 -0
- package/template/wall-e/llm/worker-route.js +43 -1
- package/template/wall-e/loops/brain-custom-writes.js +45 -0
- package/template/wall-e/mcp-protocol-contract.js +41 -1
- package/template/wall-e/mcp-server.js +13 -2
- package/template/wall-e/mcp-worker-schemas.js +20 -2
- package/template/wall-e/memory/ctm-context-client.js +25 -5
- package/template/wall-e/shared/release-schema.json +3 -2
- package/template/wall-e/shared/sqlite-write-lock.js +361 -223
- package/template/wall-e/shared/storage/contract.js +35 -3
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
- package/template/wall-e/tools/local-tools.js +6 -4
- package/template/wall-e/workers/brain-owner-worker.js +9 -0
- package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
- package/template/wall-e/workers/telemetry-worker.js +8 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WP-3 (docs/machine-pool-picker-pairing-dashboard-2026-10-24.html §6.3, D-6/D-9, §9 S-15/S-16/S-23):
|
|
4
|
+
// the pairing code — the BOX side.
|
|
5
|
+
//
|
|
6
|
+
// A machine that wants to join a pool shows an 8-character code. The joining member posts the code
|
|
7
|
+
// plus the pool token to the box's PRE-AUTH `POST /api/machine-pairing/join`; the box verifies the
|
|
8
|
+
// code, burns it, and adopts the token. Rules that shape this module:
|
|
9
|
+
//
|
|
10
|
+
// * §6.3 verbatim — alphabet, length, TTL, the 3-live-offer ceiling and the 5-wrong-redeem burn.
|
|
11
|
+
// * The code is a BEARER secret for one redemption. It is NEVER stored and NEVER logged: the only
|
|
12
|
+
// thing that reaches the database is `sha256(normalizeCode(code))` hex (D-6, §11 anti-goals).
|
|
13
|
+
// * `redeem` compares hashes with crypto.timingSafeEqual over EQUAL-LENGTH buffers — the digest is
|
|
14
|
+
// fixed-width, so a length mismatch is a malformed row, never a timing oracle.
|
|
15
|
+
// * Expired and already-used offers answer the SAME `pairing_code_invalid` code: a wrong code, a
|
|
16
|
+
// stale offer and a burned one must be indistinguishable to the caller, or the endpoint becomes
|
|
17
|
+
// an oracle for "was there ever an offer here".
|
|
18
|
+
//
|
|
19
|
+
// LEAF module: node built-ins only. Everything is injectable (`now`, `randomBytes`, `db`) so the
|
|
20
|
+
// unit test never touches the real clock, the real CSPRNG or a real database (clock-free rule).
|
|
21
|
+
|
|
22
|
+
const crypto = require('node:crypto');
|
|
23
|
+
|
|
24
|
+
const CODE_ALPHABET = 'BCDFGHJKLMNPQRSTVWXZ'; // RFC 8628 §6.1 — no vowels, no 0/1/I/O look-alikes
|
|
25
|
+
const CODE_LENGTH = 8; // shown (and typed) as XXXX-XXXX
|
|
26
|
+
const OFFER_TTL_MS = 600_000;
|
|
27
|
+
const MAX_LIVE_OFFERS = 3;
|
|
28
|
+
const MAX_FAILED_REDEEMS = 5;
|
|
29
|
+
|
|
30
|
+
// The container the box prints/paints once: `ctmpair1:` + base64url(JSON).
|
|
31
|
+
const PAIRING_STRING_PREFIX = 'ctmpair1:';
|
|
32
|
+
|
|
33
|
+
// |CODE_ALPHABET| = 20 and 256 is NOT a multiple of 20, so `byte % 20` would favour the first four
|
|
34
|
+
// letters roughly 13% of the time. 240 = floor(256/20)*20 is the largest multiple of 20 that fits in
|
|
35
|
+
// a byte: every byte >= 240 is discarded and re-drawn (RFC 8628 §6.1 rejection sampling).
|
|
36
|
+
const REJECT_AT = Math.floor(256 / CODE_ALPHABET.length) * CODE_ALPHABET.length; // 240
|
|
37
|
+
// A batch per draw, and a hard ceiling on batches so a broken/scripted `randomBytes` cannot spin
|
|
38
|
+
// this loop forever — 32 batches of 16 bytes is a 1-in-10^22 tail, never a real event.
|
|
39
|
+
const DRAW_BATCH = CODE_LENGTH * 2;
|
|
40
|
+
const MAX_DRAW_BATCHES = 32;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* §6.3 verbatim: uppercase, then drop every character outside the alphabet. Hyphens and spaces are
|
|
44
|
+
* therefore formatting, not content — `wdjb-mjht`, `WDJB MJHT` and `WDJBMJHT` are one code.
|
|
45
|
+
*/
|
|
46
|
+
function normalizeCode(value) {
|
|
47
|
+
return String(value || '').toUpperCase().replace(/[^BCDFGHJKLMNPQRSTVWXZ]/g, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The display form the offer response carries: XXXX-XXXX (empty in => empty out). */
|
|
51
|
+
function formatCode(value) {
|
|
52
|
+
const code = normalizeCode(value);
|
|
53
|
+
if (code.length !== CODE_LENGTH) return code;
|
|
54
|
+
return `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** D-6: the ONLY representation of a code that is ever persisted. */
|
|
58
|
+
function hashCode(value) {
|
|
59
|
+
return crypto.createHash('sha256').update(normalizeCode(value), 'utf8').digest('hex');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Constant-time hash compare over equal-length buffers. Two sha256 digests are always 64 chars, so
|
|
64
|
+
* a mismatch in length means a malformed stored row — false, never a throw and never a compare.
|
|
65
|
+
*/
|
|
66
|
+
function codeHashEquals(left, right) {
|
|
67
|
+
const a = Buffer.from(String(left == null ? '' : left), 'utf8');
|
|
68
|
+
const b = Buffer.from(String(right == null ? '' : right), 'utf8');
|
|
69
|
+
if (!a.length || a.length !== b.length) return false;
|
|
70
|
+
return crypto.timingSafeEqual(a, b);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `XXXX-XXXX` -> a batch of random letters, rejection-sampled (no modulo bias). */
|
|
74
|
+
function mintCode(randomBytes) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let batch = 0; batch < MAX_DRAW_BATCHES && out.length < CODE_LENGTH; batch += 1) {
|
|
77
|
+
const bytes = randomBytes(DRAW_BATCH);
|
|
78
|
+
for (const byte of bytes) {
|
|
79
|
+
if (byte >= REJECT_AT) continue; // biased tail — re-draw, never fold
|
|
80
|
+
out.push(CODE_ALPHABET[byte % CODE_ALPHABET.length]);
|
|
81
|
+
if (out.length >= CODE_LENGTH) break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (out.length < CODE_LENGTH) throw new Error('machine-pairing: randomBytes did not yield an unbiased code');
|
|
85
|
+
return out.join('');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* §6.3: `'ctmpair1:' + base64url(JSON{v:1,i:instanceId,n:displayName,u:candidates,c:code})`.
|
|
90
|
+
* `c` is the DISPLAY form (XXXX-XXXX); `normalizeCode` strips the hyphen on the way back in.
|
|
91
|
+
*/
|
|
92
|
+
function encodePairingString({ instanceId, displayName, urls, code } = {}) {
|
|
93
|
+
const payload = JSON.stringify({
|
|
94
|
+
v: 1,
|
|
95
|
+
i: String(instanceId || ''),
|
|
96
|
+
n: String(displayName || ''),
|
|
97
|
+
u: (Array.isArray(urls) ? urls : []).map((url) => String(url || '')).filter(Boolean),
|
|
98
|
+
c: String(code || ''),
|
|
99
|
+
});
|
|
100
|
+
return `${PAIRING_STRING_PREFIX}${Buffer.from(payload, 'utf8').toString('base64url')}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The member side of the container. Returns null for anything that is not a v1 pairing string —
|
|
105
|
+
* a hand-edited or truncated string is a 400 (`pairing_string_invalid`), never a throw.
|
|
106
|
+
*/
|
|
107
|
+
function decodePairingString(value) {
|
|
108
|
+
const raw = String(value == null ? '' : value).trim();
|
|
109
|
+
if (!raw.startsWith(PAIRING_STRING_PREFIX)) return null;
|
|
110
|
+
let parsed = null;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(Buffer.from(raw.slice(PAIRING_STRING_PREFIX.length), 'base64url').toString('utf8'));
|
|
113
|
+
} catch {
|
|
114
|
+
return null; // expected: a corrupted container is a client error, not a server fault
|
|
115
|
+
}
|
|
116
|
+
if (!parsed || typeof parsed !== 'object' || parsed.v !== 1) return null;
|
|
117
|
+
const code = normalizeCode(parsed.c);
|
|
118
|
+
if (code.length !== CODE_LENGTH) return null;
|
|
119
|
+
const urls = (Array.isArray(parsed.u) ? parsed.u : [])
|
|
120
|
+
.map((url) => String(url || '').trim().replace(/\/+$/, ''))
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
const instanceId = String(parsed.i || '').trim();
|
|
123
|
+
if (!instanceId && !urls.length) return null;
|
|
124
|
+
return { version: 1, instanceId, displayName: String(parsed.n || ''), urls, code };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The box's pairing store.
|
|
129
|
+
*
|
|
130
|
+
* @param {{db: object, now?: () => number, randomBytes?: (n: number) => Buffer}} deps
|
|
131
|
+
* `db` must expose the four DB-owner ops: listLivePairingOffers, insertPairingOffer,
|
|
132
|
+
* markPairingOfferRedeemed, burnLivePairingOffers.
|
|
133
|
+
* @returns {{createOffer: () => Promise<object>, redeem: (code: string, options?: object) => Promise<object>,
|
|
134
|
+
* hasLiveOffers: () => Promise<boolean>, burnAll: () => Promise<object>}}
|
|
135
|
+
*/
|
|
136
|
+
function createMachinePairing({ db, now = () => Date.now(), randomBytes = (n) => crypto.randomBytes(n) } = {}) {
|
|
137
|
+
if (!db || typeof db.listLivePairingOffers !== 'function') {
|
|
138
|
+
throw new TypeError('machine-pairing: db.listLivePairingOffers is required');
|
|
139
|
+
}
|
|
140
|
+
// §6.3: in memory on purpose — a restart clearing the counter is fine (the offers themselves are
|
|
141
|
+
// single-use and TTL-bounded), while persisting it would put an attacker-controlled number in the
|
|
142
|
+
// DB. Five CONSECUTIVE failures burn everything live; one success resets it.
|
|
143
|
+
let failedRedeems = 0;
|
|
144
|
+
|
|
145
|
+
function _now() {
|
|
146
|
+
return Math.trunc(Number(now()) || 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* §6.3 `POST /api/machine-pairing/offers`. At most MAX_LIVE_OFFERS unspent offers exist at any
|
|
151
|
+
* moment, so a walked-away browser cannot leave an unbounded set of valid codes behind.
|
|
152
|
+
*/
|
|
153
|
+
async function createOffer() {
|
|
154
|
+
const at = _now();
|
|
155
|
+
const live = await db.listLivePairingOffers({ at });
|
|
156
|
+
if (Array.isArray(live) && live.length >= MAX_LIVE_OFFERS) {
|
|
157
|
+
return { ok: false, code: 'pairing_offer_limit' };
|
|
158
|
+
}
|
|
159
|
+
const code = mintCode(randomBytes);
|
|
160
|
+
const expiresAt = at + OFFER_TTL_MS;
|
|
161
|
+
await db.insertPairingOffer({ id: crypto.randomUUID(), codeHash: hashCode(code), createdAt: at, expiresAt });
|
|
162
|
+
return { ok: true, code: formatCode(code), expiresAt };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* §6.3 ordering: hash-compare against the live set, then burn the one that matched. `by` is the
|
|
167
|
+
* redeeming machine's instance id and is the only thing recorded — never the code.
|
|
168
|
+
*
|
|
169
|
+
* SINGLE USE IS THE UPDATE, NOT THE READ (F3). The candidate was found by reading the live set, but
|
|
170
|
+
* that read is not a claim: two joins holding the same code would both find the row and both return
|
|
171
|
+
* ok from a read-then-trust redeem. `markPairingOfferRedeemed` carries the `redeemed_at IS NULL`
|
|
172
|
+
* predicate in its WHERE clause and reports how many rows it changed, so exactly one of the two
|
|
173
|
+
* callers sees `changes === 1` and the loser is told `pairing_code_invalid` — the same answer an
|
|
174
|
+
* already-used code gets, which is what keeps the endpoint from being an oracle. A lost claim is NOT
|
|
175
|
+
* a failed guess (the caller was holding a real code), so it must not count toward the burn.
|
|
176
|
+
*/
|
|
177
|
+
async function redeem(code, { by = '' } = {}) {
|
|
178
|
+
const at = _now();
|
|
179
|
+
const live = await db.listLivePairingOffers({ at });
|
|
180
|
+
const wanted = hashCode(code);
|
|
181
|
+
const offer = (Array.isArray(live) ? live : []).find((row) => codeHashEquals(row && row.codeHash, wanted));
|
|
182
|
+
if (!offer) {
|
|
183
|
+
failedRedeems += 1;
|
|
184
|
+
if (failedRedeems >= MAX_FAILED_REDEEMS) {
|
|
185
|
+
failedRedeems = 0;
|
|
186
|
+
// §6.3 AC-3c: the 5th consecutive miss burns EVERY live offer, so an attacker who is
|
|
187
|
+
// brute-forcing the 20^8 space loses the offers they were guessing against.
|
|
188
|
+
await db.burnLivePairingOffers({ at });
|
|
189
|
+
}
|
|
190
|
+
return { ok: false, code: 'pairing_code_invalid' };
|
|
191
|
+
}
|
|
192
|
+
const id = String(offer.id || '');
|
|
193
|
+
const changed = Number(await db.markPairingOfferRedeemed({
|
|
194
|
+
id,
|
|
195
|
+
at,
|
|
196
|
+
redeemedBy: String(by || '').slice(0, 200),
|
|
197
|
+
})) || 0;
|
|
198
|
+
if (changed !== 1) return { ok: false, code: 'pairing_code_invalid' };
|
|
199
|
+
// Only a claimed offer is a success, and only a success resets the consecutive-miss counter.
|
|
200
|
+
failedRedeems = 0;
|
|
201
|
+
return { ok: true, offer: { id, expiresAt: Number(offer.expiresAt) || 0 } };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** §6.3/D-9: is this box showing a live code right now? (the identity probe's yes/no, never a code) */
|
|
205
|
+
async function hasLiveOffers() {
|
|
206
|
+
const live = await db.listLivePairingOffers({ at: _now() });
|
|
207
|
+
return Array.isArray(live) && live.length > 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* F4: after a successful adopt this box belongs to somebody, so every OTHER live code must die —
|
|
212
|
+
* otherwise a second primary holding a code minted before the join could re-adopt the box and walk
|
|
213
|
+
* away with the pool secret. Also clears the failure counter: the box is no longer "unpaired".
|
|
214
|
+
*/
|
|
215
|
+
async function burnAll() {
|
|
216
|
+
failedRedeems = 0;
|
|
217
|
+
return db.burnLivePairingOffers({ at: _now() });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { createOffer, redeem, hasLiveOffers, burnAll };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
CODE_ALPHABET,
|
|
225
|
+
CODE_LENGTH,
|
|
226
|
+
OFFER_TTL_MS,
|
|
227
|
+
MAX_LIVE_OFFERS,
|
|
228
|
+
MAX_FAILED_REDEEMS,
|
|
229
|
+
PAIRING_STRING_PREFIX,
|
|
230
|
+
normalizeCode,
|
|
231
|
+
formatCode,
|
|
232
|
+
hashCode,
|
|
233
|
+
codeHashEquals,
|
|
234
|
+
mintCode,
|
|
235
|
+
encodePairingString,
|
|
236
|
+
decodePairingString,
|
|
237
|
+
createMachinePairing,
|
|
238
|
+
};
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WP-2a (docs/machine-pool-picker-pairing-dashboard-2026-10-24.html §6.2, D-7/D-8, P-1..P-6): the
|
|
4
|
+
// pool core. This is the ONLY module that reads or writes the pool secret.
|
|
5
|
+
//
|
|
6
|
+
// Rev 2 D-7: one shared pool secret per pool, shaped `ctmpool1_<poolId>.<secret>`, stored in
|
|
7
|
+
// getDataDir()/machine-pool.json (mode 0600, atomic write). It is deliberately NOT stored in the
|
|
8
|
+
// DB — every member also sends the token OUTBOUND, so it cannot be hash-at-rest, and DB backups
|
|
9
|
+
// exist. The poolId half is public (it names the pool in logs and errors); the secret half is the
|
|
10
|
+
// 32 random bytes that authenticate.
|
|
11
|
+
//
|
|
12
|
+
// LEAF module: it requires only node built-ins and ./data-dir. It must NOT require
|
|
13
|
+
// hosted-sync-worker / machine-registry / remote-machine-create — lib/** dependency-cycle lint is
|
|
14
|
+
// at ceiling 0, and lib/auth-context.js (which does the inbound verification) is on that cycle's
|
|
15
|
+
// critical path. Everything is injectable so the unit test never touches the real data dir or the
|
|
16
|
+
// real clock (clock-free rule).
|
|
17
|
+
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const crypto = require('node:crypto');
|
|
21
|
+
const { getDataDir } = require('./data-dir');
|
|
22
|
+
|
|
23
|
+
const TOKEN_PREFIX = 'ctmpool1_';
|
|
24
|
+
// P-6: a rotation keeps the OLD secret accepted inbound for ten minutes, so a member that was
|
|
25
|
+
// offline during the rotate call is not locked out before the next sweep can reach it.
|
|
26
|
+
const ROTATION_GRACE_MS = 600_000;
|
|
27
|
+
// D-7: poolId = 12 lowercase base32 chars (RFC 4648 lowercase alphabet, 32 symbols = 5 bits each).
|
|
28
|
+
const POOL_ID_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
|
|
29
|
+
const POOL_ID_LENGTH = 12;
|
|
30
|
+
const SECRET_BYTES = 32;
|
|
31
|
+
|
|
32
|
+
function _poolFile() {
|
|
33
|
+
return path.join(getDataDir(), 'machine-pool.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Constant-time compare over equal-length buffers; a length mismatch (or an empty side) is false. */
|
|
37
|
+
function _constantTimeEqual(a, b) {
|
|
38
|
+
const left = Buffer.from(String(a == null ? '' : a), 'utf8');
|
|
39
|
+
const right = Buffer.from(String(b == null ? '' : b), 'utf8');
|
|
40
|
+
if (!left.length || left.length !== right.length) return false;
|
|
41
|
+
return crypto.timingSafeEqual(left, right);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 12 lowercase base32 chars from `randomBytes` — the public pool id (D-7). */
|
|
45
|
+
function _mintPoolId(randomBytes) {
|
|
46
|
+
const bytes = randomBytes(POOL_ID_LENGTH);
|
|
47
|
+
let out = '';
|
|
48
|
+
for (let i = 0; i < POOL_ID_LENGTH; i += 1) {
|
|
49
|
+
out += POOL_ID_ALPHABET[bytes[i] % POOL_ID_ALPHABET.length];
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** ctmpool1_<poolId>.<secret>; the secret is 32 random bytes, base64url (D-7). */
|
|
55
|
+
function _mintToken(randomBytes) {
|
|
56
|
+
const poolId = _mintPoolId(randomBytes);
|
|
57
|
+
const secret = Buffer.from(randomBytes(SECRET_BYTES)).toString('base64url');
|
|
58
|
+
return `${TOKEN_PREFIX}${poolId}.${secret}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The public half of a token, or '' when the shape is not a pool token. */
|
|
62
|
+
function poolIdOf(token) {
|
|
63
|
+
const raw = String(token == null ? '' : token);
|
|
64
|
+
if (!raw.startsWith(TOKEN_PREFIX)) return '';
|
|
65
|
+
const rest = raw.slice(TOKEN_PREFIX.length);
|
|
66
|
+
const dot = rest.indexOf('.');
|
|
67
|
+
if (dot <= 0) return '';
|
|
68
|
+
return rest.slice(0, dot);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Atomic write with mode 0600: temp file in the same dir, fsync, rename (the house pattern). */
|
|
72
|
+
async function _defaultWriteAtomic(file, data) {
|
|
73
|
+
const dir = path.dirname(file);
|
|
74
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
75
|
+
const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
76
|
+
let handle = null;
|
|
77
|
+
try {
|
|
78
|
+
handle = await fs.promises.open(tmp, 'w', 0o600);
|
|
79
|
+
await handle.writeFile(String(data), 'utf8');
|
|
80
|
+
await handle.sync();
|
|
81
|
+
} finally {
|
|
82
|
+
if (handle) await handle.close();
|
|
83
|
+
}
|
|
84
|
+
await fs.promises.rename(tmp, file);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _defaultReadFile(file) {
|
|
88
|
+
return fs.promises.readFile(file, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {{now?: () => number, readFile?: (p: string) => Promise<string>, writeFileAtomic?: (p: string, d: string) => Promise<void>, randomBytes?: (n: number) => Buffer}} [deps]
|
|
93
|
+
*/
|
|
94
|
+
function createMachinePool(deps = {}) {
|
|
95
|
+
const now = typeof deps.now === 'function' ? deps.now : () => Date.now();
|
|
96
|
+
const readFile = typeof deps.readFile === 'function' ? deps.readFile : _defaultReadFile;
|
|
97
|
+
const writeFileAtomic = typeof deps.writeFileAtomic === 'function' ? deps.writeFileAtomic : _defaultWriteAtomic;
|
|
98
|
+
const randomBytes = typeof deps.randomBytes === 'function' ? deps.randomBytes : (n) => crypto.randomBytes(n);
|
|
99
|
+
|
|
100
|
+
// { version, token, previous: {token, until} | null, roster: {revision, updatedAt, by},
|
|
101
|
+
// members: string[] }
|
|
102
|
+
let state = null;
|
|
103
|
+
|
|
104
|
+
function _snapshot() {
|
|
105
|
+
return state
|
|
106
|
+
? {
|
|
107
|
+
version: 1,
|
|
108
|
+
token: state.token,
|
|
109
|
+
previous: state.previous ? { token: state.previous.token, until: state.previous.until } : null,
|
|
110
|
+
roster: { ...state.roster },
|
|
111
|
+
members: [...state.members],
|
|
112
|
+
}
|
|
113
|
+
: null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _normalizeRoster(value) {
|
|
117
|
+
const roster = value && typeof value === 'object' ? value : {};
|
|
118
|
+
return {
|
|
119
|
+
revision: Number(roster.revision) || 0,
|
|
120
|
+
updatedAt: Number(roster.updatedAt) || 0,
|
|
121
|
+
by: String(roster.by || ''),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* P-8: the persisted membership set — the instanceIds of the OTHER machines in this pool. It is
|
|
127
|
+
* what makes the outbound credential a PER-TARGET decision: an env-token box that never joined
|
|
128
|
+
* must be dialled with the env bearer, not the pool secret it has never seen. Empty/duplicate
|
|
129
|
+
* entries are dropped; a non-array reads as "no members".
|
|
130
|
+
*/
|
|
131
|
+
function _normalizeMembers(value) {
|
|
132
|
+
if (!Array.isArray(value)) return [];
|
|
133
|
+
return [...new Set(value.map((id) => String(id == null ? '' : id).trim()).filter(Boolean))];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function _persist() {
|
|
137
|
+
await writeFileAtomic(_poolFile(), JSON.stringify(_snapshot(), null, 2));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Boot read. ENOENT (or any unreadable file) means "this machine has no pool" — never throws. */
|
|
141
|
+
async function load() {
|
|
142
|
+
let raw = '';
|
|
143
|
+
try {
|
|
144
|
+
raw = await readFile(_poolFile());
|
|
145
|
+
} catch {
|
|
146
|
+
state = null; // expected: no pool file yet — a pool-less member is the normal first state
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(String(raw || ''));
|
|
151
|
+
const token = String((parsed && parsed.token) || '');
|
|
152
|
+
if (!token.startsWith(TOKEN_PREFIX)) {
|
|
153
|
+
state = null;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const previous = parsed && parsed.previous && typeof parsed.previous === 'object'
|
|
157
|
+
? { token: String(parsed.previous.token || ''), until: Number(parsed.previous.until) || 0 }
|
|
158
|
+
: null;
|
|
159
|
+
state = {
|
|
160
|
+
version: 1,
|
|
161
|
+
token,
|
|
162
|
+
previous: previous && previous.token ? previous : null,
|
|
163
|
+
roster: _normalizeRoster(parsed && parsed.roster),
|
|
164
|
+
members: _normalizeMembers(parsed && parsed.members),
|
|
165
|
+
};
|
|
166
|
+
} catch {
|
|
167
|
+
state = null; // expected: a garbled file is treated as "no pool", never a boot failure
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function hasPool() {
|
|
172
|
+
return !!(state && state.token);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function poolId() {
|
|
176
|
+
return state ? poolIdOf(state.token) : '';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function token() {
|
|
180
|
+
return state ? state.token : '';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* D-7 inbound check. Synchronous over in-memory state (no I/O on the auth path) and constant
|
|
185
|
+
* time on equal-length buffers. The previous secret is accepted only inside its grace window
|
|
186
|
+
* (P-6); an expired one is refused even though it is still on disk.
|
|
187
|
+
*/
|
|
188
|
+
function verify(bearer) {
|
|
189
|
+
const raw = String(bearer == null ? '' : bearer);
|
|
190
|
+
if (!raw.startsWith(TOKEN_PREFIX) || !state || !state.token) return false;
|
|
191
|
+
if (_constantTimeEqual(raw, state.token)) return true;
|
|
192
|
+
if (state.previous && now() < state.previous.until && _constantTimeEqual(raw, state.previous.token)) return true;
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* P-6: the CURRENT secret only, never the grace one. A machine removed from the pool still holds
|
|
198
|
+
* the previous secret for ROTATION_GRACE_MS, so a pool MUTATION (roster push, rotate) accepted on
|
|
199
|
+
* the grace secret would let it rotate a remaining member to a token of its own choosing. The
|
|
200
|
+
* legitimate remover dials with the secret that is still the receiver's current one, so it passes.
|
|
201
|
+
*/
|
|
202
|
+
function isCurrent(bearer) {
|
|
203
|
+
const raw = String(bearer == null ? '' : bearer);
|
|
204
|
+
if (!raw.startsWith(TOKEN_PREFIX) || !state || !state.token) return false;
|
|
205
|
+
return _constantTimeEqual(raw, state.token);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** D-7: the first pairing on a pool-less member mints the pool (id + secret) at that moment. */
|
|
209
|
+
async function ensurePool() {
|
|
210
|
+
if (hasPool()) return state.token;
|
|
211
|
+
state = {
|
|
212
|
+
version: 1,
|
|
213
|
+
token: _mintToken(randomBytes),
|
|
214
|
+
previous: null,
|
|
215
|
+
roster: _normalizeRoster(state && state.roster),
|
|
216
|
+
members: _normalizeMembers(state && state.members),
|
|
217
|
+
};
|
|
218
|
+
await _persist();
|
|
219
|
+
return state.token;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* D-9 join: the box stores the token the redeeming member handed it, plus the roster version and
|
|
224
|
+
* (P-8) the membership set — without it this box would send the pool secret to every target,
|
|
225
|
+
* including the legacy env-token boxes that never joined and would 401 it.
|
|
226
|
+
*/
|
|
227
|
+
async function adopt({ token: adoptedToken, roster, members } = {}) {
|
|
228
|
+
const next = String(adoptedToken || '');
|
|
229
|
+
if (!next.startsWith(TOKEN_PREFIX)) throw new TypeError('machine-pool: adopt requires a ctmpool1_ token');
|
|
230
|
+
state = {
|
|
231
|
+
version: 1,
|
|
232
|
+
token: next,
|
|
233
|
+
previous: null,
|
|
234
|
+
roster: _normalizeRoster(roster),
|
|
235
|
+
members: _normalizeMembers(members),
|
|
236
|
+
};
|
|
237
|
+
await _persist();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** P-6: mint a new secret and keep the old one accepted inbound for the grace window. */
|
|
241
|
+
async function rotate() {
|
|
242
|
+
const old = state && state.token;
|
|
243
|
+
const next = _mintToken(randomBytes);
|
|
244
|
+
state = {
|
|
245
|
+
version: 1,
|
|
246
|
+
token: next,
|
|
247
|
+
previous: old ? { token: old, until: now() + ROTATION_GRACE_MS } : null,
|
|
248
|
+
roster: _normalizeRoster(state && state.roster),
|
|
249
|
+
members: _normalizeMembers(state && state.members),
|
|
250
|
+
};
|
|
251
|
+
await _persist();
|
|
252
|
+
return next;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** P-6 receiver side: the sender's new token becomes current; the old one gets the grace. */
|
|
256
|
+
async function acceptRotation({ token: rotated } = {}) {
|
|
257
|
+
const next = String(rotated || '');
|
|
258
|
+
if (!next.startsWith(TOKEN_PREFIX)) throw new TypeError('machine-pool: acceptRotation requires a ctmpool1_ token');
|
|
259
|
+
const old = state && state.token;
|
|
260
|
+
if (old === next) return;
|
|
261
|
+
state = {
|
|
262
|
+
version: 1,
|
|
263
|
+
token: next,
|
|
264
|
+
previous: old ? { token: old, until: now() + ROTATION_GRACE_MS } : null,
|
|
265
|
+
roster: _normalizeRoster(state && state.roster),
|
|
266
|
+
members: _normalizeMembers(state && state.members),
|
|
267
|
+
};
|
|
268
|
+
await _persist();
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function members() {
|
|
272
|
+
return state ? [...state.members] : [];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** P-8: is `id` a machine of this pool (and therefore dialled with the pool secret)? */
|
|
276
|
+
function isMember(id) {
|
|
277
|
+
const wanted = String(id == null ? '' : id).trim();
|
|
278
|
+
if (!wanted || !state) return false;
|
|
279
|
+
return state.members.includes(wanted);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** P-5 receive side: the pushed membership IS this member's set (self already excluded). */
|
|
283
|
+
async function setMembers(ids) {
|
|
284
|
+
state = {
|
|
285
|
+
version: 1,
|
|
286
|
+
token: state ? state.token : '',
|
|
287
|
+
previous: state ? state.previous : null,
|
|
288
|
+
roster: _normalizeRoster(state && state.roster),
|
|
289
|
+
members: _normalizeMembers(ids),
|
|
290
|
+
};
|
|
291
|
+
if (state.token) await _persist();
|
|
292
|
+
return members();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function addMember(id) {
|
|
296
|
+
const wanted = String(id == null ? '' : id).trim();
|
|
297
|
+
if (!wanted) return members();
|
|
298
|
+
return setMembers([...members(), wanted]);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function removeMember(id) {
|
|
302
|
+
const unwanted = String(id == null ? '' : id).trim();
|
|
303
|
+
if (!unwanted) return members();
|
|
304
|
+
return setMembers(members().filter((member) => member !== unwanted));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function rosterVersion() {
|
|
308
|
+
return _normalizeRoster(state && state.roster);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* P-5 receive side (beyond §6.2's literal list, which has no store-the-pushed-version call): the
|
|
313
|
+
* receiver stores the version it just applied verbatim, so its next GET/pull answers with the set
|
|
314
|
+
* the sender pushed instead of re-bumping and re-propagating. Returns the stored version.
|
|
315
|
+
*/
|
|
316
|
+
async function setRosterVersion(version) {
|
|
317
|
+
state = {
|
|
318
|
+
version: 1,
|
|
319
|
+
token: state ? state.token : '',
|
|
320
|
+
previous: state ? state.previous : null,
|
|
321
|
+
roster: _normalizeRoster(version),
|
|
322
|
+
members: _normalizeMembers(state && state.members),
|
|
323
|
+
};
|
|
324
|
+
if (state.token) await _persist();
|
|
325
|
+
return rosterVersion();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** P-5: revision+1, updatedAt = now(), by = self. A pool-less member starts at revision 1. */
|
|
329
|
+
async function bumpRoster(selfId) {
|
|
330
|
+
const current = _normalizeRoster(state && state.roster);
|
|
331
|
+
state = {
|
|
332
|
+
version: 1,
|
|
333
|
+
token: state ? state.token : '',
|
|
334
|
+
previous: state ? state.previous : null,
|
|
335
|
+
roster: { revision: current.revision + 1, updatedAt: now(), by: String(selfId || '') },
|
|
336
|
+
members: _normalizeMembers(state && state.members),
|
|
337
|
+
};
|
|
338
|
+
if (state.token) await _persist();
|
|
339
|
+
return rosterVersion();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* P-5 ordering: revision first, then updatedAt, then `by` as a string compare. `isNewer(a, b)`
|
|
344
|
+
* answers "does a outrank b" — an equal triple is not newer.
|
|
345
|
+
*/
|
|
346
|
+
function isNewer(a, b) {
|
|
347
|
+
const left = _normalizeRoster(a);
|
|
348
|
+
const right = _normalizeRoster(b);
|
|
349
|
+
if (left.revision !== right.revision) return left.revision > right.revision;
|
|
350
|
+
if (left.updatedAt !== right.updatedAt) return left.updatedAt > right.updatedAt;
|
|
351
|
+
return left.by > right.by;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* D-7: leave deletes the file — the token AND (P-8) the membership set go with it, so a
|
|
356
|
+
* re-joined box never dials a stale pool with a secret it no longer holds. The caller deletes the
|
|
357
|
+
* roster rows (deleteAllMachineRows).
|
|
358
|
+
*/
|
|
359
|
+
async function leave() {
|
|
360
|
+
state = null;
|
|
361
|
+
try {
|
|
362
|
+
await fs.promises.unlink(_poolFile());
|
|
363
|
+
} catch (error) {
|
|
364
|
+
// expected: no file to delete (a pool-less member leaving) is not a failure
|
|
365
|
+
if (error && error.code !== 'ENOENT') throw error;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
load,
|
|
371
|
+
hasPool,
|
|
372
|
+
poolId,
|
|
373
|
+
token,
|
|
374
|
+
verify,
|
|
375
|
+
isCurrent,
|
|
376
|
+
ensurePool,
|
|
377
|
+
adopt,
|
|
378
|
+
rotate,
|
|
379
|
+
acceptRotation,
|
|
380
|
+
rosterVersion,
|
|
381
|
+
setRosterVersion,
|
|
382
|
+
bumpRoster,
|
|
383
|
+
isNewer,
|
|
384
|
+
members,
|
|
385
|
+
isMember,
|
|
386
|
+
setMembers,
|
|
387
|
+
addMember,
|
|
388
|
+
removeMember,
|
|
389
|
+
leave,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// The process singleton lib/auth-context.js verifies against. auth-context's branch is
|
|
394
|
+
// SYNCHRONOUS (no I/O on the auth path), so it reads this instance's in-memory state; server.js
|
|
395
|
+
// awaits load() once at boot (before routes are served) to populate it.
|
|
396
|
+
let _singleton = null;
|
|
397
|
+
|
|
398
|
+
function getMachinePool() {
|
|
399
|
+
if (!_singleton) _singleton = createMachinePool();
|
|
400
|
+
return _singleton;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = {
|
|
404
|
+
TOKEN_PREFIX,
|
|
405
|
+
ROTATION_GRACE_MS,
|
|
406
|
+
createMachinePool,
|
|
407
|
+
getMachinePool,
|
|
408
|
+
poolIdOf,
|
|
409
|
+
};
|