create-walle 0.10.2 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/lib/release-schema.json +2 -2
- package/lib/runtime-closure-policy.json +8 -7
- package/package.json +1 -1
- package/template/coding-task-manager/api-prompts.js +45 -7
- package/template/coding-task-manager/bin/claude-hook.js +55 -1
- package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
- package/template/coding-task-manager/bin/land.js +2069 -0
- package/template/coding-task-manager/bin/memory-census.js +4 -2
- package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
- package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
- package/template/coding-task-manager/db.js +156 -32
- package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
- package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
- package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
- package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
- package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
- package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
- package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
- package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
- package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
- package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
- package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
- package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
- package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
- package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
- package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
- package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
- package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
- package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
- package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
- package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
- package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
- package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
- package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
- package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
- package/template/coding-task-manager/lib/agent-definitions.js +19 -0
- package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
- package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
- package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
- package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
- package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
- package/template/coding-task-manager/lib/agent-presets.js +84 -1
- package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
- package/template/coding-task-manager/lib/agent-version-service.js +74 -11
- package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
- package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
- package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
- package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
- package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
- package/template/coding-task-manager/lib/api/fleet.js +118 -0
- package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
- package/template/coding-task-manager/lib/api/ingress.js +9 -1
- package/template/coding-task-manager/lib/api/landing.js +403 -0
- package/template/coding-task-manager/lib/api/machines.js +691 -2
- package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
- package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
- package/template/coding-task-manager/lib/api/session-attach.js +623 -0
- package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
- package/template/coding-task-manager/lib/api/sessions.js +77 -51
- package/template/coding-task-manager/lib/api/setup-network.js +544 -16
- package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
- package/template/coding-task-manager/lib/api/setup.js +829 -10
- package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
- package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
- package/template/coding-task-manager/lib/auth-rules.js +292 -21
- package/template/coding-task-manager/lib/background-stop.js +369 -0
- package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
- package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
- package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
- package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
- package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
- package/template/coding-task-manager/lib/build-info.js +4 -2
- package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
- package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
- package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
- package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
- package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
- package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
- package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
- package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
- package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
- package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
- package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
- package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
- package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
- package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
- package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
- package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
- package/template/coding-task-manager/lib/conversation-sync.js +355 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
- package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
- package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
- package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
- package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
- package/template/coding-task-manager/lib/db-domains.js +1 -0
- package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
- package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
- package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
- package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
- package/template/coding-task-manager/lib/feature-manifest.js +4 -0
- package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
- package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
- package/template/coding-task-manager/lib/fleet-hub.js +762 -0
- package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
- package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
- package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
- package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
- package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
- package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
- package/template/coding-task-manager/lib/fleet-update.js +798 -0
- package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
- package/template/coding-task-manager/lib/fs-watcher.js +58 -42
- package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
- package/template/coding-task-manager/lib/headless-term-service.js +84 -8
- package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
- package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
- package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
- package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
- package/template/coding-task-manager/lib/host-handoff.js +16 -19
- package/template/coding-task-manager/lib/host-stats.js +150 -3
- package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
- package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
- package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
- package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
- package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
- package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
- package/template/coding-task-manager/lib/hub-bridge.js +398 -0
- package/template/coding-task-manager/lib/idle-notify.js +491 -0
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
- package/template/coding-task-manager/lib/instance-manifest.js +5 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
- package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
- package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
- package/template/coding-task-manager/lib/landing/config.js +536 -0
- package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
- package/template/coding-task-manager/lib/landing/gate.js +360 -0
- package/template/coding-task-manager/lib/landing/git.js +344 -0
- package/template/coding-task-manager/lib/landing/lease.js +247 -0
- package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
- package/template/coding-task-manager/lib/landing/proof.js +223 -0
- package/template/coding-task-manager/lib/landing/queue.js +998 -0
- package/template/coding-task-manager/lib/landing/radar.js +489 -0
- package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
- package/template/coding-task-manager/lib/landing/state.js +263 -0
- package/template/coding-task-manager/lib/landing/stop.js +49 -0
- package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
- package/template/coding-task-manager/lib/landing/sync.js +456 -0
- package/template/coding-task-manager/lib/landing/train.js +885 -0
- package/template/coding-task-manager/lib/landing/window.js +57 -0
- package/template/coding-task-manager/lib/launch-environment.js +121 -0
- package/template/coding-task-manager/lib/launch-model-override.js +36 -10
- package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
- package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
- package/template/coding-task-manager/lib/lead-model-options.js +160 -3
- package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
- package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
- package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
- package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
- package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
- package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
- package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
- package/template/coding-task-manager/lib/machine-registry.js +750 -5
- package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
- package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
- package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
- package/template/coding-task-manager/lib/memory-join.js +337 -0
- package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
- package/template/coding-task-manager/lib/memory-sync.js +565 -0
- package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
- package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
- package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
- package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
- package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
- package/template/coding-task-manager/lib/model-availability.js +727 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
- package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
- package/template/coding-task-manager/lib/model-id-rule.js +19 -0
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
- package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
- package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
- package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
- package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
- package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
- package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
- package/template/coding-task-manager/lib/native-history-control.js +6 -4
- package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
- package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
- package/template/coding-task-manager/lib/opencode-cli.js +480 -0
- package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
- package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
- package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
- package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
- package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
- package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
- package/template/coding-task-manager/lib/perf-summary.js +7 -0
- package/template/coding-task-manager/lib/perf-tracker.js +63 -2
- package/template/coding-task-manager/lib/project-config.js +7 -0
- package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
- package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
- package/template/coding-task-manager/lib/read-pool-client.js +89 -2
- package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
- package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
- package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
- package/template/coding-task-manager/lib/relay-client.js +57 -3
- package/template/coding-task-manager/lib/relink-audit.js +34 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
- package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
- package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
- package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
- package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
- package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
- package/template/coding-task-manager/lib/restart-guard.js +18 -6
- package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
- package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
- package/template/coding-task-manager/lib/resume-admission.js +17 -3
- package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
- package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
- package/template/coding-task-manager/lib/runtime-contract.js +3 -1
- package/template/coding-task-manager/lib/schema-indexes.js +1 -0
- package/template/coding-task-manager/lib/session-context-locator.js +742 -0
- package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
- package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
- package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
- package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
- package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
- package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
- package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
- package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
- package/template/coding-task-manager/lib/session-create/response.js +7 -7
- package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
- package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
- package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
- package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
- package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
- package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
- package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
- package/template/coding-task-manager/lib/session-empty.js +13 -10
- package/template/coding-task-manager/lib/session-host-manager.js +60 -0
- package/template/coding-task-manager/lib/session-jobs.js +3 -1
- package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
- package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
- package/template/coding-task-manager/lib/session-park.js +738 -0
- package/template/coding-task-manager/lib/session-path-grant.js +13 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
- package/template/coding-task-manager/lib/session-transport.js +17 -0
- package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
- package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
- package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
- package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
- package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
- package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
- package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
- package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
- package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
- package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
- package/template/coding-task-manager/lib/submission-ack.js +90 -2
- package/template/coding-task-manager/lib/systemd-scope.js +115 -0
- package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
- package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
- package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
- package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
- package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
- package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
- package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
- package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
- package/template/coding-task-manager/lib/transcript-source.js +36 -4
- package/template/coding-task-manager/lib/transcript-store.js +9 -0
- package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
- package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
- package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
- package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
- package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
- package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
- package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
- package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
- package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
- package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
- package/template/coding-task-manager/lib/walle/turn.js +1 -1
- package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
- package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
- package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
- package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
- package/template/coding-task-manager/lib/walle-row-history.js +495 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
- package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
- package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
- package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
- package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
- package/template/coding-task-manager/lib/ws/misc.js +1 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
- package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
- package/template/coding-task-manager/oauth-proxy.js +112 -115
- package/template/coding-task-manager/package-lock.json +10 -94
- package/template/coding-task-manager/package.json +4 -3
- package/template/coding-task-manager/protocol/message-table.json +10 -0
- package/template/coding-task-manager/protocol/messages.js +10 -0
- package/template/coding-task-manager/protocol/ws-schema.json +20 -0
- package/template/coding-task-manager/protocol/ws-schema.md +2 -0
- package/template/coding-task-manager/public/css/app.css +170 -0
- package/template/coding-task-manager/public/css/walle-session.css +4 -0
- package/template/coding-task-manager/public/index.html +473 -243
- package/template/coding-task-manager/public/js/agent-command.js +86 -0
- package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
- package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
- package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
- package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
- package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
- package/template/coding-task-manager/public/js/background-work.js +196 -27
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
- package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
- package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
- package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
- package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
- package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
- package/template/coding-task-manager/public/js/document-review-links.js +9 -1
- package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
- package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
- package/template/coding-task-manager/public/js/message-renderer.js +4 -3
- package/template/coding-task-manager/public/js/model-picker.js +3 -1
- package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
- package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
- package/template/coding-task-manager/public/js/pasted-content.js +236 -0
- package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
- package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
- package/template/coding-task-manager/public/js/prompts.js +11 -10
- package/template/coding-task-manager/public/js/protocol.js +10 -0
- package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
- package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
- package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
- package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
- package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
- package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
- package/template/coding-task-manager/public/js/stream-view.js +6 -5
- package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
- package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
- package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
- package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
- package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
- package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
- package/template/coding-task-manager/public/js/walle.js +6 -0
- package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
- package/template/coding-task-manager/public/m/app.css +140 -0
- package/template/coding-task-manager/public/m/app.js +507 -68
- package/template/coding-task-manager/public/m/index.html +17 -0
- package/template/coding-task-manager/public/m/machines.js +838 -4
- package/template/coding-task-manager/public/m/private-api.js +31 -1
- package/template/coding-task-manager/public/usage.html +1 -0
- package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
- package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
- package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
- package/template/coding-task-manager/server.js +1950 -11679
- package/template/coding-task-manager/session-integrity.js +64 -4
- package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
- package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
- package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
- package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
- package/template/coding-task-manager/workers/session-host-process.js +5 -5
- package/template/coding-task-manager/workers/watch-worker.js +105 -0
- package/template/coding-task-manager/workers/worktree-worker.js +168 -33
- package/template/ctm-relay/lib/protocol.js +14 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/shared/backup-retention.js +82 -0
- package/template/shared/conversation-admission.js +2 -0
- package/template/shared/conversation-provenance.js +3 -7
- package/template/shared/conversation-storage-v2-contract.js +5 -0
- package/template/shared/linked-worktree.js +51 -0
- package/template/shared/macos-fda-target.js +57 -0
- package/template/shared/orphan-parents.js +55 -0
- package/template/shared/release-schema.json +2 -2
- package/template/shared/storage/contract.js +3 -0
- package/template/wall-e/adapters/imessage.js +12 -0
- package/template/wall-e/agent.js +71 -4
- package/template/wall-e/api-walle.js +21 -1
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
- package/template/wall-e/brain/brain-join.js +859 -0
- package/template/wall-e/brain/brain-sync-membership.js +20 -0
- package/template/wall-e/brain/brain-sync-store.js +131 -77
- package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
- package/template/wall-e/brain/brain-sync-v2.js +578 -0
- package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
- package/template/wall-e/brain/deterministic-id.js +51 -0
- package/template/wall-e/brain/memories.js +2 -2
- package/template/wall-e/brain/migrations.js +40 -6
- package/template/wall-e/brain/model-identity-migration.js +1 -1
- package/template/wall-e/brain/owner-call-whitelist.js +23 -7
- package/template/wall-e/brain/schema.js +45 -85
- package/template/wall-e/brain.js +171 -35
- package/template/wall-e/chat.js +3 -3
- package/template/wall-e/coding/agent-shell-env.js +7 -2
- package/template/wall-e/coding/prompt-bundle.js +66 -21
- package/template/wall-e/config/main-loop-exemptions.json +2 -2
- package/template/wall-e/http/admin-api.js +37 -0
- package/template/wall-e/http/gws-api.js +4 -0
- package/template/wall-e/http/helper-rules-api.js +7 -1
- package/template/wall-e/http/model-admin.js +8 -1
- package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
- package/template/wall-e/http/model-proxy-providers.js +174 -1
- package/template/wall-e/http/model-route-health.js +157 -0
- package/template/wall-e/http/sync-api.js +278 -464
- package/template/wall-e/jobs/job-runner.js +28 -1
- package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
- package/template/wall-e/lib/ctm-data-dir.js +26 -0
- package/template/wall-e/lib/derivation-home.js +147 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
- package/template/wall-e/lib/helper-rules/grade.js +51 -8
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
- package/template/wall-e/lib/helper-rules/store.js +4 -2
- package/template/wall-e/lib/landing-mcp-tools.js +170 -0
- package/template/wall-e/lib/mcp-client-config.js +133 -14
- package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
- package/template/wall-e/lib/mcp-integration.js +58 -13
- package/template/wall-e/lib/parent-death.js +7 -1
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
- package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
- package/template/wall-e/llm/delegate-vendor-default.js +12 -5
- package/template/wall-e/llm/model-roles.js +1 -1
- package/template/wall-e/llm/native-delegate-selection.js +56 -8
- package/template/wall-e/llm/worker-delegate-args.js +72 -7
- package/template/wall-e/llm/worker-mcp-routing.js +66 -17
- package/template/wall-e/loops/brain-custom-writes.js +19 -0
- package/template/wall-e/loops/ingest.js +17 -1
- package/template/wall-e/loops/think.js +19 -4
- package/template/wall-e/mcp-contract.js +222 -6
- package/template/wall-e/mcp-protocol-contract.js +40 -5
- package/template/wall-e/mcp-server.js +128 -29
- package/template/wall-e/mcp-worker-schemas.js +5 -5
- package/template/wall-e/memory/ctm-context-client.js +85 -0
- package/template/wall-e/memory/ctm-session-context.js +9 -3
- package/template/wall-e/memory/hub-bridge-client.js +388 -0
- package/template/wall-e/memory/replica-origin.js +56 -0
- package/template/wall-e/memory/session-pool.js +135 -0
- package/template/wall-e/runtime/live-capabilities.js +8 -3
- package/template/wall-e/server.js +4 -2
- package/template/wall-e/shared/release-schema.json +2 -2
- package/template/wall-e/shared/sqlite-file-set.js +39 -0
- package/template/wall-e/shared/storage/contract.js +3 -0
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
- package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
- package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
- package/template/wall-e/tools/chrome-automation.js +6 -0
- package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
- package/template/wall-e/workers/brain-owner-worker.js +18 -4
- package/template/wall-e/workers/mcp-gateway-process.js +5 -5
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WP-2 (docs/remote-session-hub-bridge-2026-10-30.html §5 R-4/R-5/R-8/R-9, the Rev-2 card R-11, §6.2,
|
|
4
|
+
// §8 WP-2): the ACCESS DECISION for a hub-bridge op — may this caller session run this tier on THIS
|
|
5
|
+
// machine, and if not, what does the hub tell the agent and the user?
|
|
6
|
+
//
|
|
7
|
+
// R-4 grants, on TWO axes (docs/hub-bridge-approval-scopes-2026-11-01.html §5 D-1..D-5, §6.1).
|
|
8
|
+
// A grant is keyed (machineId, callerSessionId, tier) and covers only its own tier (`exec`
|
|
9
|
+
// implies `read`). SCOPE is one caller session, or every session of that machine written
|
|
10
|
+
// `callerSessionId: '*'` (ALL_SESSIONS). DURATION is a TTL in memory (default 3600 s, hard max
|
|
11
|
+
// 28800 s — a restart revokes it) or `always: true`, persisted in the trust file's `grants`
|
|
12
|
+
// array so it survives a restart. Neither axis ever writes machine trust or the mesh (D-2).
|
|
13
|
+
// Minted only by a user principal (`POST /api/hub-bridge/grants`, lib/api/hub-bridge.js). To
|
|
14
|
+
// keep it testable the clock is INJECTED (`deps.now`), never read directly.
|
|
15
|
+
// R-5 requested, never ambient. A refusal at trust `ask` records ONE pending row per
|
|
16
|
+
// (machineId, callerSessionId, tier) — a second call while it is open adds no row and
|
|
17
|
+
// re-broadcasts nothing — and answers `hub_grant_required` with that row's id.
|
|
18
|
+
// R-11 trust policy per machine, set on the machine that does the work. `getDataDir()/hub-bridge-trust.json`
|
|
19
|
+
// (mode 0600, atomic tmp+rename) holds `{ v:1, default:'ask', machines:{ [instanceId]: 'ask'|'always'|'never' },
|
|
20
|
+
// grants:[ {machineId, callerSessionId, tier, at} ] }`. `always` is an implicit grant of every
|
|
21
|
+
// tier to every session of that member (audited, no TTL); `never` refuses with
|
|
22
|
+
// `hub_bridge_denied` and creates NO pending row; `ask` is the R-4 flow. `grants` holds the
|
|
23
|
+
// machine-scoped and durable grants of D-1/D-4 and is written ONLY by grant/revoke (a trust
|
|
24
|
+
// PUT carries it through untouched). The file is re-read per decision (mtime+size cached), so a
|
|
25
|
+
// change applies to the next call.
|
|
26
|
+
// R-9 audited. `argsDigest` is sha256(JSON(args)) and the RAW args are never written.
|
|
27
|
+
//
|
|
28
|
+
// Everything is injectable (`configure`) and the process singleton is wired once at boot in server.js.
|
|
29
|
+
|
|
30
|
+
const crypto = require('node:crypto');
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
|
|
34
|
+
const { getDataDir } = require('./data-dir');
|
|
35
|
+
|
|
36
|
+
const TRUST_FILE = 'hub-bridge-trust.json';
|
|
37
|
+
const AUDIT_FILE = 'hub-bridge-audit.jsonl';
|
|
38
|
+
const TRUST_VALUES = Object.freeze(['ask', 'always', 'never']);
|
|
39
|
+
// The known tiers, ordered for display only. Coverage is NOT by rank: R-4 says `exec` implies `read`
|
|
40
|
+
// and nothing else — a `browser` grant gives no shell, and an `exec` grant drives no browser
|
|
41
|
+
// (IMPLIES below). "always covers every tier" (R-11) is the trust check in decide(), not a grant.
|
|
42
|
+
const TIER_RANK = Object.freeze({ none: 0, read: 1, exec: 2, browser: 3 });
|
|
43
|
+
const IMPLIES = Object.freeze({ read: ['read'], exec: ['exec', 'read'], browser: ['browser'] });
|
|
44
|
+
const DEFAULT_TTL_SEC = 3600;
|
|
45
|
+
const MAX_TTL_SEC = 28800;
|
|
46
|
+
const SUMMARY_MAX = 120;
|
|
47
|
+
const AUDIT_TAIL_BYTES = 256 * 1024;
|
|
48
|
+
const AUDIT_DEFAULT_LIMIT = 20;
|
|
49
|
+
const AUDIT_MAX_LIMIT = 200;
|
|
50
|
+
const TRUST_FILE_MODE = 0o600;
|
|
51
|
+
// D-3: the machine-scope sentinel. A grant (or a request) whose callerSessionId is ALL_SESSIONS
|
|
52
|
+
// stands for "every session of that machine" on the wire, in memory and on disk. Real caller session
|
|
53
|
+
// ids are UUIDs, so the sentinel can never collide with one — and decide() REFUSES a caller that
|
|
54
|
+
// claims to be '*' or empty, so a box can never impersonate "all sessions".
|
|
55
|
+
const ALL_SESSIONS = '*';
|
|
56
|
+
|
|
57
|
+
let _deps = null;
|
|
58
|
+
// pairKey -> Map<tier, expiresAtMs>. Grants are per-process (a restart revokes everything, R-4).
|
|
59
|
+
const _grants = new Map();
|
|
60
|
+
// pendingId -> row. At most one row per (machineId, callerSessionId, tier) — see PENDING_INDEX.
|
|
61
|
+
const _pending = new Map();
|
|
62
|
+
// `${machineId}\u0000${callerSessionId}\u0000${tier}` -> pendingId. This is what makes R-5's
|
|
63
|
+
// "one row per ask" true without scanning.
|
|
64
|
+
const _pendingIndex = new Map();
|
|
65
|
+
// mtime+size cache for the trust file (R-11: re-read per decision, cheap when unchanged).
|
|
66
|
+
let _trustCache = { file: '', mtimeMs: -1, size: -1, value: null };
|
|
67
|
+
// Every trust-file write (trust PUTs AND the durable grants of D-4) runs through this one chain, so
|
|
68
|
+
// two concurrent writers cannot both read the same "current" and lose one of the two rows.
|
|
69
|
+
let _trustWriteChain = Promise.resolve();
|
|
70
|
+
|
|
71
|
+
const DEFAULT_DEPS = {
|
|
72
|
+
now: () => Date.now(),
|
|
73
|
+
randomUUID: () => crypto.randomUUID(),
|
|
74
|
+
// (frame) => void — the hub browser fan-out (hub-bridge-pending). Absent in tests that only care
|
|
75
|
+
// about the decision.
|
|
76
|
+
broadcast: null,
|
|
77
|
+
dataDir: '',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function configure(deps = {}) {
|
|
81
|
+
_deps = { ...DEFAULT_DEPS, ...(deps || {}) };
|
|
82
|
+
return _deps;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _d() {
|
|
86
|
+
return _deps || DEFAULT_DEPS;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _dataDir() {
|
|
90
|
+
const injected = _d().dataDir;
|
|
91
|
+
try {
|
|
92
|
+
return path.resolve(String(injected || getDataDir()));
|
|
93
|
+
} catch {
|
|
94
|
+
// expected: an unresolvable data dir still yields a usable absolute path for the default.
|
|
95
|
+
return path.resolve(getDataDir());
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The trust file's absolute path (R-11). */
|
|
100
|
+
function trustPath() {
|
|
101
|
+
return path.join(_dataDir(), TRUST_FILE);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The audit log's absolute path (R-9). */
|
|
105
|
+
function auditPath() {
|
|
106
|
+
return path.join(_dataDir(), AUDIT_FILE);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Ranks for the two tiers an op may declare, in ascending order (exported for the tests). */
|
|
110
|
+
function tierRank(tier) {
|
|
111
|
+
return Object.prototype.hasOwnProperty.call(TIER_RANK, String(tier)) ? TIER_RANK[String(tier)] : -1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isValidTier(tier) {
|
|
115
|
+
return tierRank(tier) >= 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Does a grant of `grantTier` satisfy a request for `tier`? (R-4: only exec implies read.) */
|
|
119
|
+
function covers(grantTier, tier) {
|
|
120
|
+
const implied = IMPLIES[String(grantTier)];
|
|
121
|
+
return Array.isArray(implied) && implied.includes(String(tier));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A refusal/validation failure as this module speaks it: the code rides the error, never the message. */
|
|
125
|
+
function _namedError(code, message) {
|
|
126
|
+
return Object.assign(new Error(message), { code });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Fan one browser frame out (R-5's `hub-bridge-pending`, and its `-cleared` twin). Best-effort: a
|
|
131
|
+
* dead browser socket must never fail the decision that triggered the frame.
|
|
132
|
+
*/
|
|
133
|
+
function _broadcast(frame) {
|
|
134
|
+
const broadcast = _d().broadcast;
|
|
135
|
+
if (typeof broadcast !== 'function') return;
|
|
136
|
+
try {
|
|
137
|
+
broadcast(frame);
|
|
138
|
+
} catch {
|
|
139
|
+
// expected: the fan-out is best-effort — a dead browser socket must never fail a decision.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* R-5/R-13: tell every browser which pending rows just went away, so a toolbar banner can hide
|
|
145
|
+
* without a re-read. Fires ONCE per removal batch and NEVER when nothing was removed.
|
|
146
|
+
* @param {string[]} ids the removed pending row ids
|
|
147
|
+
* @returns {boolean} whether a frame was broadcast
|
|
148
|
+
*/
|
|
149
|
+
function _broadcastCleared(ids) {
|
|
150
|
+
const list = (Array.isArray(ids) ? ids : []).map((id) => String(id || '')).filter(Boolean);
|
|
151
|
+
if (!list.length) return false;
|
|
152
|
+
_broadcast({ type: 'hub-bridge-pending-cleared', ids: list });
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _pairKey(machineId, callerSessionId) {
|
|
157
|
+
return `${machineId}\u0000${callerSessionId}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _pendingKey(machineId, callerSessionId, tier) {
|
|
161
|
+
return `${machineId}\u0000${callerSessionId}\u0000${tier}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------------------------
|
|
165
|
+
// R-11 — the trust file
|
|
166
|
+
// ---------------------------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/** The one shape a persisted grant row may take, or null when the row is not one (D-4). */
|
|
169
|
+
function _sanitizeGrantRow(row) {
|
|
170
|
+
if (!row || typeof row !== 'object') return null;
|
|
171
|
+
const machineId = String(row.machineId || '');
|
|
172
|
+
const callerSessionId = String(row.callerSessionId || '');
|
|
173
|
+
const tier = String(row.tier || '');
|
|
174
|
+
if (!machineId || !callerSessionId) return null;
|
|
175
|
+
if (!isValidTier(tier) || tier === 'none') return null;
|
|
176
|
+
return { machineId, callerSessionId, tier, at: Number(row.at) || 0 };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** D-4's `grants`: keep the valid rows, drop exact (machineId, callerSessionId, tier) duplicates. */
|
|
180
|
+
function _sanitizeGrants(input) {
|
|
181
|
+
const rows = Array.isArray(input) ? input : [];
|
|
182
|
+
const out = [];
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
for (const raw of rows) {
|
|
185
|
+
const row = _sanitizeGrantRow(raw);
|
|
186
|
+
if (!row) continue;
|
|
187
|
+
const key = `${row.machineId}\u0000${row.callerSessionId}\u0000${row.tier}`;
|
|
188
|
+
if (seen.has(key)) continue;
|
|
189
|
+
seen.add(key);
|
|
190
|
+
out.push(row);
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Coerce anything read off disk (or off the wire) into the R-11 shape; unknown values collapse to ask. */
|
|
196
|
+
function sanitizeTrust(input) {
|
|
197
|
+
const raw = input && typeof input === 'object' ? input : {};
|
|
198
|
+
const out = { v: 1, default: 'ask', machines: {}, grants: [] };
|
|
199
|
+
if (TRUST_VALUES.includes(raw.default)) out.default = raw.default;
|
|
200
|
+
const machines = raw.machines && typeof raw.machines === 'object' ? raw.machines : {};
|
|
201
|
+
for (const [id, value] of Object.entries(machines)) {
|
|
202
|
+
if (!id) continue;
|
|
203
|
+
if (TRUST_VALUES.includes(value)) out.machines[String(id)] = value;
|
|
204
|
+
}
|
|
205
|
+
out.grants = _sanitizeGrants(raw.grants);
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The trust policy in force right now. Re-read per call, but only when the file's mtime/size changed
|
|
211
|
+
* (R-11: "the policy is read when each request is handled"). A missing or unreadable file is the
|
|
212
|
+
* documented default (`{v:1, default:'ask', machines:{}}`) — never a crash, never a silent 'always'.
|
|
213
|
+
*/
|
|
214
|
+
function readTrust() {
|
|
215
|
+
const file = trustPath();
|
|
216
|
+
let stat = null;
|
|
217
|
+
try {
|
|
218
|
+
stat = fs.statSync(file);
|
|
219
|
+
} catch {
|
|
220
|
+
stat = null; // expected: no trust file yet — the default policy applies
|
|
221
|
+
}
|
|
222
|
+
if (!stat) {
|
|
223
|
+
_trustCache = { file, mtimeMs: -1, size: -1, value: sanitizeTrust(null) };
|
|
224
|
+
return _trustCache.value;
|
|
225
|
+
}
|
|
226
|
+
if (_trustCache.file === file && _trustCache.mtimeMs === stat.mtimeMs && _trustCache.size === stat.size) {
|
|
227
|
+
return _trustCache.value;
|
|
228
|
+
}
|
|
229
|
+
let text = '';
|
|
230
|
+
try {
|
|
231
|
+
text = fs.readFileSync(file, 'utf8');
|
|
232
|
+
} catch {
|
|
233
|
+
text = ''; // REPORT via the default below: an unreadable trust file reads as 'ask' (fail to the ask flow, not to ambient trust)
|
|
234
|
+
}
|
|
235
|
+
let parsed = null;
|
|
236
|
+
try {
|
|
237
|
+
parsed = JSON.parse(text);
|
|
238
|
+
} catch {
|
|
239
|
+
parsed = null; // expected: a half-written or hand-broken file collapses to the default policy
|
|
240
|
+
}
|
|
241
|
+
const value = sanitizeTrust(parsed);
|
|
242
|
+
_trustCache = { file, mtimeMs: stat.mtimeMs, size: stat.size, value };
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The trust value for one machine: its own row, else the file default (R-11). */
|
|
247
|
+
function trustFor(machineId) {
|
|
248
|
+
const trust = readTrust();
|
|
249
|
+
const id = String(machineId || '');
|
|
250
|
+
const value = Object.prototype.hasOwnProperty.call(trust.machines, id) ? trust.machines[id] : trust.default;
|
|
251
|
+
return TRUST_VALUES.includes(value) ? value : 'ask';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Merge-and-write the trust policy atomically (tmp + rename, mode 0600). `patch.machines` is merged
|
|
256
|
+
* per id; a `null`/`''` value for an id REMOVES that id's row (back to the default). Values are
|
|
257
|
+
* validated against ask|always|never and a bad one rejects `trust_value_invalid` (the route answers
|
|
258
|
+
* 400). `patch.grants` is IGNORED: the durable grants of D-4 ride through untouched, so a trust PUT
|
|
259
|
+
* can neither add nor drop one (only grantGrants/revokeGrants write grants). Async: a write never
|
|
260
|
+
* runs on the hub's main loop (a loaded disk must not freeze the UI).
|
|
261
|
+
*/
|
|
262
|
+
async function writeTrust(patch = {}) {
|
|
263
|
+
const raw = patch && typeof patch === 'object' ? patch : {};
|
|
264
|
+
if (raw.default !== undefined && !TRUST_VALUES.includes(raw.default)) {
|
|
265
|
+
throw _namedError('trust_value_invalid', `default must be one of ${TRUST_VALUES.join('|')}`);
|
|
266
|
+
}
|
|
267
|
+
const incoming = raw.machines && typeof raw.machines === 'object' ? raw.machines : {};
|
|
268
|
+
for (const [id, value] of Object.entries(incoming)) {
|
|
269
|
+
if (!id || value === null || value === '') continue;
|
|
270
|
+
if (!TRUST_VALUES.includes(value)) {
|
|
271
|
+
throw _namedError('trust_value_invalid', `machines['${id}'] must be one of ${TRUST_VALUES.join('|')}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const next = await _chainTrustWrite(async () => {
|
|
275
|
+
const current = readTrust();
|
|
276
|
+
const machines = { ...current.machines };
|
|
277
|
+
for (const [id, value] of Object.entries(incoming)) {
|
|
278
|
+
if (!id) continue;
|
|
279
|
+
if (value === null || value === '') {
|
|
280
|
+
delete machines[id];
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
machines[id] = value;
|
|
284
|
+
}
|
|
285
|
+
const value = sanitizeTrust({
|
|
286
|
+
v: 1,
|
|
287
|
+
default: raw.default !== undefined ? raw.default : current.default,
|
|
288
|
+
machines,
|
|
289
|
+
// D-4: a trust PUT can never add or drop a grant — the durable rows ride through untouched.
|
|
290
|
+
grants: current.grants,
|
|
291
|
+
});
|
|
292
|
+
await _writeTrustFile(value);
|
|
293
|
+
return value;
|
|
294
|
+
});
|
|
295
|
+
_clearPendingTrusted();
|
|
296
|
+
return next;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Run one trust-file write at a time. The chain NEVER rejects (a failed write must not poison the
|
|
301
|
+
* queue for the writes behind it); the caller still sees its own rejection through the returned
|
|
302
|
+
* promise. Both writeTrust and the durable grants of D-4 go through here.
|
|
303
|
+
* @template T
|
|
304
|
+
* @param {() => Promise<T>} run
|
|
305
|
+
* @returns {Promise<T>}
|
|
306
|
+
*/
|
|
307
|
+
function _chainTrustWrite(run) {
|
|
308
|
+
const result = _trustWriteChain.then(run);
|
|
309
|
+
_trustWriteChain = result.then(() => {}, () => {});
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Write the whole trust object atomically (tmp + rename, mode 0600) and become the cache's truth.
|
|
315
|
+
* Shared by writeTrust and the `always` grant writes, so both leave the exact same file on disk.
|
|
316
|
+
* @param {{v:number, default:string, machines:object, grants:Array}} next
|
|
317
|
+
*/
|
|
318
|
+
async function _writeTrustFile(next) {
|
|
319
|
+
const file = trustPath();
|
|
320
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
321
|
+
// G-5's spirit (no partial file ever visible): write a sibling tmp then rename over the target.
|
|
322
|
+
const tmp = `${file}.tmp-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
323
|
+
await fs.promises.writeFile(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: TRUST_FILE_MODE });
|
|
324
|
+
await fs.promises.rename(tmp, file);
|
|
325
|
+
// The rename may keep the same mtime tick; trust the value we just wrote rather than the stat cache.
|
|
326
|
+
_trustCache = { file, mtimeMs: -1, size: -1, value: next };
|
|
327
|
+
return next;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A trust write that makes a machine 'always' has answered its open asks (the [Always allow] button, or
|
|
332
|
+
* the card select / page switch): drop them and broadcast ONE cleared frame, so no banner keeps asking.
|
|
333
|
+
*/
|
|
334
|
+
function _clearPendingTrusted() {
|
|
335
|
+
const cleared = [];
|
|
336
|
+
for (const [id, row] of [..._pending.entries()]) {
|
|
337
|
+
if (trustFor(row.machineId) !== 'always') continue;
|
|
338
|
+
_pending.delete(id);
|
|
339
|
+
_pendingIndex.delete(_pendingKey(row.machineId, row.callerSessionId, row.tier));
|
|
340
|
+
cleared.push(id);
|
|
341
|
+
}
|
|
342
|
+
_broadcastCleared(cleared);
|
|
343
|
+
return cleared.length > 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------------------------
|
|
347
|
+
// R-4 — grants (D-1: one session or the whole machine × a TTL or `always`)
|
|
348
|
+
// ---------------------------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
/** D-1: the scope a callerSessionId names — '*' is every session of the machine. */
|
|
351
|
+
function _scopeOf(callerSessionId) {
|
|
352
|
+
return callerSessionId === ALL_SESSIONS ? 'machine' : 'session';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** D-1's wire/route shape of one grant row. */
|
|
356
|
+
function _grantRow({ machineId, callerSessionId, tier, always, ttlSec, expiresAt }) {
|
|
357
|
+
return {
|
|
358
|
+
machineId,
|
|
359
|
+
callerSessionId,
|
|
360
|
+
tier,
|
|
361
|
+
scope: _scopeOf(callerSessionId),
|
|
362
|
+
always: always === true,
|
|
363
|
+
ttlSec: always === true ? null : ttlSec,
|
|
364
|
+
expiresAt: always === true ? null : expiresAt,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Does a durable (D-4) row for this exact pair cover `tier`? The trust file is the source of truth. */
|
|
369
|
+
function _persistedGrant(machineId, callerSessionId, tier) {
|
|
370
|
+
return readTrust().grants.some((row) => row.machineId === machineId
|
|
371
|
+
&& row.callerSessionId === callerSessionId
|
|
372
|
+
&& covers(row.tier, tier));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function _clampTtlSec(value) {
|
|
376
|
+
const n = Number(value);
|
|
377
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_TTL_SEC;
|
|
378
|
+
return Math.min(MAX_TTL_SEC, Math.max(1, Math.trunc(n)));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** The live grant covering `tier` for this pair, or null. Expired rows are dropped as they are seen. */
|
|
382
|
+
function _activeGrant(pairKey, tier) {
|
|
383
|
+
const byTier = _grants.get(pairKey);
|
|
384
|
+
if (!byTier) return null;
|
|
385
|
+
const nowMs = Number(_d().now()) || 0;
|
|
386
|
+
let found = null;
|
|
387
|
+
for (const [grantTier, expiresAt] of [...byTier.entries()]) {
|
|
388
|
+
if (!(Number(expiresAt) > nowMs)) {
|
|
389
|
+
byTier.delete(grantTier);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (covers(grantTier, tier)) found = { tier: grantTier, expiresAt };
|
|
393
|
+
}
|
|
394
|
+
if (!byTier.size) _grants.delete(pairKey);
|
|
395
|
+
return found;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Mint (or refresh) a grant on both axes (D-1). `callerSessionId` is one session — or ALL_SESSIONS
|
|
400
|
+
* for every session of the machine.
|
|
401
|
+
* - `always !== true`: an in-memory TTL grant as before. `ttlSec` defaults to 3600 and is capped at
|
|
402
|
+
* 28800 (R-4); a restart drops it.
|
|
403
|
+
* - `always === true`: a durable row `{machineId, callerSessionId, tier, at}` appended to the trust
|
|
404
|
+
* file (D-4) through the same atomic write as writeTrust, idempotent per triple; `ttlSec` is
|
|
405
|
+
* ignored. It never touches `machines`, never calls the mesh, and survives a restart.
|
|
406
|
+
* Both clear the pending rows the grant answers (D-5) and broadcast ONE cleared frame.
|
|
407
|
+
* @returns {Promise<{machineId:string, callerSessionId:string, tier:string, scope:'session'|'machine',
|
|
408
|
+
* always:boolean, ttlSec:number|null, expiresAt:number|null}>}
|
|
409
|
+
*/
|
|
410
|
+
async function grantGrants({ machineId, callerSessionId, tier, ttlSec, always } = {}) {
|
|
411
|
+
const id = String(machineId || '');
|
|
412
|
+
const session = String(callerSessionId || '');
|
|
413
|
+
const wanted = String(tier || '');
|
|
414
|
+
if (!id || !session) throw _namedError('grant_invalid', 'machineId and callerSessionId are required');
|
|
415
|
+
if (!isValidTier(wanted) || wanted === 'none') {
|
|
416
|
+
throw _namedError('grant_invalid', `tier must be one of ${Object.keys(TIER_RANK).filter((t) => t !== 'none').join('|')}`);
|
|
417
|
+
}
|
|
418
|
+
if (always === true) {
|
|
419
|
+
const at = Number(_d().now()) || 0;
|
|
420
|
+
await _chainTrustWrite(async () => {
|
|
421
|
+
const current = readTrust();
|
|
422
|
+
const rows = current.grants.slice();
|
|
423
|
+
const known = rows.some((row) => row.machineId === id && row.callerSessionId === session && row.tier === wanted);
|
|
424
|
+
if (!known) rows.push({ machineId: id, callerSessionId: session, tier: wanted, at });
|
|
425
|
+
return _writeTrustFile(sanitizeTrust({
|
|
426
|
+
v: 1,
|
|
427
|
+
default: current.default,
|
|
428
|
+
machines: current.machines,
|
|
429
|
+
grants: rows,
|
|
430
|
+
}));
|
|
431
|
+
});
|
|
432
|
+
_clearPending(id, session, wanted);
|
|
433
|
+
return _grantRow({ machineId: id, callerSessionId: session, tier: wanted, always: true });
|
|
434
|
+
}
|
|
435
|
+
const ttl = _clampTtlSec(ttlSec);
|
|
436
|
+
const expiresAt = (Number(_d().now()) || 0) + ttl * 1000;
|
|
437
|
+
const pairKey = _pairKey(id, session);
|
|
438
|
+
const byTier = _grants.get(pairKey) || new Map();
|
|
439
|
+
byTier.set(wanted, expiresAt);
|
|
440
|
+
_grants.set(pairKey, byTier);
|
|
441
|
+
_clearPending(id, session, wanted);
|
|
442
|
+
return _grantRow({ machineId: id, callerSessionId: session, tier: wanted, always: false, ttlSec: ttl, expiresAt });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Drop every pending row this grant answers (D-5). A machine-scoped grant (callerSessionId ===
|
|
447
|
+
* ALL_SESSIONS) answers every session of that machine; otherwise the match is the strict pair.
|
|
448
|
+
*/
|
|
449
|
+
function _clearPending(machineId, callerSessionId, grantedTier) {
|
|
450
|
+
const everySession = callerSessionId === ALL_SESSIONS;
|
|
451
|
+
const cleared = [];
|
|
452
|
+
for (const [id, row] of [..._pending.entries()]) {
|
|
453
|
+
if (row.machineId !== machineId) continue;
|
|
454
|
+
if (!everySession && row.callerSessionId !== callerSessionId) continue;
|
|
455
|
+
if (!covers(grantedTier, row.tier)) continue;
|
|
456
|
+
_pending.delete(id);
|
|
457
|
+
_pendingIndex.delete(_pendingKey(row.machineId, row.callerSessionId, row.tier));
|
|
458
|
+
cleared.push(id);
|
|
459
|
+
}
|
|
460
|
+
// R-13: one frame per removal batch, and none at all when the grant answered no open ask.
|
|
461
|
+
_broadcastCleared(cleared);
|
|
462
|
+
return cleared.length > 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Revoke grants for a pair. With no `tier`, every tier is dropped; with one, that tier and every
|
|
467
|
+
* grant that covers it is dropped (revoking `read` also drops `exec`, matching how a grant is honoured).
|
|
468
|
+
* Both the in-memory TTL rows and the durable rows of D-4 go — the trust file is rewritten ONLY when
|
|
469
|
+
* a durable row was actually removed, so revoking an hour grant never touches the disk.
|
|
470
|
+
* @returns {Promise<number>} how many grant rows were removed (both kinds together)
|
|
471
|
+
*/
|
|
472
|
+
async function revokeGrants({ machineId, callerSessionId, tier } = {}) {
|
|
473
|
+
const id = String(machineId || '');
|
|
474
|
+
const session = String(callerSessionId || '');
|
|
475
|
+
const wanted = String(tier || '');
|
|
476
|
+
const pairKey = _pairKey(id, session);
|
|
477
|
+
const byTier = _grants.get(pairKey);
|
|
478
|
+
let removed = 0;
|
|
479
|
+
if (byTier) {
|
|
480
|
+
for (const grantTier of [...byTier.keys()]) {
|
|
481
|
+
if (!wanted || covers(grantTier, wanted)) {
|
|
482
|
+
byTier.delete(grantTier);
|
|
483
|
+
removed += 1;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (!byTier.size) _grants.delete(pairKey);
|
|
487
|
+
}
|
|
488
|
+
const persisted = await _chainTrustWrite(async () => {
|
|
489
|
+
const current = readTrust();
|
|
490
|
+
const kept = [];
|
|
491
|
+
let dropped = 0;
|
|
492
|
+
for (const row of current.grants) {
|
|
493
|
+
if (row.machineId === id && row.callerSessionId === session && (!wanted || covers(row.tier, wanted))) {
|
|
494
|
+
dropped += 1;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
kept.push(row);
|
|
498
|
+
}
|
|
499
|
+
if (!dropped) return 0;
|
|
500
|
+
await _writeTrustFile(sanitizeTrust({
|
|
501
|
+
v: 1,
|
|
502
|
+
default: current.default,
|
|
503
|
+
machines: current.machines,
|
|
504
|
+
grants: kept,
|
|
505
|
+
}));
|
|
506
|
+
return dropped;
|
|
507
|
+
});
|
|
508
|
+
return removed + persisted;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Every live grant: the in-memory TTL rows as before, plus the durable rows of D-4 (`always:true`,
|
|
513
|
+
* `ttlSec`/`expiresAt` null, read straight off the trust file). Sort (D-1's display order): machine,
|
|
514
|
+
* then session rows before the '*' machine rows, then callerSessionId, then tier rank, then `always`
|
|
515
|
+
* last inside a triple.
|
|
516
|
+
*/
|
|
517
|
+
function listGrants() {
|
|
518
|
+
const nowMs = Number(_d().now()) || 0;
|
|
519
|
+
const rows = [];
|
|
520
|
+
for (const byTier of _grants.values()) {
|
|
521
|
+
for (const [tier, expiresAt] of [...byTier.entries()]) {
|
|
522
|
+
if (!(Number(expiresAt) > nowMs)) byTier.delete(tier);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
for (const [pairKey, byTier] of [..._grants.entries()]) {
|
|
526
|
+
if (!byTier.size) {
|
|
527
|
+
_grants.delete(pairKey);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const [machineId, callerSessionId] = pairKey.split('\u0000');
|
|
531
|
+
for (const [tier, expiresAt] of byTier.entries()) {
|
|
532
|
+
rows.push({
|
|
533
|
+
machineId,
|
|
534
|
+
callerSessionId,
|
|
535
|
+
tier,
|
|
536
|
+
scope: _scopeOf(callerSessionId),
|
|
537
|
+
always: false,
|
|
538
|
+
ttlSec: Math.max(0, Math.round((expiresAt - nowMs) / 1000)),
|
|
539
|
+
expiresAt,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (const row of readTrust().grants) {
|
|
544
|
+
rows.push(_grantRow({ ...row, always: true }));
|
|
545
|
+
}
|
|
546
|
+
rows.sort((a, b) => (a.machineId.localeCompare(b.machineId)
|
|
547
|
+
|| (a.scope === b.scope ? 0 : a.scope === 'session' ? -1 : 1)
|
|
548
|
+
|| a.callerSessionId.localeCompare(b.callerSessionId)
|
|
549
|
+
|| tierRank(a.tier) - tierRank(b.tier)
|
|
550
|
+
|| (a.always === b.always ? 0 : a.always ? 1 : -1)));
|
|
551
|
+
return rows;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ---------------------------------------------------------------------------------------------
|
|
555
|
+
// R-5 — pending asks
|
|
556
|
+
// ---------------------------------------------------------------------------------------------
|
|
557
|
+
|
|
558
|
+
/** The one-line human summary an ask carries: the first 120 chars of the command or path. */
|
|
559
|
+
function summarizeArgs(args) {
|
|
560
|
+
const raw = args && typeof args === 'object' ? args : {};
|
|
561
|
+
const text = String(raw.command || raw.path || raw.ref || '').replace(/\s+/g, ' ').trim();
|
|
562
|
+
return text.slice(0, SUMMARY_MAX);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Create or REUSE the single pending row for (machineId, callerSessionId, tier) and broadcast it
|
|
567
|
+
* ONLY when it is new (R-5: an agent polling in a loop must not spam the toolbar).
|
|
568
|
+
*/
|
|
569
|
+
function _requestPending({ machineId, callerSessionId, tier, op, machineName, summary }) {
|
|
570
|
+
const key = _pendingKey(machineId, callerSessionId, tier);
|
|
571
|
+
const existingId = _pendingIndex.get(key);
|
|
572
|
+
if (existingId && _pending.has(existingId)) return _pending.get(existingId);
|
|
573
|
+
const row = {
|
|
574
|
+
id: _d().randomUUID(),
|
|
575
|
+
machineId,
|
|
576
|
+
machineName: String(machineName || ''),
|
|
577
|
+
callerSessionId,
|
|
578
|
+
tier,
|
|
579
|
+
op: String(op || ''),
|
|
580
|
+
summary: String(summary || '').slice(0, SUMMARY_MAX),
|
|
581
|
+
at: Number(_d().now()) || 0,
|
|
582
|
+
};
|
|
583
|
+
_pending.set(row.id, row);
|
|
584
|
+
_pendingIndex.set(key, row.id);
|
|
585
|
+
_broadcast({ type: 'hub-bridge-pending', pending: row });
|
|
586
|
+
return row;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function listPending({ machineId } = {}) {
|
|
590
|
+
const rows = [..._pending.values()];
|
|
591
|
+
const id = String(machineId || '');
|
|
592
|
+
return (id ? rows.filter((row) => row.machineId === id) : rows).map((row) => ({ ...row }));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Clear one pending ask (the toolbar's Deny). Returns whether a row was actually removed. */
|
|
596
|
+
function denyPending(id) {
|
|
597
|
+
const key = String(id || '');
|
|
598
|
+
const row = _pending.get(key);
|
|
599
|
+
if (!row) return false;
|
|
600
|
+
_pending.delete(key);
|
|
601
|
+
_pendingIndex.delete(_pendingKey(row.machineId, row.callerSessionId, row.tier));
|
|
602
|
+
// R-13: the toolbar hides this row's banner on the frame, not on a re-read.
|
|
603
|
+
_broadcastCleared([key]);
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ---------------------------------------------------------------------------------------------
|
|
608
|
+
// R-9 — audit
|
|
609
|
+
// ---------------------------------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
/** sha256(JSON(args)) — the ONLY form of the args that ever reaches disk (R-9). */
|
|
612
|
+
function digestArgs(args) {
|
|
613
|
+
let text;
|
|
614
|
+
try {
|
|
615
|
+
text = JSON.stringify(args === undefined ? null : args);
|
|
616
|
+
} catch {
|
|
617
|
+
text = '"[unserialisable]"';
|
|
618
|
+
}
|
|
619
|
+
return crypto.createHash('sha256').update(String(text), 'utf8').digest('hex');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Appends are chained so lines land in call order without a sync write on the main loop.
|
|
623
|
+
let _auditChain = Promise.resolve();
|
|
624
|
+
|
|
625
|
+
/** Resolves once every audit line queued so far is on disk (recentAudit and the tests await it). */
|
|
626
|
+
function flushAudit() {
|
|
627
|
+
return _auditChain;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Queue one JSONL audit line and return the row. `entry.args` is digested here and never written;
|
|
632
|
+
* every other field is copied verbatim. Best-effort: a failed write must never fail the op it
|
|
633
|
+
* describes.
|
|
634
|
+
*/
|
|
635
|
+
function audit(entry = {}) {
|
|
636
|
+
const row = {
|
|
637
|
+
at: Number(entry.at) || Number(_d().now()) || 0,
|
|
638
|
+
machineId: String(entry.machineId || ''),
|
|
639
|
+
callerSessionId: String(entry.callerSessionId || ''),
|
|
640
|
+
op: String(entry.op || ''),
|
|
641
|
+
tier: String(entry.tier || 'none'),
|
|
642
|
+
via: String(entry.via || 'none'),
|
|
643
|
+
argsDigest: digestArgs(entry.args),
|
|
644
|
+
ok: entry.ok === true,
|
|
645
|
+
code: String(entry.code || ''),
|
|
646
|
+
ms: Math.max(0, Number(entry.ms) || 0),
|
|
647
|
+
};
|
|
648
|
+
const file = auditPath();
|
|
649
|
+
const line = `${JSON.stringify(row)}\n`;
|
|
650
|
+
_auditChain = _auditChain
|
|
651
|
+
.then(() => fs.promises.mkdir(path.dirname(file), { recursive: true }))
|
|
652
|
+
.then(() => fs.promises.appendFile(file, line))
|
|
653
|
+
.catch(() => {
|
|
654
|
+
// expected: the audit sink is best-effort (R-9) — a full disk must not fail a granted op.
|
|
655
|
+
});
|
|
656
|
+
return row;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* The newest audit rows for one machine, newest first, from at most the last 256 KiB of the file.
|
|
661
|
+
* A line that straddles the read window (or is torn) is dropped rather than surfacing as garbage.
|
|
662
|
+
*/
|
|
663
|
+
async function recentAudit({ machineId, limit = AUDIT_DEFAULT_LIMIT } = {}) {
|
|
664
|
+
await flushAudit();
|
|
665
|
+
const file = auditPath();
|
|
666
|
+
const cap = Math.min(AUDIT_MAX_LIMIT, Math.max(1, Math.trunc(Number(limit) || AUDIT_DEFAULT_LIMIT)));
|
|
667
|
+
let handle = null;
|
|
668
|
+
let text = '';
|
|
669
|
+
let startedMidFile = false;
|
|
670
|
+
try {
|
|
671
|
+
handle = await fs.promises.open(file, 'r');
|
|
672
|
+
const stat = await handle.stat();
|
|
673
|
+
const start = Math.max(0, stat.size - AUDIT_TAIL_BYTES);
|
|
674
|
+
startedMidFile = start > 0;
|
|
675
|
+
const length = stat.size - start;
|
|
676
|
+
if (length <= 0) return [];
|
|
677
|
+
const buffer = Buffer.alloc(length);
|
|
678
|
+
await handle.read(buffer, 0, length, start);
|
|
679
|
+
text = buffer.toString('utf8');
|
|
680
|
+
} catch {
|
|
681
|
+
// REPORT via []: no audit file yet (or an unreadable one) is an empty history, not an error.
|
|
682
|
+
return [];
|
|
683
|
+
} finally {
|
|
684
|
+
if (handle) {
|
|
685
|
+
try { await handle.close(); } catch { /* expected: already closed */ }
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const lines = text.split('\n');
|
|
689
|
+
if (startedMidFile) lines.shift(); // the first line may be a fragment of a row above the window
|
|
690
|
+
const wanted = String(machineId || '');
|
|
691
|
+
const rows = [];
|
|
692
|
+
for (const line of lines) {
|
|
693
|
+
if (!line.trim()) continue;
|
|
694
|
+
let parsed = null;
|
|
695
|
+
try {
|
|
696
|
+
parsed = JSON.parse(line);
|
|
697
|
+
} catch {
|
|
698
|
+
continue; // expected: a torn trailing line (another writer mid-append) is skipped
|
|
699
|
+
}
|
|
700
|
+
if (wanted && String(parsed.machineId || '') !== wanted) continue;
|
|
701
|
+
rows.push(parsed);
|
|
702
|
+
}
|
|
703
|
+
rows.reverse(); // newest first
|
|
704
|
+
return rows.slice(0, cap);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ---------------------------------------------------------------------------------------------
|
|
708
|
+
// The decision
|
|
709
|
+
// ---------------------------------------------------------------------------------------------
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* R-4/R-11/D-3/D-5: decide one request. Order is fixed (D-5): an invalid tier or a caller that claims
|
|
713
|
+
* to be "all sessions" is refused outright; then machine trust; then the four grant lookups (hour
|
|
714
|
+
* session, hour '*', durable session, durable '*'); then the ask. Every grant hit reports via 'grant'.
|
|
715
|
+
* Still SYNCHRONOUS — it rides the op's request path (the trust file's cached sync read is why).
|
|
716
|
+
*
|
|
717
|
+
* @param {{machineId:string, callerSessionId:string, tier:string, op?:string, machineName?:string,
|
|
718
|
+
* args?:object, summary?:string}} request
|
|
719
|
+
* @returns {{allow:true, via:'trust'|'grant'|'none'} |
|
|
720
|
+
* {allow:false, code:'hub_bridge_denied'} |
|
|
721
|
+
* {allow:false, code:'hub_grant_required', pendingId:string, tier:string}}
|
|
722
|
+
*/
|
|
723
|
+
function decide(request = {}) {
|
|
724
|
+
const machineId = String(request.machineId || '');
|
|
725
|
+
const callerSessionId = String(request.callerSessionId || '');
|
|
726
|
+
const tier = String(request.tier || 'none');
|
|
727
|
+
if (!isValidTier(tier)) return { allow: false, code: 'hub_bridge_denied' };
|
|
728
|
+
if (tier === 'none') return { allow: true, via: 'none' };
|
|
729
|
+
// D-3: '*' is the machine-scope SENTINEL, never a caller. A box claiming it (or claiming nothing)
|
|
730
|
+
// is refused before any trust/grant read, and creates NO pending row — it can never impersonate
|
|
731
|
+
// "all sessions" and thereby answer somebody else's ask.
|
|
732
|
+
if (!callerSessionId || callerSessionId === ALL_SESSIONS) return { allow: false, code: 'hub_bridge_denied' };
|
|
733
|
+
|
|
734
|
+
const trust = trustFor(machineId);
|
|
735
|
+
if (trust === 'never') return { allow: false, code: 'hub_bridge_denied' };
|
|
736
|
+
if (trust === 'always') return { allow: true, via: 'trust' };
|
|
737
|
+
|
|
738
|
+
// D-5: this session's own hour grant, then the machine's hour grant, then the two durable kinds.
|
|
739
|
+
if (_activeGrant(_pairKey(machineId, callerSessionId), tier)) return { allow: true, via: 'grant' };
|
|
740
|
+
if (_activeGrant(_pairKey(machineId, ALL_SESSIONS), tier)) return { allow: true, via: 'grant' };
|
|
741
|
+
if (_persistedGrant(machineId, callerSessionId, tier)) return { allow: true, via: 'grant' };
|
|
742
|
+
if (_persistedGrant(machineId, ALL_SESSIONS, tier)) return { allow: true, via: 'grant' };
|
|
743
|
+
|
|
744
|
+
const pending = _requestPending({
|
|
745
|
+
machineId,
|
|
746
|
+
callerSessionId,
|
|
747
|
+
tier,
|
|
748
|
+
op: request.op,
|
|
749
|
+
machineName: request.machineName,
|
|
750
|
+
summary: request.summary !== undefined ? request.summary : summarizeArgs(request.args),
|
|
751
|
+
});
|
|
752
|
+
return { allow: false, code: 'hub_grant_required', pendingId: pending.id, tier };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function _resetForTests() {
|
|
756
|
+
_deps = null;
|
|
757
|
+
_grants.clear();
|
|
758
|
+
_pending.clear();
|
|
759
|
+
_pendingIndex.clear();
|
|
760
|
+
_trustCache = { file: '', mtimeMs: -1, size: -1, value: null };
|
|
761
|
+
_trustWriteChain = Promise.resolve();
|
|
762
|
+
_auditChain = Promise.resolve();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
module.exports = {
|
|
766
|
+
TRUST_FILE,
|
|
767
|
+
AUDIT_FILE,
|
|
768
|
+
TRUST_VALUES,
|
|
769
|
+
ALL_SESSIONS,
|
|
770
|
+
TIER_RANK,
|
|
771
|
+
DEFAULT_TTL_SEC,
|
|
772
|
+
MAX_TTL_SEC,
|
|
773
|
+
SUMMARY_MAX,
|
|
774
|
+
AUDIT_TAIL_BYTES,
|
|
775
|
+
AUDIT_DEFAULT_LIMIT,
|
|
776
|
+
configure,
|
|
777
|
+
tierRank,
|
|
778
|
+
isValidTier,
|
|
779
|
+
covers,
|
|
780
|
+
trustPath,
|
|
781
|
+
auditPath,
|
|
782
|
+
sanitizeTrust,
|
|
783
|
+
readTrust,
|
|
784
|
+
trustFor,
|
|
785
|
+
writeTrust,
|
|
786
|
+
grantGrants,
|
|
787
|
+
revokeGrants,
|
|
788
|
+
listGrants,
|
|
789
|
+
summarizeArgs,
|
|
790
|
+
listPending,
|
|
791
|
+
denyPending,
|
|
792
|
+
digestArgs,
|
|
793
|
+
audit,
|
|
794
|
+
flushAudit,
|
|
795
|
+
recentAudit,
|
|
796
|
+
decide,
|
|
797
|
+
_resetForTests,
|
|
798
|
+
};
|