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
|
@@ -5,11 +5,13 @@ const crypto = require('node:crypto');
|
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const runtimeHealth = require('../lib/runtime-health');
|
|
8
|
+
const errorSink = require('../lib/error-sink');
|
|
8
9
|
const { redactSecrets } = require('../llm/provider-error');
|
|
9
10
|
const { redactPositionalSecretArray, redactSecretText } = require('../lib/secret-redaction');
|
|
10
11
|
const { defaultSessionsDir } = require('./workspace-replay');
|
|
11
12
|
const { canonicalRepoKey } = require('./repo-identity');
|
|
12
13
|
const transcriptSource = require('../../coding-task-manager/lib/transcript-source');
|
|
14
|
+
const { selectJournalDispatcher } = require('./journal-dispatcher');
|
|
13
15
|
|
|
14
16
|
const MAX_BODY_CHARS = 1500;
|
|
15
17
|
const MAX_SEGMENT_CHARS = 4000;
|
|
@@ -311,38 +313,9 @@ async function handleDurableFailure(kind, entry, error, options = {}, startedAt
|
|
|
311
313
|
}
|
|
312
314
|
|
|
313
315
|
function recoveryJournalDispatcher(options = {}) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (shared) return createJournalWriteDispatcher({ client: shared });
|
|
318
|
-
// Stop hooks and standalone lifecycle processes cannot see the daemon's
|
|
319
|
-
// process-local singleton. The bounded one-shot child owns SQLite and keeps
|
|
320
|
-
// this process free from better-sqlite3 teardown hazards.
|
|
321
|
-
const { writeJournalEntryInChild } = require('../lib/coding-journal-writer-child-client');
|
|
322
|
-
return createJournalWriteDispatcher({
|
|
323
|
-
client: {
|
|
324
|
-
persistCodingJournal(entry) {
|
|
325
|
-
const dbPath = options.dbPath || defaultBrainDbPath(options.env);
|
|
326
|
-
// J-1: no daemon is actually serving a brain (no explicit dbPath, no
|
|
327
|
-
// WALL_E_DATA_DIR, no live-data instance-id.json) — never spawn the
|
|
328
|
-
// one-shot child, which holds an owner grant and would create/migrate
|
|
329
|
-
// the phantom ~/.walle/data brain. Write nothing; skip loudly.
|
|
330
|
-
if (!dbPath) {
|
|
331
|
-
console.error('[wall-e] coding journal write skipped: reason=no_served_brain (no daemon-served brain to write to)');
|
|
332
|
-
return Promise.resolve({ ok: true, skipped: true, reason: 'no_served_brain' });
|
|
333
|
-
}
|
|
334
|
-
return writeJournalEntryInChild(entry, {
|
|
335
|
-
dbPath,
|
|
336
|
-
// S-8: state THIS process's schema generation so the fresh-code child
|
|
337
|
-
// refuses rather than migrating the brain out from under us.
|
|
338
|
-
parentSchemaVersion: require('../brain').SCHEMA_VERSION,
|
|
339
|
-
timeoutMs: options.timeoutMs,
|
|
340
|
-
forkImpl: options.forkImpl,
|
|
341
|
-
env: options.env,
|
|
342
|
-
});
|
|
343
|
-
},
|
|
344
|
-
},
|
|
345
|
-
});
|
|
316
|
+
return selectJournalDispatcher(options, {
|
|
317
|
+
createJournalWriteDispatcher, defaultBrainDbPath,
|
|
318
|
+
}).dispatcher;
|
|
346
319
|
}
|
|
347
320
|
|
|
348
321
|
async function readJournalRetryMarker(markerPath) {
|
|
@@ -1001,7 +974,11 @@ function dispatchOffThread(kind, dispatcher, prepare, options = {}) {
|
|
|
1001
974
|
narrativeOp.end({ ok: true, meta: { applied } });
|
|
1002
975
|
} catch (error) {
|
|
1003
976
|
narrativeOp.end({ ok: false, error });
|
|
1004
|
-
|
|
977
|
+
try {
|
|
978
|
+
options.logger?.warn?.(`[coding-journal] narrative polisher failed open: ${redactJournalString(error?.message || error, 240)}`);
|
|
979
|
+
} catch (warningError) {
|
|
980
|
+
errorSink.report('coding-journal', 'narrative-warning', warningError);
|
|
981
|
+
}
|
|
1005
982
|
}
|
|
1006
983
|
}
|
|
1007
984
|
|
|
@@ -1251,14 +1228,24 @@ function journalInputForRun({
|
|
|
1251
1228
|
});
|
|
1252
1229
|
}
|
|
1253
1230
|
|
|
1254
|
-
function
|
|
1231
|
+
function startRunJournal(input = {}, options = {}) {
|
|
1255
1232
|
const timestamp = String(input.timestamp || new Date().toISOString());
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1233
|
+
const selection = selectJournalDispatcher(options, {
|
|
1234
|
+
createJournalWriteDispatcher, defaultBrainDbPath,
|
|
1235
|
+
});
|
|
1236
|
+
return Object.freeze({
|
|
1237
|
+
completion: dispatchOffThread(
|
|
1238
|
+
'entry',
|
|
1239
|
+
selection.dispatcher,
|
|
1240
|
+
() => buildJournalEntry({ ...journalInputForRun(input), timestamp }),
|
|
1241
|
+
options,
|
|
1242
|
+
),
|
|
1243
|
+
defaultAwaitTerminalJournal: selection.defaultAwaitTerminalJournal,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function scheduleRunJournal(input = {}, options = {}) {
|
|
1248
|
+
return startRunJournal(input, options).completion;
|
|
1262
1249
|
}
|
|
1263
1250
|
|
|
1264
1251
|
function transcriptText(record = {}) {
|
|
@@ -1577,6 +1564,7 @@ module.exports = {
|
|
|
1577
1564
|
persistJournalEntry,
|
|
1578
1565
|
persistJournalSegment,
|
|
1579
1566
|
scheduleJournalEntry,
|
|
1567
|
+
startRunJournal,
|
|
1580
1568
|
scheduleRunJournal,
|
|
1581
1569
|
scheduleJournalSegment,
|
|
1582
1570
|
extractDecisionContent,
|
|
@@ -3393,18 +3393,13 @@ async function runAgentLoopUnaccounted(prompt, opts = {}) {
|
|
|
3393
3393
|
// that stable session when the caller omitted runId.
|
|
3394
3394
|
const runId = String(opts.runId || crypto.randomUUID());
|
|
3395
3395
|
const sessionId = String(opts.chatSessionId || opts.session_id || runId);
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
awaitTerminalJournal = Boolean(opts.journalDispatcher) || !getSharedBrainOwnerClient();
|
|
3404
|
-
} catch {
|
|
3405
|
-
awaitTerminalJournal = true;
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3396
|
+
// The wait policy belongs to the dispatcher actually selected at the terminal
|
|
3397
|
+
// boundary, which is why this is only the caller's explicit override, read
|
|
3398
|
+
// once at run start (see flushTerminalJournal). A daemon turn enqueues to the
|
|
3399
|
+
// already-running owner and returns without gating user-visible completion on
|
|
3400
|
+
// SQLite; a standalone/CLI run owns its process lifetime and drains the
|
|
3401
|
+
// one-shot writer before exiting.
|
|
3402
|
+
const explicitJournalAwait = opts.awaitTerminalJournal;
|
|
3408
3403
|
const cwd = await realpathBestEffort(opts.cwd || process.cwd());
|
|
3409
3404
|
let repoKey = String(opts.repoKey || '');
|
|
3410
3405
|
if (!repoKey) {
|
|
@@ -3428,10 +3423,10 @@ async function runAgentLoopUnaccounted(prompt, opts = {}) {
|
|
|
3428
3423
|
opts.journalNarrativeLogger?.warn?.(`[coding-journal] narrative setup skipped: ${error?.message || String(error)}`);
|
|
3429
3424
|
}
|
|
3430
3425
|
}
|
|
3431
|
-
let
|
|
3426
|
+
let terminalJournalOperation = null;
|
|
3432
3427
|
const scheduleTerminalJournal = ({ result = null, error = null } = {}) => {
|
|
3433
|
-
const {
|
|
3434
|
-
|
|
3428
|
+
const { startRunJournal } = require('./coding/session-journal');
|
|
3429
|
+
terminalJournalOperation = startRunJournal({
|
|
3435
3430
|
runId,
|
|
3436
3431
|
sessionId,
|
|
3437
3432
|
repoKey,
|
|
@@ -3452,20 +3447,22 @@ async function runAgentLoopUnaccounted(prompt, opts = {}) {
|
|
|
3452
3447
|
});
|
|
3453
3448
|
};
|
|
3454
3449
|
const flushTerminalJournal = async () => {
|
|
3455
|
-
if (!
|
|
3456
|
-
const
|
|
3457
|
-
|
|
3450
|
+
if (!terminalJournalOperation) return;
|
|
3451
|
+
const shouldAwait = explicitJournalAwait == null
|
|
3452
|
+
? terminalJournalOperation.defaultAwaitTerminalJournal !== false
|
|
3453
|
+
: explicitJournalAwait;
|
|
3454
|
+
if (!shouldAwait) return;
|
|
3455
|
+
// The awaited caller joins the journal scheduler's own promise, so the
|
|
3456
|
+
// dispatch enqueue and the scheduler's durable-failure handling finish before
|
|
3457
|
+
// the run boundary returns; when the selected dispatcher is the native
|
|
3458
|
+
// writer, that same promise also covers its real child close. A caller-side
|
|
3459
|
+
// race would abandon that ownership while the write is still in flight. A
|
|
3460
|
+
// custom dispatcher proves only what its own promise proves: its resolved
|
|
3461
|
+
// Promise is not physical-effect evidence, and no timer can supply one.
|
|
3458
3462
|
try {
|
|
3459
|
-
await
|
|
3460
|
-
terminalJournalPromise,
|
|
3461
|
-
new Promise((resolve) => {
|
|
3462
|
-
timer = setTimeout(() => resolve({ ok: false, timedOut: true }), timeoutMs);
|
|
3463
|
-
}),
|
|
3464
|
-
]);
|
|
3463
|
+
await terminalJournalOperation.completion;
|
|
3465
3464
|
} catch (error) {
|
|
3466
3465
|
console.warn?.(`[coding-journal] terminal flush failed: ${error.message}`);
|
|
3467
|
-
} finally {
|
|
3468
|
-
if (timer) clearTimeout(timer);
|
|
3469
3466
|
}
|
|
3470
3467
|
};
|
|
3471
3468
|
let actionMemoryWriter = opts.actionMemoryWriter || null;
|
|
@@ -8,6 +8,11 @@ const PUBLIC_ROUTES = new Set([
|
|
|
8
8
|
'/api/wall-e/health',
|
|
9
9
|
'/api/wall-e/webhook',
|
|
10
10
|
'/api/wall-e/feedback/submit',
|
|
11
|
+
// WP-1 (blog-view-counts): the public aggregate counter for the three static
|
|
12
|
+
// articles. Public at this outer gate ONLY — the handler itself accepts a POST
|
|
13
|
+
// solely from the two published site origins (getBlogViewOrigin), and its
|
|
14
|
+
// writes are capped by the store's receipt bound.
|
|
15
|
+
'/api/wall-e/telemetry/blog-views',
|
|
11
16
|
'/api/wall-e/telemetry/ingest',
|
|
12
17
|
'/api/wall-e/telemetry/summary',
|
|
13
18
|
'/api/wall-e/telemetry/storage',
|
|
@@ -17,6 +22,7 @@ const PUBLIC_ROUTES = new Set([
|
|
|
17
22
|
]);
|
|
18
23
|
|
|
19
24
|
const { ctmPort: resolveCtmPort } = require('../lib/ctm-endpoint');
|
|
25
|
+
const { getBlogViewOrigin, isBlogViewsPath } = require('../lib/blog-view-policy');
|
|
20
26
|
// WP-7: request provenance comes from the VENDORED standalone copy of the
|
|
21
27
|
// canonical shared module (byte-identical sibling) — the standalone product
|
|
22
28
|
// never imports the CTM checkout.
|
|
@@ -24,8 +30,19 @@ const { inspectRequestOrigin, isLoopbackRequest } = require('../shared/request-p
|
|
|
24
30
|
|
|
25
31
|
function getAllowedOrigin(req) {
|
|
26
32
|
const ctmPort = process.env.CTM_PORT || resolveCtmPort();
|
|
27
|
-
if (process.env.CTM_ORIGIN) return process.env.CTM_ORIGIN;
|
|
28
33
|
const origin = req?.headers?.origin;
|
|
34
|
+
// WP-1 (blog-view-counts, D-4): ROUTE-SCOPED exception for the public blog
|
|
35
|
+
// counter only. The static site is served from walle.sh, so its two exact
|
|
36
|
+
// origins are reflected for this one path — walle.sh is NEVER added to the
|
|
37
|
+
// private routes' set (which stays the local CTM origin), and an unrecognized
|
|
38
|
+
// origin still falls through to the existing local answer.
|
|
39
|
+
//
|
|
40
|
+
// Deliberately FIRST, ahead of the CTM_ORIGIN override below: a private
|
|
41
|
+
// deployment's custom origin setting must never be able to break this exact
|
|
42
|
+
// public route's two published site origins. Every other route still answers
|
|
43
|
+
// with CTM_ORIGIN / the local set exactly as before.
|
|
44
|
+
if (isBlogViewsPath(String(req?.url || '').split('?')[0]) && getBlogViewOrigin(origin)) return origin;
|
|
45
|
+
if (process.env.CTM_ORIGIN) return process.env.CTM_ORIGIN;
|
|
29
46
|
const allowed = new Set([
|
|
30
47
|
`http://127.0.0.1:${ctmPort}`,
|
|
31
48
|
`http://localhost:${ctmPort}`,
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WP-1 (docs/blog-view-counts-2026-10-22.html §5 D-4/D-5, §6): the ONE public,
|
|
4
|
+
// unauthenticated blog-view counter endpoint (GET totals for the index, POST one
|
|
5
|
+
// visible article opening) at the exact path the policy module names —
|
|
6
|
+
// /api/wall-e/telemetry/blog-views, which api-walle.js already routes into the
|
|
7
|
+
// isolated telemetry process in the daemon role. Dispatched from
|
|
8
|
+
// http/telemetry-api.js with
|
|
9
|
+
// {getTelemetryDb, errorSink} — the same deps the telemetry routes already get —
|
|
10
|
+
// and deliberately narrow: only the exact path, no query parameters, no cookies,
|
|
11
|
+
// no Set-Cookie, no CORS header of its own (server.js reflects the route-scoped
|
|
12
|
+
// origin from http/auth.js), and never an exception detail in a response body.
|
|
13
|
+
//
|
|
14
|
+
// Bounded by construction: the 1024-byte body cap, a separate in-memory per-IP
|
|
15
|
+
// POST limiter (60/min, 10000 buckets, lazily pruned) and the store's receipt
|
|
16
|
+
// cap. The client's raw address is only ever a limiter key in this process's
|
|
17
|
+
// memory — never persisted, never logged, never forwarded to the error sink.
|
|
18
|
+
//
|
|
19
|
+
// Test seams (no environment flag): callers may inject `now` and a `rateLimiter`
|
|
20
|
+
// built by createBlogViewRateLimiter() for deterministic windows.
|
|
21
|
+
|
|
22
|
+
const net = require('node:net');
|
|
23
|
+
const { readBody } = require('./api-utils');
|
|
24
|
+
const {
|
|
25
|
+
BLOG_VIEW_MAX_BODY_BYTES,
|
|
26
|
+
getBlogViewOrigin,
|
|
27
|
+
isBlogPostSlug,
|
|
28
|
+
isBlogViewsPath,
|
|
29
|
+
isBlogViewToken,
|
|
30
|
+
isKnownBotUserAgent,
|
|
31
|
+
} = require('../lib/blog-view-policy');
|
|
32
|
+
const {
|
|
33
|
+
BLOG_VIEW_CAPACITY,
|
|
34
|
+
BLOG_VIEW_OVERFLOW,
|
|
35
|
+
getBlogViewCounts,
|
|
36
|
+
recordBlogView,
|
|
37
|
+
} = require('../lib/blog-view-store');
|
|
38
|
+
|
|
39
|
+
const BLOG_VIEW_RATE_MAX_PER_WINDOW = 60;
|
|
40
|
+
const BLOG_VIEW_RATE_WINDOW_MS = 60000;
|
|
41
|
+
const BLOG_VIEW_RATE_MAX_BUCKETS = 10000;
|
|
42
|
+
const UNKNOWN_CLIENT_KEY = 'unknown';
|
|
43
|
+
|
|
44
|
+
// Bounded fixed-window limiter. check(key, nowMs) never allocates for a key it
|
|
45
|
+
// refuses, and a full bucket map refuses the NEW key instead of evicting live
|
|
46
|
+
// counters (a flood must not be able to reset everyone's window). Expired buckets
|
|
47
|
+
// are pruned lazily, at most once per window, so no timer is needed.
|
|
48
|
+
function createBlogViewRateLimiter(options = {}) {
|
|
49
|
+
const maxPerWindow = Number.isInteger(options.maxPerWindow) ? options.maxPerWindow : BLOG_VIEW_RATE_MAX_PER_WINDOW;
|
|
50
|
+
const windowMs = Number.isInteger(options.windowMs) ? options.windowMs : BLOG_VIEW_RATE_WINDOW_MS;
|
|
51
|
+
const maxBuckets = Number.isInteger(options.maxBuckets) ? options.maxBuckets : BLOG_VIEW_RATE_MAX_BUCKETS;
|
|
52
|
+
const buckets = new Map();
|
|
53
|
+
let lastPruneMs = 0;
|
|
54
|
+
|
|
55
|
+
function prune(nowMs) {
|
|
56
|
+
if (nowMs - lastPruneMs < windowMs) return;
|
|
57
|
+
lastPruneMs = nowMs;
|
|
58
|
+
for (const [key, bucket] of buckets) {
|
|
59
|
+
if (bucket.startMs + windowMs <= nowMs) buckets.delete(key);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function check(key, nowMs) {
|
|
64
|
+
const now = Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
65
|
+
prune(now);
|
|
66
|
+
const existing = buckets.get(key);
|
|
67
|
+
if (!existing) {
|
|
68
|
+
if (buckets.size >= maxBuckets) {
|
|
69
|
+
return { ok: false, retryAfterSeconds: Math.max(1, Math.ceil(windowMs / 1000)) };
|
|
70
|
+
}
|
|
71
|
+
buckets.set(key, { startMs: now, count: 1 });
|
|
72
|
+
return { ok: true, retryAfterSeconds: 0 };
|
|
73
|
+
}
|
|
74
|
+
if (now - existing.startMs >= windowMs) {
|
|
75
|
+
existing.startMs = now;
|
|
76
|
+
existing.count = 1;
|
|
77
|
+
return { ok: true, retryAfterSeconds: 0 };
|
|
78
|
+
}
|
|
79
|
+
existing.count += 1;
|
|
80
|
+
if (existing.count > maxPerWindow) {
|
|
81
|
+
const remainingMs = existing.startMs + windowMs - now;
|
|
82
|
+
return { ok: false, retryAfterSeconds: Math.max(1, Math.ceil(remainingMs / 1000)) };
|
|
83
|
+
}
|
|
84
|
+
return { ok: true, retryAfterSeconds: 0 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
check,
|
|
89
|
+
size: () => buckets.size,
|
|
90
|
+
reset: () => { buckets.clear(); lastPruneMs = 0; },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The process-wide limiter the API dispatch path uses. Exported so a test can
|
|
95
|
+
// reset or exercise it without reaching into module state.
|
|
96
|
+
const blogViewRateLimiter = createBlogViewRateLimiter();
|
|
97
|
+
|
|
98
|
+
function readHeaderString(headers, name) {
|
|
99
|
+
const raw = headers ? headers[name] : undefined;
|
|
100
|
+
return typeof raw === 'string' ? raw : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Every counter response is uncacheable at the edge and varies by Origin; none of
|
|
104
|
+
// them carries a cookie or a secret.
|
|
105
|
+
function sendJson(res, status, body, extraHeaders) {
|
|
106
|
+
const payload = JSON.stringify(body);
|
|
107
|
+
res.writeHead(status, {
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
110
|
+
'Cache-Control': 'no-store',
|
|
111
|
+
'Vary': 'Origin',
|
|
112
|
+
...(extraHeaders || {}),
|
|
113
|
+
});
|
|
114
|
+
res.end(payload);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isJsonContentType(value) {
|
|
118
|
+
if (typeof value !== 'string') return false;
|
|
119
|
+
const [type, ...parameters] = value.split(';');
|
|
120
|
+
if (type.trim().toLowerCase() !== 'application/json') return false;
|
|
121
|
+
return parameters.every((parameter) => {
|
|
122
|
+
const trimmed = parameter.trim().toLowerCase();
|
|
123
|
+
return trimmed === '' || trimmed.startsWith('charset=');
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fly terminates TLS and forwards the peer it saw in Fly-Client-IP; that header is
|
|
128
|
+
// trusted ONLY when this process really is a Fly app. Otherwise the socket address
|
|
129
|
+
// is the truth. X-Forwarded-For is client-controlled here and is never read.
|
|
130
|
+
function resolveClientIp(req) {
|
|
131
|
+
const headers = (req && req.headers) || {};
|
|
132
|
+
if (process.env.FLY_APP_NAME) {
|
|
133
|
+
const flyClientIp = readHeaderString(headers, 'fly-client-ip').trim().slice(0, 64);
|
|
134
|
+
if (net.isIP(flyClientIp)) return flyClientIp;
|
|
135
|
+
}
|
|
136
|
+
const socketAddress = req && req.socket ? req.socket.remoteAddress : undefined;
|
|
137
|
+
if (typeof socketAddress === 'string') {
|
|
138
|
+
const trimmed = socketAddress.trim().slice(0, 64);
|
|
139
|
+
if (net.isIP(trimmed)) return trimmed;
|
|
140
|
+
}
|
|
141
|
+
// Missing/invalid address: one shared bucket, never a per-garbage-value one.
|
|
142
|
+
return UNKNOWN_CLIENT_KEY;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Strict envelope: exactly {slug, token}, both strings. Anything else (an array,
|
|
146
|
+
// null, extra keys, a nested object) is a malformed payload.
|
|
147
|
+
function parseBlogViewPayload(body) {
|
|
148
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return null;
|
|
149
|
+
if (Object.keys(body).length !== 2) return null;
|
|
150
|
+
if (!Object.prototype.hasOwnProperty.call(body, 'slug')) return null;
|
|
151
|
+
if (!Object.prototype.hasOwnProperty.call(body, 'token')) return null;
|
|
152
|
+
if (typeof body.slug !== 'string' || typeof body.token !== 'string') return null;
|
|
153
|
+
return { slug: body.slug, token: body.token };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function handleBlogViewsApi(req, res, url, deps = {}) {
|
|
157
|
+
if (!url || !isBlogViewsPath(url.pathname)) return false;
|
|
158
|
+
|
|
159
|
+
const getTelemetryDb = typeof deps.getTelemetryDb === 'function' ? deps.getTelemetryDb : () => null;
|
|
160
|
+
const now = typeof deps.now === 'function' ? deps.now : Date.now;
|
|
161
|
+
const rateLimiter = deps.rateLimiter || blogViewRateLimiter;
|
|
162
|
+
const errorSink = deps.errorSink;
|
|
163
|
+
const headers = (req && req.headers) || {};
|
|
164
|
+
|
|
165
|
+
// The sink receives a static, sanitized error only: no request headers, no
|
|
166
|
+
// payload, no visitor address and no driver message ever reach it.
|
|
167
|
+
function reportRecordFailure() {
|
|
168
|
+
if (!errorSink || typeof errorSink.report !== 'function') return;
|
|
169
|
+
try {
|
|
170
|
+
errorSink.report('wall-e', 'blog_view_record_failed', new Error('blog view record failed'));
|
|
171
|
+
} catch { /* expected: a reporting sink is contractually fail-open and must never break the response it reports on */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The ONE 503 shape. `expectedRefusal` marks a policy outcome — a full receipt
|
|
175
|
+
// table or a counter at the safe-integer ceiling — which is answered the same
|
|
176
|
+
// way but is not reported as a failure. Every other error is reported (with a
|
|
177
|
+
// static error object, never the driver's), and the response is written at most
|
|
178
|
+
// once.
|
|
179
|
+
function failClosed(expectedRefusal) {
|
|
180
|
+
if (!expectedRefusal) reportRecordFailure();
|
|
181
|
+
if (!res.headersSent) sendJson(res, 503, { error: 'View count unavailable' });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// getTelemetryDb() is a real open: it can DECLINE (telemetry server mode off →
|
|
185
|
+
// a null handle, answered 404 "not enabled") or THROW (an unusable database,
|
|
186
|
+
// answered 503 like every other internal failure). Both are caught here so no
|
|
187
|
+
// path can leak an exception out of the handler — and for BOTH methods.
|
|
188
|
+
function resolveDb() {
|
|
189
|
+
try {
|
|
190
|
+
return { db: getTelemetryDb() || null, thrown: false };
|
|
191
|
+
} catch {
|
|
192
|
+
return { db: null, thrown: true };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Writes the generic refusal for a failed resolveDb(): 503 when the open threw,
|
|
197
|
+
// 404 when the counter is simply not enabled in this deployment mode.
|
|
198
|
+
function refuseUnavailable(resolved) {
|
|
199
|
+
if (resolved.thrown) failClosed(false);
|
|
200
|
+
else if (!res.headersSent) sendJson(res, 404, { error: 'View count unavailable' });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The counter takes no query string at all: url.search is empty or the request
|
|
204
|
+
// is malformed (a cache-buster or an extra parameter is not a documented input,
|
|
205
|
+
// and silently ignoring one would make the surface wider than the design's
|
|
206
|
+
// "no queries"). Refused for both methods, before anything is read or written.
|
|
207
|
+
if (url.search) {
|
|
208
|
+
sendJson(res, 400, { error: 'query_not_allowed' });
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const method = String(req.method || '').toUpperCase();
|
|
213
|
+
const hasOrigin = Object.prototype.hasOwnProperty.call(headers, 'origin');
|
|
214
|
+
const allowedOrigin = getBlogViewOrigin(readHeaderString(headers, 'origin'));
|
|
215
|
+
|
|
216
|
+
if (method === 'GET') {
|
|
217
|
+
// The public listing may be read without an Origin; a present Origin must be
|
|
218
|
+
// exactly one of the two published site origins.
|
|
219
|
+
if (hasOrigin && !allowedOrigin) {
|
|
220
|
+
sendJson(res, 403, { error: 'origin_not_allowed' });
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
const resolved = resolveDb();
|
|
224
|
+
if (!resolved.db) {
|
|
225
|
+
refuseUnavailable(resolved);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
sendJson(res, 200, { views: getBlogViewCounts(resolved.db) });
|
|
230
|
+
} catch {
|
|
231
|
+
failClosed(false);
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (method !== 'POST') {
|
|
237
|
+
sendJson(res, 405, { error: 'method_not_allowed' }, { Allow: 'GET, POST' });
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A write must come from the published site itself and be JSON.
|
|
242
|
+
if (!allowedOrigin) {
|
|
243
|
+
sendJson(res, 403, { error: 'origin_not_allowed' });
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (!isJsonContentType(readHeaderString(headers, 'content-type'))) {
|
|
247
|
+
sendJson(res, 415, { error: 'unsupported_media_type' });
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
const gate = rateLimiter.check(resolveClientIp(req), now());
|
|
251
|
+
if (!gate.ok) {
|
|
252
|
+
sendJson(res, 429, { error: 'rate_limited' }, { 'Retry-After': String(gate.retryAfterSeconds) });
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Body handling is asynchronous, exactly like the existing ingest route: the
|
|
257
|
+
// request is already claimed, so the boolean is true now and the response is
|
|
258
|
+
// written when the bounded read finishes.
|
|
259
|
+
readBody(req, { maxBytes: BLOG_VIEW_MAX_BODY_BYTES }).then((body) => {
|
|
260
|
+
try {
|
|
261
|
+
const payload = parseBlogViewPayload(body);
|
|
262
|
+
if (!payload) {
|
|
263
|
+
sendJson(res, 400, { error: 'invalid_payload' });
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!isBlogPostSlug(payload.slug)) {
|
|
267
|
+
sendJson(res, 404, { error: 'unknown_post' });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (!isBlogViewToken(payload.token)) {
|
|
271
|
+
sendJson(res, 400, { error: 'invalid_payload' });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const resolved = resolveDb();
|
|
275
|
+
if (!resolved.db) {
|
|
276
|
+
refuseUnavailable(resolved);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (isKnownBotUserAgent(readHeaderString(headers, 'user-agent'))) {
|
|
280
|
+
// A recognized crawler is answered honestly with the current total and no
|
|
281
|
+
// write at all — the counter never claims a view it did not record.
|
|
282
|
+
sendJson(res, 200, { slug: payload.slug, views: getBlogViewCounts(resolved.db)[payload.slug], counted: false });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const result = recordBlogView(resolved.db, payload.slug, payload.token, now());
|
|
286
|
+
sendJson(res, 200, { slug: result.slug, views: result.views, counted: result.counted });
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// Every INTERNAL exception lands here — a store/driver failure, a broken
|
|
289
|
+
// getTelemetryDb(), a failed query — and answers 503. Capacity/overflow are
|
|
290
|
+
// policy refusals: identical body, no failure report. Neither leaks a driver
|
|
291
|
+
// message, and the `headersSent` guard in failClosed() keeps this to one
|
|
292
|
+
// response even when a body write already started.
|
|
293
|
+
failClosed(!!error && (error.code === BLOG_VIEW_CAPACITY || error.code === BLOG_VIEW_OVERFLOW));
|
|
294
|
+
}
|
|
295
|
+
}).catch((error) => {
|
|
296
|
+
// readBody rejects ONLY on a client-side body problem: an oversized body is a
|
|
297
|
+
// 413, an aborted or malformed one a 400. The fulfillment handler above never
|
|
298
|
+
// rethrows (it answers 503 itself), so nothing internal can be misclassified
|
|
299
|
+
// as a client error here.
|
|
300
|
+
if (res.headersSent) return;
|
|
301
|
+
if (error && error.code === 'BODY_TOO_LARGE') {
|
|
302
|
+
sendJson(res, 413, { error: 'payload_too_large' });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// A malformed/aborted body is a client problem, never a 500 with details.
|
|
306
|
+
sendJson(res, 400, { error: 'invalid_payload' });
|
|
307
|
+
});
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
BLOG_VIEW_RATE_MAX_BUCKETS,
|
|
313
|
+
BLOG_VIEW_RATE_MAX_PER_WINDOW,
|
|
314
|
+
BLOG_VIEW_RATE_WINDOW_MS,
|
|
315
|
+
blogViewRateLimiter,
|
|
316
|
+
createBlogViewRateLimiter,
|
|
317
|
+
handleBlogViewsApi,
|
|
318
|
+
};
|