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,200 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// §5 D-7 / §6 (docs/model-proxy-survives-ctm-restart-2026-10-22.html): the CLI half of oauth-proxy.js.
|
|
4
|
+
// It lives here so oauth-proxy.js stays inside its hard 1500-line ceiling, and nothing in this file is
|
|
5
|
+
// reachable from an embedded start() -- only from `node oauth-proxy.js`.
|
|
6
|
+
//
|
|
7
|
+
// Two ways in, distinguished by ONE thing: whether there is an IPC channel.
|
|
8
|
+
// * the supervisor spawns the proxy with the admin secret on STDIN (WP-0 §5 D-4): read one line, start
|
|
9
|
+
// serving, and the secret is only ever used for the loopback admin calls above;
|
|
10
|
+
// * a PREDECESSOR performing an upgrade handoff (§5 D-7) spawns it with an `ipc` channel and no stdin
|
|
11
|
+
// at all (`stdio: ['ignore', logFd, logFd, 'ipc']`): the three `handoff/v1*` messages carry the two
|
|
12
|
+
// listening sockets, the admin secret and the memory-only token state, and this process is the
|
|
13
|
+
// SUCCESSOR. It publishes its own identity and answers `ready` -- the predecessor stops accepting
|
|
14
|
+
// only after that answer, which is why the answer is sent from start(), not from here.
|
|
15
|
+
|
|
16
|
+
const readline = require('node:readline');
|
|
17
|
+
|
|
18
|
+
const LOOPBACK_HOST = '127.0.0.1';
|
|
19
|
+
const LOOPBACK_REFUSAL_EXIT_CODE = 78;
|
|
20
|
+
const STARTUP_FAILURE_EXIT_CODE = 1;
|
|
21
|
+
const SIGTERM_EXIT_CODE = 143;
|
|
22
|
+
|
|
23
|
+
// Test seam (§6, AC-2b): truthy => the successor does everything a successor does EXCEPT send `ready`, so
|
|
24
|
+
// a test can observe the predecessor's bounded wait from outside both processes. It is read through a
|
|
25
|
+
// local `env` binding -- the shape lib/hermetic-agent-launch-guard.js uses for its own test-only
|
|
26
|
+
// switches -- and it reaches a successor because the supervisor's `childEnv = { ...process.env }` and the
|
|
27
|
+
// predecessor's `env: process.env` both pass it through. There is no operator flag here.
|
|
28
|
+
function _holdReadyFlag(env = process.env) {
|
|
29
|
+
return String(env.CTM_TEST_HANDOFF_SUCCESSOR_HOLD_READY || '') === '1';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* §5 D-8 test seam: the CLI is the ONLY caller that arms the proxy's own linger timer, and this is its
|
|
34
|
+
* idle window. It is read through a local `env` binding, exactly like `_holdReadyFlag` above, so a child
|
|
35
|
+
* spawned by a test can be observed leaving its window in about a second instead of fifteen minutes; the
|
|
36
|
+
* production value comes from handoff.js (`PROXY_LINGER_MS`, === HOST_LINGER_MS). No operator flag, and
|
|
37
|
+
* no production path ever sets this.
|
|
38
|
+
*/
|
|
39
|
+
function _lingerMsFlag(env = process.env, handoff) {
|
|
40
|
+
const raw = Number(env.CTM_TEST_PROXY_LINGER_MS);
|
|
41
|
+
return Number.isSafeInteger(raw) && raw > 0 ? raw : handoff.PROXY_LINGER_MS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** D-1: the CLI's bind host, resolved once from the environment exactly as it always was. */
|
|
45
|
+
function _resolveHost(env = process.env) {
|
|
46
|
+
const raw = env.OAUTH_PROXY_HOST;
|
|
47
|
+
return typeof raw === 'string' && raw.length > 0 ? raw : LOOPBACK_HOST;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── D-4 stdin secret (CLI mode only) ────────────────────────────────────
|
|
51
|
+
// Reads exactly one line from stdin at process start. Never env, never argv.
|
|
52
|
+
// Resolves null (no admin secret; admin endpoints stay locked) if stdin is a
|
|
53
|
+
// TTY or closes without producing a line.
|
|
54
|
+
function _readAdminSecretFromStdin() {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
if (!process.stdin || process.stdin.isTTY) {
|
|
57
|
+
resolve(null);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
61
|
+
let resolved = false;
|
|
62
|
+
const finish = (value) => {
|
|
63
|
+
if (resolved) return;
|
|
64
|
+
resolved = true;
|
|
65
|
+
rl.close();
|
|
66
|
+
resolve(value);
|
|
67
|
+
};
|
|
68
|
+
rl.once('line', (line) => finish((line || '').trim() || null));
|
|
69
|
+
rl.once('close', () => finish(null));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* §9 S-5: record the exit, say it once, then leave. Semantics are unchanged -- nothing drains, and
|
|
75
|
+
* 128+15 keeps the supervisor's `code !== 0` classification of a SIGTERM exactly as it is today.
|
|
76
|
+
*/
|
|
77
|
+
function _installSigtermHandler({ startedContext, streamAccountingMod, instanceRecord, handoff }) {
|
|
78
|
+
process.on('SIGTERM', () => {
|
|
79
|
+
const ctx = startedContext();
|
|
80
|
+
if (!ctx) {
|
|
81
|
+
process.exit(SIGTERM_EXIT_CODE);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const openStreams = ctx.streamAccounting.count();
|
|
85
|
+
// §5 D-5: an explicit stop leaves no record, so the next boot spawns rather than adopts. A
|
|
86
|
+
// PREDECESSOR that is draining a handoff is the one exception: the record on disk already names its
|
|
87
|
+
// successor, and removing it would take the live proxy out of the next boot's reach.
|
|
88
|
+
if (!handoff.isDraining()) instanceRecord.unlinkInstanceRecord({ dataDir: ctx.dataDir });
|
|
89
|
+
streamAccountingMod.writeExitRecord({ dataDir: ctx.dataDir, reason: 'SIGTERM', openStreams })
|
|
90
|
+
.catch((err) => {
|
|
91
|
+
// REPORT: the exit code below still classifies this stop, so a failed write is not fatal.
|
|
92
|
+
console.error(`[model-proxy] exit record not written: ${(err && err.code) || 'unknown'}`);
|
|
93
|
+
})
|
|
94
|
+
.then(() => {
|
|
95
|
+
console.log(`[model-proxy] exit reason=SIGTERM open_streams=${openStreams}`);
|
|
96
|
+
process.exit(SIGTERM_EXIT_CODE);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The CLI entry point, called by oauth-proxy.js's `require.main === module` block with its own
|
|
103
|
+
* dependencies injected (so this module never requires oauth-proxy.js back).
|
|
104
|
+
*
|
|
105
|
+
* @param {object} input
|
|
106
|
+
* @param {Function} input.start oauth-proxy.js's start()
|
|
107
|
+
* @param {object} input.streamAccountingMod lib/model-proxy/stream-accounting.js
|
|
108
|
+
* @param {object} input.instanceRecord lib/model-proxy/proxy-instance-record.js
|
|
109
|
+
* @param {object} input.handoff lib/model-proxy/handoff.js
|
|
110
|
+
* @param {Function} [input.startedContext] getter for the context of the server this process started
|
|
111
|
+
* @param {string} [input.loopbackErrorCode] the named refusal code start() rejects with
|
|
112
|
+
*/
|
|
113
|
+
function run({
|
|
114
|
+
start,
|
|
115
|
+
streamAccountingMod,
|
|
116
|
+
instanceRecord,
|
|
117
|
+
handoff,
|
|
118
|
+
startedContext = () => null,
|
|
119
|
+
loopbackErrorCode = 'EBIND_NOT_LOOPBACK',
|
|
120
|
+
} = {}) {
|
|
121
|
+
const host = _resolveHost();
|
|
122
|
+
if (host !== LOOPBACK_HOST) {
|
|
123
|
+
// D-1: refuse before ever touching stdin, so a bad --host/env doesn't hang waiting for an admin
|
|
124
|
+
// secret that will never matter.
|
|
125
|
+
console.error(`[oauth-proxy] ${loopbackErrorCode}: refusing to bind to "${host}" — must be 127.0.0.1 (D-1)`);
|
|
126
|
+
process.exit(LOOPBACK_REFUSAL_EXIT_CODE);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const successor = typeof process.send === 'function';
|
|
130
|
+
const startup = successor
|
|
131
|
+
// §5 D-7: the successor never reads stdin -- its secret and its data directory arrive in the
|
|
132
|
+
// handoff message, and its listener is a socket it did not open.
|
|
133
|
+
? handoff.receiveInheritance().then((received) => ({
|
|
134
|
+
host,
|
|
135
|
+
adminSecret: received.inherited.adminSecret,
|
|
136
|
+
dataDir: received.inherited.dataDir,
|
|
137
|
+
inherited: { ...received.inherited, state: received.state, holdReady: _holdReadyFlag() },
|
|
138
|
+
}))
|
|
139
|
+
// A TTY, or stdin closing before a line arrived, is not a failure: the process simply serves with no
|
|
140
|
+
// admin secret, and its admin surface stays locked. The read itself already resolves null in both of
|
|
141
|
+
// those cases, so this tail exists only for an unexpected readline error.
|
|
142
|
+
: _readAdminSecretFromStdin()
|
|
143
|
+
.catch((/* expected: no line on stdin -- start with no admin secret rather than not at all */) => null)
|
|
144
|
+
.then((adminSecret) => ({ host, adminSecret }));
|
|
145
|
+
|
|
146
|
+
startup
|
|
147
|
+
// §5 D-8: `lingerMs` is what makes THIS process (and only this one: an embedded start() passes none)
|
|
148
|
+
// leave on its own once CTM has been gone for a full window.
|
|
149
|
+
.then((args) => start({ ...args, lingerMs: _lingerMsFlag(process.env, handoff) }))
|
|
150
|
+
.then(() => {
|
|
151
|
+
if (successor) {
|
|
152
|
+
// §5 D-7: the successor outlives the predecessor that spawned it, so the channel it was born
|
|
153
|
+
// with must never be what keeps it alive -- nor may its closure end it. `ready` is already sent.
|
|
154
|
+
try {
|
|
155
|
+
process.channel.unref();
|
|
156
|
+
} catch (err) {
|
|
157
|
+
// expected: nothing to detach when this process has no channel state left to manage.
|
|
158
|
+
void err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
_installSigtermHandler({ startedContext, streamAccountingMod, instanceRecord, handoff });
|
|
162
|
+
})
|
|
163
|
+
.catch((err) => {
|
|
164
|
+
if (err && err.code === loopbackErrorCode) {
|
|
165
|
+
console.error(`[oauth-proxy] ${err.code}: ${err.message}`);
|
|
166
|
+
process.exit(LOOPBACK_REFUSAL_EXIT_CODE);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
console.error('[oauth-proxy] failed to start:', err.message);
|
|
170
|
+
process.exit(STARTUP_FAILURE_EXIT_CODE);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The `require.main === module` entry: oauth-proxy.js calls exactly this, so its own CLI seams
|
|
176
|
+
* (start, the accounting/record modules, the SIGTERM context) do not have to be spelled out at the bottom
|
|
177
|
+
* of a file that is at its hard line ceiling. The require is deferred to call time, so loading THIS module
|
|
178
|
+
* never loads oauth-proxy.js back -- and when the proxy is its own main, this module is loaded from
|
|
179
|
+
* inside that same evaluation, by which point oauth-proxy.js's exports are complete.
|
|
180
|
+
*/
|
|
181
|
+
function runCli() {
|
|
182
|
+
const proxy = require('../../oauth-proxy.js');
|
|
183
|
+
return run({
|
|
184
|
+
start: proxy.start,
|
|
185
|
+
streamAccountingMod: require('./stream-accounting'),
|
|
186
|
+
instanceRecord: require('./proxy-instance-record'),
|
|
187
|
+
handoff: require('./handoff'),
|
|
188
|
+
startedContext: proxy._startedContext,
|
|
189
|
+
loopbackErrorCode: proxy.EBIND_NOT_LOOPBACK,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
run,
|
|
195
|
+
runCli,
|
|
196
|
+
_readAdminSecretFromStdin,
|
|
197
|
+
_holdReadyFlag,
|
|
198
|
+
_lingerMsFlag,
|
|
199
|
+
_resolveHost,
|
|
200
|
+
};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// §5 D-1/D-2/D-3 + §6 (docs/model-proxy-survives-ctm-restart-2026-10-22.html): the durable identity of a
|
|
4
|
+
// LIVE model proxy, and the four paths it lives on. The supervisor binds the child's stdout/stderr to
|
|
5
|
+
// proxyLogPath() (the proxy never writes into a CTM-owned pipe -- incident 7: an orphaned child dies on
|
|
6
|
+
// its second write with EPIPE); the child publishes instance.json after listen and removes it on an
|
|
7
|
+
// explicit stop; CTM writes the per-spawn admin secret BEFORE the spawn, so a CTM that dies between the
|
|
8
|
+
// spawn and its own exit still leaves a readable secret for the next boot -- which is also the CTM that
|
|
9
|
+
// adopts the survivor (§5 D-4). Both processes must agree on these paths byte-for-byte, so they are
|
|
10
|
+
// defined once, here.
|
|
11
|
+
//
|
|
12
|
+
// Publications are atomic in the shape lib/host-registry.js uses (a nonce-scoped tmp file created with
|
|
13
|
+
// flag 'wx' and mode 0600, renamed over the final path), and the directory is 0700 -- the same tree the
|
|
14
|
+
// native mTLS authority already lives in. The same 0600 secret authenticates loopback admin calls only;
|
|
15
|
+
// nothing here is a transport credential.
|
|
16
|
+
|
|
17
|
+
const crypto = require('node:crypto');
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const { execFileSync } = require('node:child_process');
|
|
21
|
+
const { computeClosure } = require('../host-closure-hash');
|
|
22
|
+
|
|
23
|
+
// lib/host-closure-hash.js resolves its own default repo root from ITS __dirname (coding-task-manager/lib
|
|
24
|
+
// -> the repo root). This module sits one level deeper, so the same root is re-stated explicitly: the
|
|
25
|
+
// file list the PROXY hashes and the file list CTM hashes from disk must name the same files.
|
|
26
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
27
|
+
|
|
28
|
+
const RECORD_VERSION = 1;
|
|
29
|
+
const PROXY_PROTOCOL = 'proxy-v1';
|
|
30
|
+
const PROXY_FLOOR_VERSION = 0;
|
|
31
|
+
const PROXY_SCRIPT_NAME = 'oauth-proxy.js';
|
|
32
|
+
// §5 D-1: one generation of log is kept. 203,900 request lines in 26 days is ~1.5 MB/day, so ~40 days
|
|
33
|
+
// per generation; the rotation is a rename, not a truncation, so the previous generation stays readable.
|
|
34
|
+
const LOG_ROTATE_BYTES = 64 * 1024 * 1024;
|
|
35
|
+
const LOG_ROTATE_SUFFIX = '.1';
|
|
36
|
+
const PS_TIMEOUT_MS = 2000;
|
|
37
|
+
|
|
38
|
+
function proxyDir(dataDir) {
|
|
39
|
+
return path.join(dataDir, 'oauth-proxy');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function instanceRecordPath(dataDir) {
|
|
43
|
+
return path.join(proxyDir(dataDir), 'instance.json');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function adminSecretPath(dataDir) {
|
|
47
|
+
return path.join(proxyDir(dataDir), 'admin-secret');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function proxyLogPath(dataDir) {
|
|
51
|
+
return path.join(proxyDir(dataDir), 'oauth-proxy.log');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 0700, created on demand. Synchronous because the spawn-time log descriptor below needs the directory to
|
|
55
|
+
// exist before the child does; one call per spawn and one per publication, never a request path.
|
|
56
|
+
function ensureProxyDir(dataDir) {
|
|
57
|
+
const dir = proxyDir(dataDir);
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _writeAtomic0600Sync(target, text) {
|
|
63
|
+
const tmp = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
64
|
+
fs.writeFileSync(tmp, text, { mode: 0o600, flag: 'wx' });
|
|
65
|
+
fs.renameSync(tmp, target);
|
|
66
|
+
return target;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function _unlinkQuiet(target) {
|
|
70
|
+
try {
|
|
71
|
+
fs.unlinkSync(target);
|
|
72
|
+
return true;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err && err.code === 'ENOENT') return false;
|
|
75
|
+
// REPORT: a file that cannot be removed is stale truth on disk; the caller's decision still stands.
|
|
76
|
+
process.stderr.write(`[model-proxy] record not removed: ${path.basename(target)} ${(err && err.code) || 'unknown'}\n`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* §5 D-2: the record the proxy publishes after listen. Written before the first request is answered, so
|
|
83
|
+
* a CTM that restarts immediately afterwards still finds the exact identity of what is listening.
|
|
84
|
+
* @returns {object} the record written
|
|
85
|
+
*/
|
|
86
|
+
function writeInstanceRecord({ dataDir, record }) {
|
|
87
|
+
ensureProxyDir(dataDir);
|
|
88
|
+
_writeAtomic0600Sync(instanceRecordPath(dataDir), `${JSON.stringify(record)}\n`);
|
|
89
|
+
return record;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* §5 D-3/D-2: compute this process's loaded-code hash ONCE, publish the record, and hand the same two
|
|
94
|
+
* fields to the accounting the /_health answer reports from -- so identity and the published record can
|
|
95
|
+
* never describe different code. §5 D-7 (WP-2): a handoff successor publishes `generation: n+1` and the
|
|
96
|
+
* predecessor it took over from, so the record names the LIVE socket owner and who handed it over.
|
|
97
|
+
*/
|
|
98
|
+
function publishInstanceRecord({ dataDir, port, nativePort = null, accounting = null, generation = 1, predecessorPid = null }) {
|
|
99
|
+
const { closureHash, closureFiles } = computeClosure({ repoRoot: REPO_ROOT });
|
|
100
|
+
const record = writeInstanceRecord({
|
|
101
|
+
dataDir,
|
|
102
|
+
record: {
|
|
103
|
+
v: RECORD_VERSION,
|
|
104
|
+
pid: process.pid,
|
|
105
|
+
port,
|
|
106
|
+
nativePort,
|
|
107
|
+
protocol: PROXY_PROTOCOL,
|
|
108
|
+
floorVersion: PROXY_FLOOR_VERSION,
|
|
109
|
+
closureHash,
|
|
110
|
+
closureFiles,
|
|
111
|
+
generation,
|
|
112
|
+
predecessorPid,
|
|
113
|
+
startedAt: new Date().toISOString(),
|
|
114
|
+
draining: false,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
if (accounting && typeof accounting.setIdentity === 'function') {
|
|
118
|
+
accounting.setIdentity({ closureHash, closureFiles, generation, draining: false });
|
|
119
|
+
}
|
|
120
|
+
return record;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* §5 D-7 step 2: rewrite the published record in place with one patch (today: `draining`). Written
|
|
125
|
+
* BEFORE the successor is spawned, so a predecessor that dies mid-handoff leaves a truthful record --
|
|
126
|
+
* and rewritten with `draining:false` on any abort, because the predecessor kept serving.
|
|
127
|
+
* @returns {object|null} the rewritten record, or null when this process published none
|
|
128
|
+
*/
|
|
129
|
+
function updateInstanceRecord({ dataDir, patch }) {
|
|
130
|
+
const current = readInstanceRecord({ dataDir });
|
|
131
|
+
if (!current) return null;
|
|
132
|
+
return writeInstanceRecord({ dataDir, record: { ...current, ...patch } });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @returns {object|null} the published record, or null when there is none. A truncated, hand-edited or
|
|
137
|
+
* unusable record is reported as absent AND removed: writes are atomic via rename, so it can only be
|
|
138
|
+
* leftover garbage, and leaving it behind would make the next boot re-read the same lie.
|
|
139
|
+
*/
|
|
140
|
+
function readInstanceRecord({ dataDir }) {
|
|
141
|
+
let raw;
|
|
142
|
+
try {
|
|
143
|
+
raw = fs.readFileSync(instanceRecordPath(dataDir), 'utf8');
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err && err.code === 'ENOENT') return null; // expected: no proxy has published one
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const record = JSON.parse(raw);
|
|
150
|
+
if (record && typeof record === 'object' && Number.isSafeInteger(record.pid) && record.pid > 0) return record;
|
|
151
|
+
} catch {
|
|
152
|
+
// expected: an unparseable record is not evidence -- fall through and remove it
|
|
153
|
+
}
|
|
154
|
+
unlinkInstanceRecord({ dataDir });
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function unlinkInstanceRecord({ dataDir }) {
|
|
159
|
+
return _unlinkQuiet(instanceRecordPath(dataDir));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** §5 D-2: written before each fresh spawn, read back when CTM adopts a proxy it did not spawn. */
|
|
163
|
+
function writeAdminSecret({ dataDir, secret }) {
|
|
164
|
+
ensureProxyDir(dataDir);
|
|
165
|
+
_writeAtomic0600Sync(adminSecretPath(dataDir), `${String(secret)}\n`);
|
|
166
|
+
return secret;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** @returns {string|null} the current secret, or null when there is none to authenticate with. */
|
|
170
|
+
function readAdminSecret({ dataDir }) {
|
|
171
|
+
let raw;
|
|
172
|
+
try {
|
|
173
|
+
raw = fs.readFileSync(adminSecretPath(dataDir), 'utf8');
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err && err.code === 'ENOENT') return null; // expected: this CTM never spawned a proxy here
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
const secret = raw.trim();
|
|
179
|
+
return secret || null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function unlinkAdminSecret({ dataDir }) {
|
|
183
|
+
return _unlinkQuiet(adminSecretPath(dataDir));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* §5 D-1: the descriptor the child's stdout and stderr are bound to. One synchronous open per spawn
|
|
188
|
+
* (never a request path), rotating one generation at 64 MiB first: every proxy generation passes through
|
|
189
|
+
* this spawn, including WP-2's handoff successor, so this is the one place rotation has to happen.
|
|
190
|
+
*/
|
|
191
|
+
function openProxyLogFd(dataDir) {
|
|
192
|
+
ensureProxyDir(dataDir);
|
|
193
|
+
const target = proxyLogPath(dataDir);
|
|
194
|
+
const fd = fs.openSync(target, 'a');
|
|
195
|
+
if (fs.fstatSync(fd).size <= LOG_ROTATE_BYTES) return fd;
|
|
196
|
+
fs.closeSync(fd);
|
|
197
|
+
fs.renameSync(target, `${target}${LOG_ROTATE_SUFFIX}`);
|
|
198
|
+
return fs.openSync(target, 'a');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The supervisor's own copy of the descriptor; the child holds its own. */
|
|
202
|
+
function closeProxyLogFd(fd) {
|
|
203
|
+
try {
|
|
204
|
+
fs.closeSync(fd);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
// expected: a descriptor that is already closed costs nothing -- the child has its own copy
|
|
207
|
+
void err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** §5 D-4: is this pid a live proxy? `ps` is the authority -- a recycled pid must never be signalled. */
|
|
212
|
+
function isProxyProcess(pid) {
|
|
213
|
+
if (!Number.isSafeInteger(pid) || pid < 1) return false;
|
|
214
|
+
try {
|
|
215
|
+
return execFileSync('ps', ['-o', 'command=', '-p', String(pid)], { encoding: 'utf8', timeout: PS_TIMEOUT_MS })
|
|
216
|
+
.includes(PROXY_SCRIPT_NAME);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
// expected: ps exits non-zero for a pid that is gone, and a failed probe is never a proxy
|
|
219
|
+
void err;
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** D-4 step 1: `process.kill(pid, 0)` -- EPERM means alive but not ours, which is still alive. */
|
|
225
|
+
function isProcessAlive(pid) {
|
|
226
|
+
if (!Number.isSafeInteger(pid) || pid < 1) return false;
|
|
227
|
+
try {
|
|
228
|
+
process.kill(pid, 0);
|
|
229
|
+
return true;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return err && err.code === 'EPERM';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
RECORD_VERSION,
|
|
237
|
+
PROXY_PROTOCOL,
|
|
238
|
+
PROXY_FLOOR_VERSION,
|
|
239
|
+
LOG_ROTATE_BYTES,
|
|
240
|
+
proxyDir,
|
|
241
|
+
instanceRecordPath,
|
|
242
|
+
adminSecretPath,
|
|
243
|
+
proxyLogPath,
|
|
244
|
+
ensureProxyDir,
|
|
245
|
+
publishInstanceRecord,
|
|
246
|
+
updateInstanceRecord,
|
|
247
|
+
writeInstanceRecord,
|
|
248
|
+
readInstanceRecord,
|
|
249
|
+
unlinkInstanceRecord,
|
|
250
|
+
writeAdminSecret,
|
|
251
|
+
readAdminSecret,
|
|
252
|
+
unlinkAdminSecret,
|
|
253
|
+
openProxyLogFd,
|
|
254
|
+
closeProxyLogFd,
|
|
255
|
+
isProxyProcess,
|
|
256
|
+
isProcessAlive,
|
|
257
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// §5 D-9 / §9 S-5, S-8: how many requests this proxy is still carrying, and what it was carrying when
|
|
4
|
+
// it last stopped. Both truths live here on purpose -- the exit record is nothing but the counter read
|
|
5
|
+
// at exit, and a second copy of that number is exactly how the two would drift apart.
|
|
6
|
+
|
|
7
|
+
const fsp = require('node:fs/promises');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
|
|
10
|
+
// The health probe is itself a request, so counting it would make the counter unobservable from the one
|
|
11
|
+
// surface that reports it (the probe would always see its own 1). Only these two status paths are
|
|
12
|
+
// exempt; every other path is counted, keep-alive or not.
|
|
13
|
+
const UNCOUNTED_PATHS = new Set(['/_health', '/_status']);
|
|
14
|
+
|
|
15
|
+
const EXIT_RECORD_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
function _pathOf(url) {
|
|
18
|
+
const raw = String(url || '');
|
|
19
|
+
const cut = raw.indexOf('?');
|
|
20
|
+
return cut === -1 ? raw : raw.slice(0, cut);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The process's own in-flight stream counter.
|
|
25
|
+
*
|
|
26
|
+
* @param {{startedAt?: string}} [options] `startedAt` is overridable only so a test can pin it.
|
|
27
|
+
*/
|
|
28
|
+
function createStreamAccounting({ startedAt = new Date().toISOString() } = {}) {
|
|
29
|
+
let openStreams = 0;
|
|
30
|
+
// §5 D-3: the loaded-code hash this process published, set ONCE after listen by
|
|
31
|
+
// proxy-instance-record.publishInstanceRecord(). Read here so the /_health answer and the on-disk
|
|
32
|
+
// record can never describe different code; null until then (a start that never binds publishes none).
|
|
33
|
+
// §5 D-7 (WP-2): `generation` and `draining` travel the same way -- the successor of a handoff is
|
|
34
|
+
// generation N+1 and says so on /_health while its predecessor is still draining.
|
|
35
|
+
let identity = null;
|
|
36
|
+
// §5 D-7 step 7: the drain ends when this counter reaches zero, so the DECREMENT is what notifies --
|
|
37
|
+
// there is no polling loop and no second copy of the count.
|
|
38
|
+
const idleWaiters = new Set();
|
|
39
|
+
|
|
40
|
+
function _notifyIdle() {
|
|
41
|
+
const waiters = [...idleWaiters];
|
|
42
|
+
idleWaiters.clear();
|
|
43
|
+
for (const waiter of waiters) {
|
|
44
|
+
try {
|
|
45
|
+
waiter();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// REPORT: a drain waiter that throws is not the counter's problem; the drain deadline owns the
|
|
48
|
+
// fallback, so the failure is worth one line and nothing else.
|
|
49
|
+
process.stderr.write(`[model-proxy] drain waiter failed: ${(err && err.message) || 'unknown'}\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
/**
|
|
56
|
+
* Count every request this server is still answering. An idle keep-alive SOCKET carries no request
|
|
57
|
+
* and is never counted; a request is in flight until its response emits 'close' (whether it
|
|
58
|
+
* completed, errored or was cut by the client).
|
|
59
|
+
*/
|
|
60
|
+
track(server) {
|
|
61
|
+
server.on('request', (req, res) => {
|
|
62
|
+
if (UNCOUNTED_PATHS.has(_pathOf(req.url))) return;
|
|
63
|
+
openStreams += 1;
|
|
64
|
+
res.once('close', () => {
|
|
65
|
+
openStreams -= 1;
|
|
66
|
+
if (openStreams === 0) _notifyIdle();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
count() {
|
|
71
|
+
return openStreams;
|
|
72
|
+
},
|
|
73
|
+
snapshot() {
|
|
74
|
+
return { openStreams, startedAt, identity };
|
|
75
|
+
},
|
|
76
|
+
/** §5 D-3: the code this process loaded, published once; what /_health reports from. */
|
|
77
|
+
setIdentity(next) {
|
|
78
|
+
identity = next
|
|
79
|
+
? {
|
|
80
|
+
closureHash: next.closureHash || null,
|
|
81
|
+
closureFiles: Array.isArray(next.closureFiles) ? next.closureFiles : [],
|
|
82
|
+
generation: Number.isSafeInteger(next.generation) && next.generation > 0 ? next.generation : 1,
|
|
83
|
+
draining: next.draining === true,
|
|
84
|
+
}
|
|
85
|
+
: null;
|
|
86
|
+
},
|
|
87
|
+
/** §5 D-7 step 8: the predecessor answers /_health with draining:true from here until it exits. */
|
|
88
|
+
setDraining(value) {
|
|
89
|
+
const current = identity || { closureHash: null, closureFiles: [], generation: 1, draining: false };
|
|
90
|
+
identity = { ...current, draining: value === true };
|
|
91
|
+
},
|
|
92
|
+
/**
|
|
93
|
+
* §5 D-7 step 7: called ONCE by the handoff drain, on the decrement that brings the count to zero.
|
|
94
|
+
* @returns {Function} unsubscribe, so a drain that was already forced does not hold a callback
|
|
95
|
+
*/
|
|
96
|
+
onIdle(callback) {
|
|
97
|
+
idleWaiters.add(callback);
|
|
98
|
+
return () => idleWaiters.delete(callback);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* §9 S-8: the process-identity fields the `/_health` answer reports alongside its existing ones. The
|
|
105
|
+
* count is read live, never cached, so the answer can never describe an earlier moment. `generation`
|
|
106
|
+
* and `draining` come from the published identity (a handoff successor is generation N+1 and reports
|
|
107
|
+
* `draining:true` while its predecessor drains); a process that published none reports today's
|
|
108
|
+
* 1/false.
|
|
109
|
+
*/
|
|
110
|
+
function healthFields(accounting) {
|
|
111
|
+
const snapshot = accounting.snapshot();
|
|
112
|
+
const identity = snapshot.identity || {};
|
|
113
|
+
return {
|
|
114
|
+
pid: process.pid,
|
|
115
|
+
generation: identity.generation || 1,
|
|
116
|
+
protocol: 'proxy-v1',
|
|
117
|
+
floor_version: 0,
|
|
118
|
+
open_streams: snapshot.openStreams,
|
|
119
|
+
draining: identity.draining === true,
|
|
120
|
+
started_at: snapshot.startedAt,
|
|
121
|
+
// §5 D-3: what CTM compares against its own disk hash to decide adopt vs hand off.
|
|
122
|
+
closure_hash: identity.closureHash || null,
|
|
123
|
+
closure_files: identity.closureFiles || [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* §9 S-8: the health fields merged into whatever JSON body the responder writes. The relay keeps
|
|
129
|
+
* ownership of its own five fields (unchanged, names and types); this only adds what D-9/S-8 own, and a
|
|
130
|
+
* non-JSON end (there is none today) is passed through untouched rather than guessed at.
|
|
131
|
+
*/
|
|
132
|
+
function withHealthFields(res, accounting) {
|
|
133
|
+
const end = res.end.bind(res);
|
|
134
|
+
res.end = (chunk, ...rest) => {
|
|
135
|
+
let body;
|
|
136
|
+
try {
|
|
137
|
+
body = JSON.parse(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
|
|
138
|
+
} catch {
|
|
139
|
+
return end(chunk, ...rest); // expected: only the health JSON body is augmented
|
|
140
|
+
}
|
|
141
|
+
return end(JSON.stringify({ ...body, ...healthFields(accounting) }), ...rest);
|
|
142
|
+
};
|
|
143
|
+
return res;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function exitRecordPath(dataDir) {
|
|
147
|
+
return path.join(dataDir, 'oauth-proxy', 'last-exit.json');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* §9 S-5: the previous exit, written once. Atomic (tmp + rename) and owner-only, so a concurrent reader
|
|
152
|
+
* sees either the whole record or no record — never a half-written one. Async like the budget journals
|
|
153
|
+
* beside it (request-budget-store.js): the proxy awaits the write before it exits, and the supervisor
|
|
154
|
+
* reads it off the main thread.
|
|
155
|
+
* @returns {Promise<object>} the record written
|
|
156
|
+
*/
|
|
157
|
+
async function writeExitRecord({ dataDir, reason, openStreams }) {
|
|
158
|
+
const record = { v: EXIT_RECORD_VERSION, at: new Date().toISOString(), pid: process.pid, reason, openStreams };
|
|
159
|
+
const target = exitRecordPath(dataDir);
|
|
160
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
161
|
+
await fsp.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
162
|
+
await fsp.writeFile(tmp, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
163
|
+
await fsp.rename(tmp, target);
|
|
164
|
+
return record;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @returns {Promise<object|null>} the recorded previous exit, or null when there is none to report. A
|
|
169
|
+
* truncated or hand-edited record is reported as absent rather than thrown: it is evidence of nothing.
|
|
170
|
+
*/
|
|
171
|
+
async function readExitRecord({ dataDir }) {
|
|
172
|
+
let raw;
|
|
173
|
+
try {
|
|
174
|
+
raw = await fsp.readFile(exitRecordPath(dataDir), 'utf8');
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err && err.code === 'ENOENT') return null; // expected: no previous exit was recorded
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const record = JSON.parse(raw);
|
|
181
|
+
return record && typeof record === 'object' ? record : null;
|
|
182
|
+
} catch {
|
|
183
|
+
return null; // expected: an unparseable record is not evidence; report it as absent
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One exit is reported once: the supervisor reads the record, then clears it. */
|
|
188
|
+
async function clearExitRecord({ dataDir }) {
|
|
189
|
+
try {
|
|
190
|
+
await fsp.rm(exitRecordPath(dataDir), { force: true });
|
|
191
|
+
} catch (err) {
|
|
192
|
+
// REPORT: a record that cannot be removed is stale truth on disk, which is worth a line.
|
|
193
|
+
console.error(`[model-proxy] exit record not removed: ${(err && err.code) || 'unknown'}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
createStreamAccounting,
|
|
199
|
+
healthFields,
|
|
200
|
+
withHealthFields,
|
|
201
|
+
writeExitRecord,
|
|
202
|
+
readExitRecord,
|
|
203
|
+
clearExitRecord,
|
|
204
|
+
exitRecordPath,
|
|
205
|
+
};
|