harnery 0.40.0 → 0.41.0
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 +1 -1
- package/dist/commander.js +1 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +25 -19
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +4 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +141 -10
- package/dist/commands/checkpoint.js +8 -12
- package/dist/commands/deinit.d.ts +1 -1
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +8 -4
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +2 -0
- package/dist/commands/doctor.js +10 -2
- package/dist/commands/fetch.d.ts.map +1 -1
- package/dist/commands/fetch.js +10 -0
- package/dist/commands/init.d.ts +19 -3
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +232 -104
- package/dist/commands/instructions.js +5 -4
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +2 -1
- package/dist/commands/webview.d.ts +4 -0
- package/dist/commands/webview.d.ts.map +1 -0
- package/dist/commands/webview.js +94 -0
- package/dist/core/adapter.d.ts +50 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +59 -1
- package/dist/core/adapters/attestation.d.ts.map +1 -1
- package/dist/core/adapters/attestation.js +3 -2
- package/dist/core/adapters/profiles.d.ts +20 -3
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +32 -1
- package/dist/core/adapters/registry.d.ts.map +1 -1
- package/dist/core/adapters/registry.js +33 -0
- package/dist/core/agents/canonical-emit.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -7
- package/dist/core/agents/cli.js +18 -16
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +7 -2
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +3 -9
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +2 -8
- package/dist/core/agents/rules/stop-hook.d.ts +2 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +1 -1
- package/dist/core/agents/session-events.d.ts +6 -2
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +105 -26
- package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalization-state-v3.js +2 -1
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalizer-v3.js +5 -4
- package/dist/core/agents/session-name-display.d.ts +28 -0
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +43 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +10 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +21 -2
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +3 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +2 -10
- package/dist/core/agents/turn-changes.d.ts +17 -0
- package/dist/core/agents/turn-changes.d.ts.map +1 -0
- package/dist/core/agents/turn-changes.js +76 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +2 -1
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +4 -3
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +14 -0
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +2 -1
- package/dist/core/conversations/archive.d.ts.map +1 -1
- package/dist/core/conversations/archive.js +4 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +3 -2
- package/dist/core/events/legacy-storage/canary.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/canary.js +5 -4
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/compression.js +2 -1
- package/dist/core/events/v3/adapter-id.d.ts +10 -2
- package/dist/core/events/v3/adapter-id.d.ts.map +1 -1
- package/dist/core/events/v3/adapter-id.js +16 -0
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +2 -1
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +5 -4
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +17 -0
- package/dist/core/events/v3/contract.d.ts +5 -0
- package/dist/core/events/v3/contract.d.ts.map +1 -1
- package/dist/core/events/v3/contract.js +7 -1
- package/dist/core/events/v3/control-witness.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.js +3 -2
- package/dist/core/events/v3/coordination-transaction-recovery.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-transaction-recovery.js +4 -3
- package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -1
- package/dist/core/events/v3/fingerprint-keys.js +7 -6
- package/dist/core/events/v3/generated.d.ts +1 -1
- package/dist/core/events/v3/generated.js +1 -1
- package/dist/core/events/v3/live-feed.d.ts.map +1 -1
- package/dist/core/events/v3/live-feed.js +5 -4
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +8 -7
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +8 -7
- package/dist/core/events/v3/producers/diagnostic-summaries.d.ts.map +1 -1
- package/dist/core/events/v3/producers/diagnostic-summaries.js +5 -4
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +10 -4
- package/dist/core/events/v3/producers/hook.d.ts +2 -0
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook.js +16 -1
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +16 -6
- package/dist/core/events/v3/producers/recorder.d.ts +2 -4
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +109 -101
- package/dist/core/events/v3/recovery.d.ts.map +1 -1
- package/dist/core/events/v3/recovery.js +4 -3
- package/dist/core/events/v3/support-storage/inventory.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/inventory.js +2 -1
- package/dist/core/events/v3/support-storage/maintenance.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/maintenance.js +4 -3
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +4 -3
- package/dist/core/events/v3/support-storage/pack-writer.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-writer.js +6 -5
- package/dist/core/events/v3/tool-line-changes.d.ts +8 -0
- package/dist/core/events/v3/tool-line-changes.d.ts.map +1 -0
- package/dist/core/events/v3/tool-line-changes.js +135 -0
- package/dist/core/events/v3/writer.d.ts.map +1 -1
- package/dist/core/events/v3/writer.js +8 -7
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +6 -5
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +12 -11
- package/dist/core/governor/state.d.ts.map +1 -1
- package/dist/core/governor/state.js +7 -6
- package/dist/core/guard/evaluator.js +1 -0
- package/dist/core/guard/storage.d.ts.map +1 -1
- package/dist/core/guard/storage.js +3 -2
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts +4 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.js +31 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts +15 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/codex.js +82 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/cursor.js +35 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts +11 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/index.js +20 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.js +34 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/opencode.js +34 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts +89 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/types.js +1 -0
- package/dist/core/hooks/adapter/detect.d.ts +1 -1
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +2 -3
- package/dist/core/hooks/adapter/events.d.ts +29 -1
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +31 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts +60 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts.map +1 -0
- package/dist/core/hooks/adapter/opencode-plugin.js +129 -0
- package/dist/core/hooks/adapter/output.d.ts +7 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +8 -1
- package/dist/core/hooks/adapter/wiring.d.ts +15 -1
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +74 -11
- package/dist/core/hooks/cli.js +635 -529
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
- package/dist/core/hooks/effects/image-capture.js +2 -1
- package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -1
- package/dist/core/hooks/prompt-context/contract.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +1 -1
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +35 -4
- package/dist/core/hooks/session-name-presence.d.ts +3 -1
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +7 -2
- package/dist/core/hooks/shell-waiter.d.ts +2 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -0
- package/dist/core/hooks/shell-waiter.js +173 -0
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +5 -4
- package/dist/core/semantic/review.d.ts.map +1 -1
- package/dist/core/semantic/review.js +5 -4
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +15 -14
- package/dist/core/semantic/storage.d.ts.map +1 -1
- package/dist/core/semantic/storage.js +5 -4
- package/dist/core/storage/atomic-json.d.ts.map +1 -1
- package/dist/core/storage/atomic-json.js +7 -3
- package/dist/core/storage/durable-history.d.ts.map +1 -1
- package/dist/core/storage/durable-history.js +6 -5
- package/dist/core/storage/maintenance-providers.d.ts.map +1 -1
- package/dist/core/storage/maintenance-providers.js +2 -1
- package/dist/core/storage/maintenance.d.ts.map +1 -1
- package/dist/core/storage/maintenance.js +7 -6
- package/dist/core/storage/metrics.d.ts.map +1 -1
- package/dist/core/storage/metrics.js +5 -1
- package/dist/core/storage/modes.d.ts +17 -0
- package/dist/core/storage/modes.d.ts.map +1 -0
- package/dist/core/storage/modes.js +47 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +3 -2
- package/dist/core/storage/receipts.d.ts.map +1 -1
- package/dist/core/storage/receipts.js +4 -3
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +7 -6
- package/dist/core/supervisor/hook-health.d.ts +2 -1
- package/dist/core/supervisor/hook-health.d.ts.map +1 -1
- package/dist/core/supervisor/hook-health.js +4 -3
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +7 -6
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +8 -7
- package/dist/core/workflow/adapters.d.ts +7 -0
- package/dist/core/workflow/adapters.d.ts.map +1 -1
- package/dist/core/workflow/adapters.js +9 -0
- package/dist/core/workflow/approvals.d.ts.map +1 -1
- package/dist/core/workflow/approvals.js +6 -5
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +33 -4
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +4 -3
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +2 -3
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -2
- package/dist/core/workflow/spawn-opencode.d.ts +36 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -0
- package/dist/core/workflow/spawn-opencode.js +132 -0
- package/dist/core/workflow/transcript.d.ts.map +1 -1
- package/dist/core/workflow/transcript.js +3 -2
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +5 -4
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +3 -2
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/paths.js +2 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +10 -9
- package/dist/lazy-commands.js +8 -1
- package/dist/lib/agent-browser/client.d.ts +9 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +13 -0
- package/dist/lib/browser/client.d.ts +35 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +117 -8
- package/dist/lib/browser/identity.d.ts +25 -0
- package/dist/lib/browser/identity.d.ts.map +1 -0
- package/dist/lib/browser/identity.js +66 -0
- package/dist/lib/browser/index.d.ts +2 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -1
- package/dist/lib/browser/launch-args.d.ts +26 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -1
- package/dist/lib/browser/launch-args.js +41 -1
- package/dist/lib/browser/user-agent.d.ts +53 -0
- package/dist/lib/browser/user-agent.d.ts.map +1 -0
- package/dist/lib/browser/user-agent.js +79 -0
- package/dist/lib/docs-metadata-managed.d.ts +1 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
- package/dist/lib/docs-metadata-managed.js +17 -1
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -9
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +2 -0
- package/dist/lib/pace/index.d.ts +2 -0
- package/dist/lib/pace/index.d.ts.map +1 -0
- package/dist/lib/pace/index.js +1 -0
- package/dist/lib/pace/pace.d.ts +95 -0
- package/dist/lib/pace/pace.d.ts.map +1 -0
- package/dist/lib/pace/pace.js +313 -0
- package/dist/lib/tunnel/forward-headers.d.ts +11 -0
- package/dist/lib/tunnel/forward-headers.d.ts.map +1 -0
- package/dist/lib/tunnel/forward-headers.js +17 -0
- package/dist/lib/tunnel/gate.js +15 -10
- package/dist/lib/webview/index.d.ts +2 -0
- package/dist/lib/webview/index.d.ts.map +1 -0
- package/dist/lib/webview/index.js +1 -0
- package/dist/lib/webview/webview.d.ts +116 -0
- package/dist/lib/webview/webview.d.ts.map +1 -0
- package/dist/lib/webview/webview.js +289 -0
- package/opencode-plugin/package.json +19 -0
- package/opencode-plugin/src/index.ts +791 -0
- package/package.json +11 -4
- package/schemas/config.schema.json +13 -0
- package/schemas/event-v3.schema.json +164 -0
- package/src/commander.ts +7 -0
- package/src/commands/agents.ts +33 -18
- package/src/commands/browse-ai.ts +8 -0
- package/src/commands/browse.ts +189 -10
- package/src/commands/checkpoint.ts +7 -9
- package/src/commands/deinit.ts +9 -4
- package/src/commands/docs.ts +2 -0
- package/src/commands/doctor.ts +11 -2
- package/src/commands/fetch.ts +14 -0
- package/src/commands/init.ts +281 -119
- package/src/commands/instructions.ts +8 -4
- package/src/commands/ledger-v3.ts +2 -1
- package/src/commands/webview.ts +159 -0
- package/src/core/adapter.ts +102 -1
- package/src/core/adapters/attestation.ts +3 -2
- package/src/core/adapters/profiles.ts +42 -1
- package/src/core/adapters/registry.ts +37 -0
- package/src/core/agents/canonical-emit.ts +2 -5
- package/src/core/agents/cli.ts +18 -17
- package/src/core/agents/coord-client.ts +9 -3
- package/src/core/agents/rules/claim-conflict.ts +3 -8
- package/src/core/agents/rules/commit-conflict.ts +2 -7
- package/src/core/agents/rules/stop-hook.ts +3 -1
- package/src/core/agents/session-events.ts +120 -26
- package/src/core/agents/session-finalization-state-v3.ts +2 -1
- package/src/core/agents/session-finalizer-v3.ts +5 -4
- package/src/core/agents/session-name-display.ts +48 -0
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +24 -2
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +3 -0
- package/src/core/agents/state/stale-sweep.ts +2 -9
- package/src/core/agents/turn-changes.ts +97 -0
- package/src/core/artifacts/delivery-card.ts +2 -1
- package/src/core/artifacts/index.ts +4 -3
- package/src/core/config.ts +23 -0
- package/src/core/context/index.ts +2 -1
- package/src/core/conversations/archive.ts +4 -3
- package/src/core/diagnostics/bundle.ts +3 -2
- package/src/core/events/legacy-storage/canary.ts +5 -4
- package/src/core/events/legacy-storage/compression.ts +2 -1
- package/src/core/events/v3/adapter-id.ts +29 -1
- package/src/core/events/v3/authority-outbox.ts +2 -1
- package/src/core/events/v3/bootstrap.ts +5 -4
- package/src/core/events/v3/capabilities.ts +17 -0
- package/src/core/events/v3/contract.ts +9 -1
- package/src/core/events/v3/control-witness.ts +3 -2
- package/src/core/events/v3/coordination-transaction-recovery.ts +4 -3
- package/src/core/events/v3/fingerprint-keys.ts +7 -6
- package/src/core/events/v3/generated.ts +1 -1
- package/src/core/events/v3/live-feed.ts +5 -4
- package/src/core/events/v3/producers/command-recorder.ts +8 -6
- package/src/core/events/v3/producers/coordination-recorder.ts +8 -7
- package/src/core/events/v3/producers/diagnostic-summaries.ts +5 -4
- package/src/core/events/v3/producers/hook-base.ts +11 -4
- package/src/core/events/v3/producers/hook.ts +19 -1
- package/src/core/events/v3/producers/intake.ts +18 -6
- package/src/core/events/v3/producers/recorder.ts +123 -114
- package/src/core/events/v3/recovery.ts +4 -3
- package/src/core/events/v3/support-storage/inventory.ts +2 -1
- package/src/core/events/v3/support-storage/maintenance.ts +8 -3
- package/src/core/events/v3/support-storage/pack-reader.ts +4 -3
- package/src/core/events/v3/support-storage/pack-writer.ts +6 -5
- package/src/core/events/v3/tool-line-changes.ts +139 -0
- package/src/core/events/v3/writer.ts +8 -7
- package/src/core/governor/planning.ts +6 -5
- package/src/core/governor/service.ts +12 -11
- package/src/core/governor/state.ts +7 -6
- package/src/core/guard/evaluator.ts +1 -0
- package/src/core/guard/storage.ts +3 -2
- package/src/core/hooks/adapter/behaviors/claude-code.ts +33 -0
- package/src/core/hooks/adapter/behaviors/codex.ts +96 -0
- package/src/core/hooks/adapter/behaviors/cursor.ts +37 -0
- package/src/core/hooks/adapter/behaviors/index.ts +31 -0
- package/src/core/hooks/adapter/behaviors/openclaw.ts +36 -0
- package/src/core/hooks/adapter/behaviors/opencode.ts +36 -0
- package/src/core/hooks/adapter/behaviors/types.ts +110 -0
- package/src/core/hooks/adapter/detect.ts +2 -3
- package/src/core/hooks/adapter/events.ts +53 -1
- package/src/core/hooks/adapter/opencode-plugin.ts +177 -0
- package/src/core/hooks/adapter/output.ts +8 -1
- package/src/core/hooks/adapter/wiring.ts +85 -11
- package/src/core/hooks/cli.ts +733 -537
- package/src/core/hooks/effects/image-capture.ts +2 -1
- package/src/core/hooks/prompt-context/contract.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +42 -5
- package/src/core/hooks/session-name-presence.ts +10 -3
- package/src/core/hooks/shell-waiter.ts +166 -0
- package/src/core/resources/service.ts +5 -4
- package/src/core/semantic/review.ts +5 -4
- package/src/core/semantic/service.ts +15 -14
- package/src/core/semantic/storage.ts +5 -4
- package/src/core/storage/atomic-json.ts +7 -3
- package/src/core/storage/durable-history.ts +6 -5
- package/src/core/storage/maintenance-providers.ts +2 -1
- package/src/core/storage/maintenance.ts +7 -6
- package/src/core/storage/metrics.ts +5 -1
- package/src/core/storage/modes.ts +53 -0
- package/src/core/storage/process-log.ts +3 -2
- package/src/core/storage/receipts.ts +4 -3
- package/src/core/storage/segments.ts +7 -6
- package/src/core/supervisor/hook-health.ts +4 -4
- package/src/core/supervisor/service.ts +7 -6
- package/src/core/work/state.ts +8 -7
- package/src/core/workflow/adapters.ts +15 -0
- package/src/core/workflow/approvals.ts +6 -5
- package/src/core/workflow/billing.ts +34 -4
- package/src/core/workflow/durable-record.ts +4 -3
- package/src/core/workflow/live-session-v3.ts +2 -3
- package/src/core/workflow/run-state.ts +3 -2
- package/src/core/workflow/spawn-opencode.ts +150 -0
- package/src/core/workflow/transcript.ts +3 -2
- package/src/core/workflow/workspaces/leases.ts +5 -4
- package/src/core/workflow/workspaces/local-git.ts +3 -2
- package/src/core/workflow/workspaces/paths.ts +2 -1
- package/src/core/workflow/workspaces/state.ts +10 -9
- package/src/lazy-commands.ts +11 -1
- package/src/lib/agent-browser/client.ts +18 -0
- package/src/lib/browser/client.ts +147 -9
- package/src/lib/browser/identity.ts +64 -0
- package/src/lib/browser/index.ts +15 -1
- package/src/lib/browser/launch-args.ts +53 -1
- package/src/lib/browser/user-agent.ts +130 -0
- package/src/lib/docs-metadata-managed.ts +21 -0
- package/src/lib/identities/assume.ts +7 -7
- package/src/lib/instructions/apply.ts +2 -0
- package/src/lib/pace/index.ts +14 -0
- package/src/lib/pace/pace.ts +370 -0
- package/src/lib/tunnel/forward-headers.ts +16 -0
- package/src/lib/tunnel/gate.ts +18 -10
- package/src/lib/webview/index.ts +13 -0
- package/src/lib/webview/webview.ts +457 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Present as a normal browser.
|
|
8
|
+
*
|
|
9
|
+
* A desktop-Linux user agent is itself a block signal: web application
|
|
10
|
+
* firewalls commonly deny "X11; Linux" outright because almost no customer
|
|
11
|
+
* browses from desktop Linux, and headless builds announce "HeadlessChrome".
|
|
12
|
+
* Sunbelt Network's Cloudflare rule was the reported case: the same page
|
|
13
|
+
* answered 200 the moment the browser said Windows. Every Harnery page load
|
|
14
|
+
* therefore uses a user agent for the host the operator actually sits at
|
|
15
|
+
* (Windows under WSL or on Windows, macOS on a Mac), never Linux, with the
|
|
16
|
+
* Chrome major version of the browser that is really running.
|
|
17
|
+
*
|
|
18
|
+
* The resolved string is persisted at `~/.cache/harnery/user-agent.json` so
|
|
19
|
+
* every process reuses the one that works; `HARNERY_BROWSER_UA` overrides it
|
|
20
|
+
* for a run (`native` disables the override), and `--user-agent` on `browse`
|
|
21
|
+
* rewrites the stored value.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type HostFamily = "windows" | "mac";
|
|
25
|
+
|
|
26
|
+
const STORE_PATH = resolve(homedir(), ".cache", "harnery", "user-agent.json");
|
|
27
|
+
|
|
28
|
+
/** Windows unless the operator is on a Mac. Linux never presents as Linux. */
|
|
29
|
+
export function hostFamily(platform: NodeJS.Platform = process.platform): HostFamily {
|
|
30
|
+
return platform === "darwin" ? "mac" : "windows";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A current-shape Chrome user agent for the host family and Chrome major. */
|
|
34
|
+
export function normalUserAgent(chromeMajor: number, family: HostFamily): string {
|
|
35
|
+
const os = family === "mac" ? "Macintosh; Intel Mac OS X 10_15_7" : "Windows NT 10.0; Win64; x64";
|
|
36
|
+
return `Mozilla/5.0 (${os}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeMajor}.0.0.0 Safari/537.36`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Major version from `<chrome> --version` output, or `undefined`. */
|
|
40
|
+
export function parseChromeMajor(versionOutput: string): number | undefined {
|
|
41
|
+
const match = /(\d+)\.\d+\.\d+\.\d+/.exec(versionOutput);
|
|
42
|
+
return match ? Number(match[1]) : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Ask an installed Chrome binary for its major version. */
|
|
46
|
+
export function chromeMajorFromExecutable(executable: string): number | undefined {
|
|
47
|
+
try {
|
|
48
|
+
const res = spawnSync(executable, ["--version"], { encoding: "utf8", timeout: 5000 });
|
|
49
|
+
return parseChromeMajor(`${res.stdout ?? ""}${res.stderr ?? ""}`);
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface StoredUserAgent {
|
|
56
|
+
userAgent: string;
|
|
57
|
+
source: "auto" | "manual";
|
|
58
|
+
chromeMajor?: number;
|
|
59
|
+
updatedAt: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function readStoredUserAgent(path: string = STORE_PATH): StoredUserAgent | undefined {
|
|
63
|
+
try {
|
|
64
|
+
if (!existsSync(path)) return undefined;
|
|
65
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<StoredUserAgent>;
|
|
66
|
+
return typeof parsed.userAgent === "string" && parsed.userAgent.length > 0
|
|
67
|
+
? (parsed as StoredUserAgent)
|
|
68
|
+
: undefined;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function writeStoredUserAgent(entry: StoredUserAgent, path: string = STORE_PATH): void {
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
76
|
+
writeFileSync(path, `${JSON.stringify(entry, null, 2)}\n`, { mode: 0o600 });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ResolveUserAgentInput {
|
|
80
|
+
/** Explicit `--user-agent` value: a string, `"native"`, or `"auto"`. */
|
|
81
|
+
requested?: string;
|
|
82
|
+
/** `HARNERY_BROWSER_UA` from the environment. */
|
|
83
|
+
env?: string;
|
|
84
|
+
/** Major version of the Chrome that will actually launch, when known. */
|
|
85
|
+
chromeMajor?: number;
|
|
86
|
+
platform?: NodeJS.Platform;
|
|
87
|
+
storePath?: string;
|
|
88
|
+
now?: () => string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The user agent to launch with, or `undefined` for the browser's native
|
|
93
|
+
* string. Precedence: explicit request, then environment, then the stored
|
|
94
|
+
* value (refreshed when the Chrome major moved), then a fresh auto value.
|
|
95
|
+
* An explicit string or `auto` request rewrites the store; `native` does not.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveUserAgent(input: ResolveUserAgentInput = {}): string | undefined {
|
|
98
|
+
const now = input.now ?? (() => new Date().toISOString());
|
|
99
|
+
const family = hostFamily(input.platform);
|
|
100
|
+
const requested = input.requested?.trim();
|
|
101
|
+
if (requested && requested !== "auto") {
|
|
102
|
+
if (requested === "native") return undefined;
|
|
103
|
+
writeStoredUserAgent(
|
|
104
|
+
{ userAgent: requested, source: "manual", updatedAt: now() },
|
|
105
|
+
input.storePath,
|
|
106
|
+
);
|
|
107
|
+
return requested;
|
|
108
|
+
}
|
|
109
|
+
const env = input.env?.trim();
|
|
110
|
+
if (env && requested !== "auto") {
|
|
111
|
+
return env === "native" ? undefined : env;
|
|
112
|
+
}
|
|
113
|
+
const stored = readStoredUserAgent(input.storePath);
|
|
114
|
+
if (
|
|
115
|
+
stored &&
|
|
116
|
+
requested !== "auto" &&
|
|
117
|
+
(stored.source === "manual" ||
|
|
118
|
+
input.chromeMajor === undefined ||
|
|
119
|
+
stored.chromeMajor === input.chromeMajor)
|
|
120
|
+
) {
|
|
121
|
+
return stored.userAgent;
|
|
122
|
+
}
|
|
123
|
+
const major = input.chromeMajor ?? stored?.chromeMajor ?? 149;
|
|
124
|
+
const userAgent = normalUserAgent(major, family);
|
|
125
|
+
writeStoredUserAgent(
|
|
126
|
+
{ userAgent, source: "auto", chromeMajor: major, updatedAt: now() },
|
|
127
|
+
input.storePath,
|
|
128
|
+
);
|
|
129
|
+
return userAgent;
|
|
130
|
+
}
|
|
@@ -5,6 +5,26 @@ function normalized(path: string): string {
|
|
|
5
5
|
return path.replaceAll("\\", "/");
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** Host-supplied path prefixes that carry another system's frontmatter.
|
|
9
|
+
*
|
|
10
|
+
* Hosts declare these once (harnery's `extraDocsExcludedPrefixes`) for
|
|
11
|
+
* generated reference dumps and vendored upstream trees. Every metadata
|
|
12
|
+
* consumer reads them through `isExcludedDocsMetadataPath`, so a prefix
|
|
13
|
+
* listed by the host is honored by the audit and the sync, not only by the
|
|
14
|
+
* lint's own file discovery.
|
|
15
|
+
*/
|
|
16
|
+
let EXTRA_EXCLUDED_PREFIXES: readonly string[] = [];
|
|
17
|
+
|
|
18
|
+
export function initDocsMetadataExclusions(prefixes: readonly string[] | undefined): void {
|
|
19
|
+
EXTRA_EXCLUDED_PREFIXES = prefixes ?? [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasExcludedPrefix(rel: string): boolean {
|
|
23
|
+
return EXTRA_EXCLUDED_PREFIXES.some(
|
|
24
|
+
(prefix) => rel.startsWith(prefix) || rel.includes(`/${prefix}`),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
/**
|
|
9
29
|
* Paths whose Markdown carries frontmatter belonging to another system:
|
|
10
30
|
* generated mirrors of documents that live elsewhere, vendored vendor
|
|
@@ -14,6 +34,7 @@ export function isExcludedDocsMetadataPath(path: string): boolean {
|
|
|
14
34
|
const rel = normalized(path);
|
|
15
35
|
const segments = rel.split("/");
|
|
16
36
|
return (
|
|
37
|
+
hasExcludedPrefix(rel) ||
|
|
17
38
|
segments.includes(".wiki-data") ||
|
|
18
39
|
// Dashboard surfaces render from copies of other repositories' documents.
|
|
19
40
|
/(?:^|\/)content\/dashboards\//.test(rel) ||
|
|
@@ -25,8 +25,11 @@ import {
|
|
|
25
25
|
} from "../../core/agents/state/live-coordination-view.ts";
|
|
26
26
|
import { resolveForkAncestry } from "../../core/agents/state/names.ts";
|
|
27
27
|
import { instanceHasLivePid, removePidmapRowsForInstance } from "../../core/agents/state/pidmap.ts";
|
|
28
|
-
import { coordFreshnessSeconds } from "../../core/config.ts";
|
|
29
|
-
import
|
|
28
|
+
import { coordFreshnessSeconds, resolveBinName } from "../../core/config.ts";
|
|
29
|
+
import {
|
|
30
|
+
type EventAdapterIdV3,
|
|
31
|
+
eventAdapterIdV3FromPlatform,
|
|
32
|
+
} from "../../core/events/v3/adapter-id.ts";
|
|
30
33
|
import { readRemoteMachines } from "../../core/presence/index.ts";
|
|
31
34
|
import { type AgentIdentity, bareName, ensureIdentity, lookupById, lookupByName } from "./index.ts";
|
|
32
35
|
|
|
@@ -237,10 +240,7 @@ function acquireLock(coordRoot: string): () => void {
|
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
function adapterOf(platform: string | undefined): EventAdapterIdV3 {
|
|
240
|
-
|
|
241
|
-
if (platform === "codex") return "codex";
|
|
242
|
-
if (platform === "openclaw") return "openclaw";
|
|
243
|
-
return "claude-code";
|
|
243
|
+
return eventAdapterIdV3FromPlatform(platform, { context: "identity-assume" });
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
/** Assume `target` for one live main session. Safe to retry. */
|
|
@@ -256,7 +256,7 @@ export function assumeIdentity(
|
|
|
256
256
|
if (!hb) {
|
|
257
257
|
throw new IdentityAssumeError(
|
|
258
258
|
"no_live_generation",
|
|
259
|
-
`no authority-safe live V3 generation for instance '${instanceId}'`,
|
|
259
|
+
`no authority-safe live V3 generation for instance '${instanceId}'; run \`${resolveBinName(coordRoot)} agents lifecycle active\` to open a fresh generation for this continuing session, then assume again`,
|
|
260
260
|
);
|
|
261
261
|
}
|
|
262
262
|
if (hb.kind && hb.kind !== "session") {
|
|
@@ -58,6 +58,8 @@ const ADAPTER_SKILLS_DIR: Readonly<Record<string, string>> = {
|
|
|
58
58
|
"claude-code": join(".claude", "skills"),
|
|
59
59
|
cursor: join(".agents", "skills"),
|
|
60
60
|
codex: join(".agents", "skills"),
|
|
61
|
+
// OpenCode natively discovers .agents/skills (and .claude/skills); no mirror needed.
|
|
62
|
+
opencode: join(".agents", "skills"),
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
function adapterSkillsDir(adapter: string): string | null {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export {
|
|
2
|
+
commandPaceGate,
|
|
3
|
+
DEFAULT_PACE_LEDGER,
|
|
4
|
+
DEFAULT_PACE_MAX_MS,
|
|
5
|
+
DEFAULT_PACE_MIN_MS,
|
|
6
|
+
describePaceWait,
|
|
7
|
+
PaceGate,
|
|
8
|
+
type PaceGateDeps,
|
|
9
|
+
type PacePolicy,
|
|
10
|
+
type PaceWait,
|
|
11
|
+
pacePolicyFromEnv,
|
|
12
|
+
paceSiteKey,
|
|
13
|
+
sleepSync,
|
|
14
|
+
} from "./pace.ts";
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
openSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Human-pace gate for page loads.
|
|
16
|
+
*
|
|
17
|
+
* Every Harnery surface that loads a web page (`browse`, `browse-ai`, `fetch`,
|
|
18
|
+
* and the headed browse-session verbs) reserves a slot here before the request
|
|
19
|
+
* goes out. Consecutive loads against the same registrable site are spaced by
|
|
20
|
+
* a random gap inside `[minMs, maxMs]`, the way a person reads one page before
|
|
21
|
+
* opening the next. Loads against different sites do not wait on each other.
|
|
22
|
+
* Loopback, private-network, single-label, and reserved-suffix hosts are exempt
|
|
23
|
+
* so local development and page QA keep running at machine speed.
|
|
24
|
+
*
|
|
25
|
+
* The ledger is a small machine-local JSON file (`~/.cache/harnery/pace.json`)
|
|
26
|
+
* shared by every process, so separate `harn fetch` calls in a shell loop are
|
|
27
|
+
* paced the same as one long `harn browse` session. A caller reserves the next
|
|
28
|
+
* slot before it sleeps, so concurrent processes queue behind each other
|
|
29
|
+
* instead of all firing at once.
|
|
30
|
+
*
|
|
31
|
+
* The gate is on by default. `HARNERY_PACE=off` disables it machine-wide and
|
|
32
|
+
* each command carries `--no-pace` for a single run.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_PACE_MIN_MS = 3_000;
|
|
36
|
+
export const DEFAULT_PACE_MAX_MS = 9_000;
|
|
37
|
+
export const DEFAULT_PACE_LEDGER = resolve(homedir(), ".cache", "harnery", "pace.json");
|
|
38
|
+
|
|
39
|
+
/** Entries idle this long drop out of the ledger on the next write. */
|
|
40
|
+
const LEDGER_RETENTION_MS = 60 * 60 * 1_000;
|
|
41
|
+
/** A lock older than this belongs to a dead process and is reclaimed. */
|
|
42
|
+
const LOCK_STALE_MS = 5_000;
|
|
43
|
+
/** Total time a reservation waits for the ledger lock before proceeding anyway. */
|
|
44
|
+
const LOCK_WAIT_MS = 500;
|
|
45
|
+
const LOCK_POLL_MS = 5;
|
|
46
|
+
|
|
47
|
+
export interface PacePolicy {
|
|
48
|
+
/** False turns the gate into a no-op. Default true. */
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
/** Shortest gap between two loads of the same site, in ms. */
|
|
51
|
+
minMs: number;
|
|
52
|
+
/** Longest gap between two loads of the same site, in ms. */
|
|
53
|
+
maxMs: number;
|
|
54
|
+
/** Extra exempt host suffixes (`example.test` matches itself and `*.example.test`). */
|
|
55
|
+
exemptSuffixes: string[];
|
|
56
|
+
/** Path of the shared reservation ledger. */
|
|
57
|
+
ledgerPath: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PaceWait {
|
|
61
|
+
/** Registrable site the reservation was made against. */
|
|
62
|
+
site: string;
|
|
63
|
+
/** Milliseconds the caller waited (0 when the slot was free). */
|
|
64
|
+
waitMs: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PaceGateDeps {
|
|
68
|
+
now?: () => number;
|
|
69
|
+
random?: () => number;
|
|
70
|
+
/** Called after a reservation, whether or not it waited. */
|
|
71
|
+
onWait?: (wait: PaceWait) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface LedgerEntry {
|
|
75
|
+
/** Epoch ms before which the next load of this site must not start. */
|
|
76
|
+
nextAllowedAt: number;
|
|
77
|
+
/** Epoch ms the most recent reservation was granted for. */
|
|
78
|
+
lastAt: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface Ledger {
|
|
82
|
+
version: 1;
|
|
83
|
+
sites: Record<string, LedgerEntry>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const OFF_VALUES = new Set(["off", "0", "false", "no", "none"]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the pace policy from the environment.
|
|
90
|
+
*
|
|
91
|
+
* - `HARNERY_PACE`: `off` (or `0`, `false`, `no`) disables the gate. Unset or
|
|
92
|
+
* `human` keeps the default.
|
|
93
|
+
* - `HARNERY_PACE_MIN_MS` / `HARNERY_PACE_MAX_MS`: gap bounds in ms. Both must
|
|
94
|
+
* be non-negative integers with min <= max.
|
|
95
|
+
* - `HARNERY_PACE_EXEMPT`: comma-separated host suffixes that never wait.
|
|
96
|
+
* - `HARNERY_PACE_LEDGER`: ledger path override.
|
|
97
|
+
*/
|
|
98
|
+
export function pacePolicyFromEnv(env: NodeJS.ProcessEnv = process.env): PacePolicy {
|
|
99
|
+
const mode = env.HARNERY_PACE?.trim().toLowerCase();
|
|
100
|
+
if (mode && mode !== "human" && !OFF_VALUES.has(mode)) {
|
|
101
|
+
throw new Error(`HARNERY_PACE must be "human" or "off" (got "${env.HARNERY_PACE}").`);
|
|
102
|
+
}
|
|
103
|
+
const minMs = readMs(env, "HARNERY_PACE_MIN_MS", DEFAULT_PACE_MIN_MS);
|
|
104
|
+
const maxMs = readMs(env, "HARNERY_PACE_MAX_MS", DEFAULT_PACE_MAX_MS);
|
|
105
|
+
if (minMs > maxMs) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`HARNERY_PACE_MIN_MS (${minMs}) must not exceed HARNERY_PACE_MAX_MS (${maxMs}).`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const exemptSuffixes = (env.HARNERY_PACE_EXEMPT ?? "")
|
|
111
|
+
.split(",")
|
|
112
|
+
.map((s) => normalizeHost(s))
|
|
113
|
+
.filter((s): s is string => s !== null);
|
|
114
|
+
return {
|
|
115
|
+
enabled: !(mode && OFF_VALUES.has(mode)),
|
|
116
|
+
minMs,
|
|
117
|
+
maxMs,
|
|
118
|
+
exemptSuffixes,
|
|
119
|
+
ledgerPath: env.HARNERY_PACE_LEDGER?.trim() || DEFAULT_PACE_LEDGER,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readMs(env: NodeJS.ProcessEnv, name: string, fallback: number): number {
|
|
124
|
+
const raw = env[name]?.trim();
|
|
125
|
+
if (!raw) return fallback;
|
|
126
|
+
if (!/^\d+$/.test(raw)) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`${name} must be a non-negative integer number of milliseconds (got "${raw}").`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return Number.parseInt(raw, 10);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Public suffixes that take two labels, so the registrable site is three
|
|
136
|
+
* labels deep (`news.bbc.co.uk` -> `bbc.co.uk`). A short curated list is
|
|
137
|
+
* enough here: the gate only needs sibling hosts of one site to share a key,
|
|
138
|
+
* and a miss degrades to per-hostname pacing, never to no pacing.
|
|
139
|
+
*/
|
|
140
|
+
const TWO_LABEL_SUFFIXES = new Set([
|
|
141
|
+
"co.uk",
|
|
142
|
+
"org.uk",
|
|
143
|
+
"ac.uk",
|
|
144
|
+
"gov.uk",
|
|
145
|
+
"com.au",
|
|
146
|
+
"net.au",
|
|
147
|
+
"org.au",
|
|
148
|
+
"co.nz",
|
|
149
|
+
"co.jp",
|
|
150
|
+
"co.kr",
|
|
151
|
+
"co.in",
|
|
152
|
+
"co.za",
|
|
153
|
+
"com.br",
|
|
154
|
+
"com.mx",
|
|
155
|
+
"com.ar",
|
|
156
|
+
"com.cn",
|
|
157
|
+
"com.tw",
|
|
158
|
+
"com.sg",
|
|
159
|
+
"com.tr",
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
/** Reserved or local-only suffixes that are never paced. */
|
|
163
|
+
const EXEMPT_SUFFIXES = ["localhost", "local", "test", "internal", "example", "invalid"];
|
|
164
|
+
|
|
165
|
+
function normalizeHost(raw: string): string | null {
|
|
166
|
+
const host = raw.trim().toLowerCase().replace(/\.+$/, "");
|
|
167
|
+
return host ? host : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasSuffix(host: string, suffix: string): boolean {
|
|
171
|
+
return host === suffix || host.endsWith(`.${suffix}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isExemptIpv4(host: string): boolean {
|
|
175
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
176
|
+
if (!m) return false;
|
|
177
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
178
|
+
return (
|
|
179
|
+
a === 127 ||
|
|
180
|
+
a === 10 ||
|
|
181
|
+
a === 0 ||
|
|
182
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
183
|
+
(a === 192 && b === 168) ||
|
|
184
|
+
(a === 169 && b === 254)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isExemptIpv6(host: string): boolean {
|
|
189
|
+
const h = host.replace(/^\[|\]$/g, "");
|
|
190
|
+
return h === "::1" || h === "::" || /^f[cd]/.test(h) || /^fe[89ab]/.test(h);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Registrable site a URL belongs to for pacing purposes, or `null` when the
|
|
195
|
+
* URL is exempt: a non-HTTP scheme, a loopback or private address, a
|
|
196
|
+
* single-label or reserved-suffix host, or a configured exemption.
|
|
197
|
+
*/
|
|
198
|
+
export function paceSiteKey(url: string, exemptSuffixes: readonly string[] = []): string | null {
|
|
199
|
+
let parsed: URL;
|
|
200
|
+
try {
|
|
201
|
+
parsed = new URL(url);
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
206
|
+
const host = normalizeHost(parsed.hostname);
|
|
207
|
+
if (!host) return null;
|
|
208
|
+
if (host.startsWith("[") || host.includes(":")) return isExemptIpv6(host) ? null : host;
|
|
209
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return isExemptIpv4(host) ? null : host;
|
|
210
|
+
for (const suffix of EXEMPT_SUFFIXES) if (hasSuffix(host, suffix)) return null;
|
|
211
|
+
for (const suffix of exemptSuffixes) if (hasSuffix(host, suffix)) return null;
|
|
212
|
+
const labels = host.split(".");
|
|
213
|
+
if (labels.length < 2) return null;
|
|
214
|
+
const lastTwo = labels.slice(-2).join(".");
|
|
215
|
+
if (labels.length >= 3 && TWO_LABEL_SUFFIXES.has(lastTwo)) return labels.slice(-3).join(".");
|
|
216
|
+
return lastTwo;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Block the current thread for `ms`. Used by clients that drive a child process synchronously. */
|
|
220
|
+
export function sleepSync(ms: number): void {
|
|
221
|
+
if (ms <= 0) return;
|
|
222
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export class PaceGate {
|
|
226
|
+
private readonly now: () => number;
|
|
227
|
+
private readonly random: () => number;
|
|
228
|
+
private readonly onWait: ((wait: PaceWait) => void) | undefined;
|
|
229
|
+
|
|
230
|
+
constructor(
|
|
231
|
+
readonly policy: PacePolicy,
|
|
232
|
+
deps: PaceGateDeps = {},
|
|
233
|
+
) {
|
|
234
|
+
this.now = deps.now ?? Date.now;
|
|
235
|
+
this.random = deps.random ?? Math.random;
|
|
236
|
+
this.onWait = deps.onWait;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** A gate that never waits, for callers that opted out. */
|
|
240
|
+
static disabled(): PaceGate {
|
|
241
|
+
return new PaceGate({ ...pacePolicyFromEnv({}), enabled: false });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Reserve the next slot for `url` without sleeping. Returns how long the
|
|
246
|
+
* caller should wait before loading, or `null` when the URL is exempt or the
|
|
247
|
+
* gate is disabled. The reservation is recorded even when the wait is zero,
|
|
248
|
+
* so the next load of the same site owes a gap.
|
|
249
|
+
*/
|
|
250
|
+
reserve(url: string): PaceWait | null {
|
|
251
|
+
if (!this.policy.enabled) return null;
|
|
252
|
+
const site = paceSiteKey(url, this.policy.exemptSuffixes);
|
|
253
|
+
if (!site) return null;
|
|
254
|
+
const now = this.now();
|
|
255
|
+
const gap = this.policy.minMs + this.random() * (this.policy.maxMs - this.policy.minMs);
|
|
256
|
+
let readyAt = now;
|
|
257
|
+
withLedgerLock(this.policy.ledgerPath, this.now, () => {
|
|
258
|
+
const ledger = readLedger(this.policy.ledgerPath);
|
|
259
|
+
readyAt = Math.max(now, ledger.sites[site]?.nextAllowedAt ?? 0);
|
|
260
|
+
for (const [key, entry] of Object.entries(ledger.sites)) {
|
|
261
|
+
if (entry.nextAllowedAt < now - LEDGER_RETENTION_MS) delete ledger.sites[key];
|
|
262
|
+
}
|
|
263
|
+
ledger.sites[site] = { nextAllowedAt: Math.round(readyAt + gap), lastAt: readyAt };
|
|
264
|
+
writeLedger(this.policy.ledgerPath, ledger);
|
|
265
|
+
});
|
|
266
|
+
const wait = { site, waitMs: Math.max(0, Math.round(readyAt - now)) };
|
|
267
|
+
this.onWait?.(wait);
|
|
268
|
+
return wait;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Reserve a slot and wait it out asynchronously. */
|
|
272
|
+
async before(url: string): Promise<PaceWait | null> {
|
|
273
|
+
const wait = this.reserve(url);
|
|
274
|
+
if (wait && wait.waitMs > 0) {
|
|
275
|
+
await new Promise<void>((done) => setTimeout(done, wait.waitMs));
|
|
276
|
+
}
|
|
277
|
+
return wait;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Reserve a slot and block the thread for it. For `spawnSync`-driven clients. */
|
|
281
|
+
beforeSync(url: string): PaceWait | null {
|
|
282
|
+
const wait = this.reserve(url);
|
|
283
|
+
if (wait && wait.waitMs > 0) sleepSync(wait.waitMs);
|
|
284
|
+
return wait;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Gate for a CLI command run. `null` when the run passed `--no-pace`;
|
|
290
|
+
* otherwise the environment policy, with every non-zero wait reported through
|
|
291
|
+
* `log` so the operator can see why a load took longer.
|
|
292
|
+
*/
|
|
293
|
+
export function commandPaceGate(optedIn: boolean, log: (message: string) => void): PaceGate | null {
|
|
294
|
+
if (!optedIn) return null;
|
|
295
|
+
return new PaceGate(pacePolicyFromEnv(), {
|
|
296
|
+
onWait: (wait) => {
|
|
297
|
+
if (wait.waitMs > 0) log(describePaceWait(wait));
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Describe a wait for a human-facing log line. */
|
|
303
|
+
export function describePaceWait(wait: PaceWait): string {
|
|
304
|
+
const seconds = (wait.waitMs / 1_000).toFixed(1);
|
|
305
|
+
return `paced ${seconds}s before ${wait.site} (human pace; --no-pace or HARNERY_PACE=off skips)`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function readLedger(path: string): Ledger {
|
|
309
|
+
try {
|
|
310
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<Ledger>;
|
|
311
|
+
if (parsed && parsed.version === 1 && parsed.sites && typeof parsed.sites === "object") {
|
|
312
|
+
const sites: Record<string, LedgerEntry> = {};
|
|
313
|
+
for (const [key, entry] of Object.entries(parsed.sites)) {
|
|
314
|
+
if (
|
|
315
|
+
entry &&
|
|
316
|
+
typeof entry.nextAllowedAt === "number" &&
|
|
317
|
+
Number.isFinite(entry.nextAllowedAt) &&
|
|
318
|
+
typeof entry.lastAt === "number" &&
|
|
319
|
+
Number.isFinite(entry.lastAt)
|
|
320
|
+
) {
|
|
321
|
+
sites[key] = { nextAllowedAt: entry.nextAllowedAt, lastAt: entry.lastAt };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return { version: 1, sites };
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// Missing or corrupt ledger: start fresh. A lost ledger costs one unpaced
|
|
328
|
+
// load per site, never a crash.
|
|
329
|
+
}
|
|
330
|
+
return { version: 1, sites: {} };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function writeLedger(path: string, ledger: Ledger): void {
|
|
334
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
335
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
336
|
+
writeFileSync(tmp, `${JSON.stringify(ledger)}\n`, { mode: 0o600 });
|
|
337
|
+
renameSync(tmp, path);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Serialize ledger read-modify-write across processes with an exclusive lock
|
|
342
|
+
* file. The lock is best-effort: a stale lock from a dead process is reclaimed,
|
|
343
|
+
* and a caller that cannot get the lock within `LOCK_WAIT_MS` proceeds without
|
|
344
|
+
* it rather than blocking a page load on a bookkeeping file.
|
|
345
|
+
*/
|
|
346
|
+
function withLedgerLock(path: string, now: () => number, fn: () => void): void {
|
|
347
|
+
const lockPath = `${path}.lock`;
|
|
348
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
349
|
+
const start = now();
|
|
350
|
+
let held = false;
|
|
351
|
+
while (!held && now() - start < LOCK_WAIT_MS) {
|
|
352
|
+
try {
|
|
353
|
+
closeSync(openSync(lockPath, "wx", 0o600));
|
|
354
|
+
held = true;
|
|
355
|
+
} catch {
|
|
356
|
+
try {
|
|
357
|
+
if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
|
|
358
|
+
rmSync(lockPath, { force: true });
|
|
359
|
+
} catch {
|
|
360
|
+
// Lock vanished between the failed open and the stat; retry.
|
|
361
|
+
}
|
|
362
|
+
sleepSync(LOCK_POLL_MS);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
fn();
|
|
367
|
+
} finally {
|
|
368
|
+
if (held) rmSync(lockPath, { force: true });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headers the tunnel gate sends to the upstream after the Host rewrite.
|
|
3
|
+
*
|
|
4
|
+
* The gate replaces Host so PHP (and other vhost-routed servers) see the
|
|
5
|
+
* configured review host. Callers that compare Origin to Host then fail,
|
|
6
|
+
* because the browser still sends the public tunnel hostname. Copy that
|
|
7
|
+
* original Host into X-Forwarded-Host, overwriting any client-supplied
|
|
8
|
+
* value, so the upstream can recover the hostname the browser used.
|
|
9
|
+
*/
|
|
10
|
+
export function applyUpstreamHeaders(headers: Headers, vhost: string): void {
|
|
11
|
+
const originalHost = headers.get("host") ?? "";
|
|
12
|
+
headers.set("host", vhost);
|
|
13
|
+
if (originalHost) headers.set("x-forwarded-host", originalHost);
|
|
14
|
+
headers.set("accept-encoding", "identity");
|
|
15
|
+
headers.delete("connection");
|
|
16
|
+
}
|
package/src/lib/tunnel/gate.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// Reverse-proxy worker spawned by `tunnel up`. Listens on 127.0.0.1:<port>
|
|
2
2
|
// and forwards HTTP + WebSocket requests to an upstream with a Host header
|
|
3
|
-
// rewrite
|
|
4
|
-
// body but retains the encoding
|
|
5
|
-
// downstream). Cloudflare provider mode
|
|
6
|
-
// allowlist; trusted local proxy mode lets
|
|
3
|
+
// rewrite, the original Host copied to X-Forwarded-Host, and Content-Encoding
|
|
4
|
+
// stripped (Bun's fetch auto-decompresses the body but retains the encoding
|
|
5
|
+
// header, which breaks browser decoding downstream). Cloudflare provider mode
|
|
6
|
+
// checks CF-Connecting-IP against an allowlist; trusted local proxy mode lets
|
|
7
|
+
// the local exposer own access.
|
|
7
8
|
//
|
|
8
9
|
// Runs detached, outside the CLI command framework; no command context is
|
|
9
10
|
// available; stdout/stderr is captured into .cache/tunnel/gate.log by the
|
|
10
11
|
// spawner.
|
|
11
12
|
|
|
12
13
|
import { renderTunnelErrorPage } from "./error-page";
|
|
14
|
+
import { applyUpstreamHeaders } from "./forward-headers";
|
|
13
15
|
|
|
14
16
|
// `--port`/`--name` are also passed on argv (not just env) so the gate's port
|
|
15
17
|
// and instance name show up in its process command line. That's what lets
|
|
@@ -39,6 +41,7 @@ const UPSTREAM_WS = `ws://${TARGET}`;
|
|
|
39
41
|
|
|
40
42
|
interface WsData {
|
|
41
43
|
path: string;
|
|
44
|
+
originalHost: string;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
function requestDetails(req: Request, url: URL) {
|
|
@@ -94,16 +97,18 @@ const server = Bun.serve<WsData, never>({
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
97
|
-
if (
|
|
100
|
+
if (
|
|
101
|
+
server.upgrade(req, {
|
|
102
|
+
data: { path: url.pathname + url.search, originalHost: req.headers.get("host") ?? "" },
|
|
103
|
+
})
|
|
104
|
+
) {
|
|
98
105
|
return undefined;
|
|
99
106
|
}
|
|
100
107
|
return new Response("WS upgrade failed\n", { status: 500 });
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
const headers = new Headers(req.headers);
|
|
104
|
-
headers
|
|
105
|
-
headers.set("accept-encoding", "identity");
|
|
106
|
-
headers.delete("connection");
|
|
111
|
+
applyUpstreamHeaders(headers, VHOST);
|
|
107
112
|
|
|
108
113
|
const init: RequestInit = {
|
|
109
114
|
method: req.method,
|
|
@@ -156,11 +161,14 @@ const server = Bun.serve<WsData, never>({
|
|
|
156
161
|
},
|
|
157
162
|
websocket: {
|
|
158
163
|
open(ws) {
|
|
159
|
-
const { path } = ws.data;
|
|
164
|
+
const { path, originalHost } = ws.data;
|
|
160
165
|
// Bun's WebSocket constructor accepts a `headers` option at runtime
|
|
161
166
|
// even though the standard lib.dom.d.ts WebSocket type doesn't.
|
|
162
167
|
const upstream = new WebSocket(UPSTREAM_WS + path, {
|
|
163
|
-
headers: {
|
|
168
|
+
headers: {
|
|
169
|
+
host: VHOST,
|
|
170
|
+
...(originalHost ? { "x-forwarded-host": originalHost } : {}),
|
|
171
|
+
},
|
|
164
172
|
} as unknown as undefined);
|
|
165
173
|
upstream.binaryType = "arraybuffer";
|
|
166
174
|
(ws as unknown as { upstream?: WebSocket }).upstream = upstream;
|