@fitchmultz/pi-subagents 0.44.7 → 0.46.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/CHANGELOG.md +264 -7
- package/LICENSE +1 -0
- package/README.md +58 -1483
- package/agents/context-builder.md +19 -1
- package/agents/debugger.md +10 -3
- package/agents/delegate.md +1 -0
- package/agents/fixer.md +4 -1
- package/agents/oracle.md +13 -2
- package/agents/planner.md +14 -3
- package/agents/researcher.md +11 -2
- package/agents/reviewer-claude.md +11 -2
- package/agents/reviewer-gpt.md +12 -2
- package/agents/reviewer-ponytail.md +11 -1
- package/agents/reviewer-security.md +13 -2
- package/agents/reviewer.md +12 -3
- package/agents/scout.md +12 -1
- package/agents/ui-designer.md +10 -3
- package/agents/watcher.md +1 -0
- package/agents/worker.md +8 -2
- package/agents/writer.md +6 -3
- package/dist/agents/agent-defaults.js +9 -0
- package/dist/agents/agent-definition.js +196 -0
- package/dist/agents/agent-management.js +64 -905
- package/dist/agents/agent-scope.js +4 -2
- package/dist/agents/agent-selection.js +10 -5
- package/dist/agents/agent-serializer.js +55 -67
- package/dist/agents/agents.js +75 -716
- package/dist/agents/builtin-overrides.js +218 -0
- package/dist/agents/chain-serializer.js +3 -367
- package/dist/agents/config-values.js +52 -0
- package/dist/agents/discovery-paths.js +91 -0
- package/dist/agents/frontmatter.js +15 -5
- package/dist/agents/identity.js +25 -8
- package/dist/agents/json-chain.js +254 -0
- package/dist/agents/management-config.js +332 -0
- package/dist/agents/management-format.js +231 -0
- package/dist/agents/management-persistence.js +287 -0
- package/dist/agents/management-targets.js +136 -0
- package/dist/agents/management-warnings.js +81 -0
- package/dist/agents/markdown-chain.js +210 -0
- package/dist/agents/package-agents.js +154 -0
- package/dist/agents/skill-files.js +157 -0
- package/dist/agents/skill-search-paths.js +246 -0
- package/dist/agents/skill-types.js +1 -0
- package/dist/agents/skills.js +54 -499
- package/dist/extension/acceptance-schema.js +55 -0
- package/dist/extension/build-info.js +1 -1
- package/dist/extension/chain-schema.js +196 -0
- package/dist/extension/child-runtime.js +237 -0
- package/dist/extension/compact-tools.js +122 -51
- package/dist/extension/config-schema.js +29 -0
- package/dist/extension/config.js +27 -11
- package/dist/extension/control-notice-schema.js +37 -0
- package/dist/extension/control-notices.js +13 -6
- package/dist/extension/delegation-bridges.js +43 -0
- package/dist/extension/doctor.js +48 -25
- package/dist/extension/everyday-schemas.js +207 -0
- package/dist/extension/fanout-child.js +18 -319
- package/dist/extension/index.js +8 -563
- package/dist/extension/message-renderers.js +225 -0
- package/dist/extension/nested-control-inbox.js +154 -0
- package/dist/extension/parent-runtime.js +339 -0
- package/dist/extension/primary-tool.js +69 -0
- package/dist/extension/result-snapshot.js +22 -0
- package/dist/extension/runtime-reload.js +62 -0
- package/dist/extension/runtime-storage.js +43 -0
- package/dist/extension/schema-overrides.js +75 -0
- package/dist/extension/schemas.js +265 -336
- package/dist/extension/tool-input.js +27 -12
- package/dist/extension/tool-result.js +19 -4
- package/dist/history/answer-texts.js +57 -0
- package/dist/history/canonical-result.js +76 -43
- package/dist/history/canonical-selection.js +40 -0
- package/dist/history/database-generation.js +82 -0
- package/dist/history/database.js +57 -33
- package/dist/history/entry-ingestion.js +61 -0
- package/dist/history/index.js +242 -97
- package/dist/history/ingest.js +168 -331
- package/dist/history/output-digests.js +40 -0
- package/dist/history/preview.js +210 -0
- package/dist/history/prune.js +33 -0
- package/dist/history/queries.js +60 -349
- package/dist/history/query-scope.js +190 -0
- package/dist/history/rows.js +77 -0
- package/dist/history/run-projection.js +137 -0
- package/dist/history/run-queries.js +153 -0
- package/dist/history/schema.js +31 -0
- package/dist/history/search-queries.js +163 -0
- package/dist/history/selected-record.js +108 -0
- package/dist/history/source-file.js +45 -0
- package/dist/history/source-watches.js +141 -0
- package/dist/history/store.js +164 -153
- package/dist/history/text-chunks.js +169 -0
- package/dist/history/text.js +35 -0
- package/dist/history/transcript-queries.js +177 -0
- package/dist/history/types.js +5 -1
- package/dist/history/values.js +36 -0
- package/dist/history/wire-inputs.js +122 -0
- package/dist/history/wire-models.js +38 -0
- package/dist/history/worker-runtime.js +441 -0
- package/dist/history/worker.js +66 -462
- package/dist/intercom/intercom-bridge.js +48 -20
- package/dist/intercom/live-intercom.js +80 -23
- package/dist/intercom/nested-result-tree.js +163 -0
- package/dist/intercom/result-delivery.js +40 -0
- package/dist/intercom/result-intercom.js +178 -261
- package/dist/pi-intercom/agent-dir.js +7 -3
- package/dist/pi-intercom/async.js +27 -0
- package/dist/pi-intercom/bridge-ask.js +57 -0
- package/dist/pi-intercom/bridge-config.js +89 -0
- package/dist/pi-intercom/bridge-http.js +86 -0
- package/dist/pi-intercom/bridge-launch-policy.js +88 -0
- package/dist/pi-intercom/bridge-launch.js +217 -0
- package/dist/pi-intercom/bridge-protocol.js +106 -0
- package/dist/pi-intercom/bridge-routes.js +72 -0
- package/dist/pi-intercom/bridge-rpc-process.js +264 -0
- package/dist/pi-intercom/bridge-session.js +420 -0
- package/dist/pi-intercom/bridge.js +221 -606
- package/dist/pi-intercom/broker/broker.js +236 -244
- package/dist/pi-intercom/broker/client.js +189 -304
- package/dist/pi-intercom/broker/connection.js +165 -0
- package/dist/pi-intercom/broker/framing.js +5 -3
- package/dist/pi-intercom/broker/paths.js +4 -1
- package/dist/pi-intercom/broker/pid.js +53 -25
- package/dist/pi-intercom/broker/snapshot-reader.js +78 -0
- package/dist/pi-intercom/broker/snapshots.js +188 -0
- package/dist/pi-intercom/broker/spawn.js +36 -25
- package/dist/pi-intercom/config.js +85 -71
- package/dist/pi-intercom/connection.js +297 -0
- package/dist/pi-intercom/identity.js +58 -0
- package/dist/pi-intercom/inbound-delivery.js +356 -0
- package/dist/pi-intercom/inbound-journal.js +303 -0
- package/dist/pi-intercom/inbound-record.js +113 -0
- package/dist/pi-intercom/inbound.js +179 -0
- package/dist/pi-intercom/index.js +79 -2933
- package/dist/pi-intercom/inspection.js +62 -0
- package/dist/pi-intercom/interview.js +236 -0
- package/dist/pi-intercom/lifecycle.js +168 -0
- package/dist/pi-intercom/message-format.js +69 -0
- package/dist/pi-intercom/outbound.js +304 -0
- package/dist/pi-intercom/peer-awareness.js +66 -0
- package/dist/pi-intercom/reply-tracker.js +62 -43
- package/dist/pi-intercom/reply-wait.js +201 -0
- package/dist/pi-intercom/runtime-types.js +3 -0
- package/dist/pi-intercom/session-events.js +180 -0
- package/dist/pi-intercom/session-targets.js +42 -34
- package/dist/pi-intercom/subagent-events.js +238 -0
- package/dist/pi-intercom/subagent-live-events.js +129 -86
- package/dist/pi-intercom/supervisor.js +282 -0
- package/dist/pi-intercom/tool-arguments.js +74 -0
- package/dist/pi-intercom/tool-registration.js +95 -0
- package/dist/pi-intercom/tool-results.js +38 -0
- package/dist/pi-intercom/tool-schemas.js +82 -0
- package/dist/pi-intercom/tools.js +51 -0
- package/dist/pi-intercom/topic-actions.js +177 -0
- package/dist/pi-intercom/topics.js +177 -92
- package/dist/pi-intercom/transport.js +1 -0
- package/dist/pi-intercom/types.js +151 -114
- package/dist/pi-intercom/ui/compose-paste.js +93 -0
- package/dist/pi-intercom/ui/compose.js +121 -143
- package/dist/pi-intercom/ui/inline-message.js +59 -59
- package/dist/pi-intercom/ui/message-renderers.js +147 -0
- package/dist/pi-intercom/ui/overlay.js +181 -0
- package/dist/pi-intercom/ui/session-format.js +124 -0
- package/dist/pi-intercom/ui/session-list.js +76 -43
- package/dist/pi-intercom/ui/tool-renderers.js +110 -0
- package/dist/pi-intercom/ui/topic-view.js +88 -0
- package/dist/pi-intercom/validation.js +59 -0
- package/dist/runs/background/async-chain-plan.js +265 -0
- package/dist/runs/background/async-control.js +134 -58
- package/dist/runs/background/async-execution.js +136 -633
- package/dist/runs/background/async-job-admission.js +40 -0
- package/dist/runs/background/async-job-projection.js +169 -0
- package/dist/runs/background/async-job-tracker.js +297 -327
- package/dist/runs/background/async-launch.js +216 -0
- package/dist/runs/background/async-plan.js +116 -0
- package/dist/runs/background/async-result-file.js +47 -23
- package/dist/runs/background/async-resume.js +195 -253
- package/dist/runs/background/async-run-discovery.js +213 -0
- package/dist/runs/background/async-run-location.js +135 -0
- package/dist/runs/background/async-run-record.js +1 -0
- package/dist/runs/background/async-run-repair.js +223 -0
- package/dist/runs/background/async-run-summary.js +1 -0
- package/dist/runs/background/async-single-plan.js +105 -0
- package/dist/runs/background/async-status-projection.js +155 -0
- package/dist/runs/background/async-status-render.js +117 -0
- package/dist/runs/background/async-status-validation.js +181 -0
- package/dist/runs/background/async-status.js +3 -442
- package/dist/runs/background/async-value.js +21 -0
- package/dist/runs/background/completion-dedupe.js +36 -12
- package/dist/runs/background/completion-delivery.js +211 -205
- package/dist/runs/background/completion-receipts.js +237 -0
- package/dist/runs/background/legacy-status-acceptance.js +34 -0
- package/dist/runs/background/notify.js +96 -48
- package/dist/runs/background/parallel-groups.js +36 -19
- package/dist/runs/background/result-payload.js +178 -0
- package/dist/runs/background/result-watcher.js +348 -339
- package/dist/runs/background/run-id-resolver.js +53 -33
- package/dist/runs/background/run-schemas.js +189 -0
- package/dist/runs/background/run-status-contracts.js +14 -0
- package/dist/runs/background/run-status-render.js +270 -0
- package/dist/runs/background/run-status.js +231 -339
- package/dist/runs/background/runner-attempt-output.js +155 -0
- package/dist/runs/background/runner-attempt.js +263 -0
- package/dist/runs/background/runner-child-executor.js +166 -0
- package/dist/runs/background/runner-child-observer.js +192 -0
- package/dist/runs/background/runner-completion-summary.js +71 -0
- package/dist/runs/background/runner-completion.js +211 -0
- package/dist/runs/background/runner-config.js +1 -0
- package/dist/runs/background/runner-contract.js +1 -0
- package/dist/runs/background/runner-dynamic.js +121 -0
- package/dist/runs/background/runner-finalization.js +149 -0
- package/dist/runs/background/runner-graph.js +68 -0
- package/dist/runs/background/runner-initial-status.js +60 -0
- package/dist/runs/background/runner-lifecycle.js +142 -0
- package/dist/runs/background/runner-monitor.js +309 -0
- package/dist/runs/background/runner-parallel.js +204 -0
- package/dist/runs/background/runner-prepare-step.js +86 -0
- package/dist/runs/background/runner-reporting.js +153 -0
- package/dist/runs/background/runner-status.js +148 -0
- package/dist/runs/background/runner-step-output.js +92 -0
- package/dist/runs/background/runner-step-record.js +45 -0
- package/dist/runs/background/runner-step.js +112 -0
- package/dist/runs/background/runner-streaming.js +173 -0
- package/dist/runs/background/runner-workflow.js +213 -0
- package/dist/runs/background/stale-run-reconciler.js +155 -292
- package/dist/runs/background/subagent-runner-launcher.js +33 -13
- package/dist/runs/background/subagent-runner.js +51 -1842
- package/dist/runs/background/top-level-async.js +2 -1
- package/dist/runs/foreground/chain-clarify.js +293 -1017
- package/dist/runs/foreground/clarification-draft.js +87 -0
- package/dist/runs/foreground/clarify-contracts.js +1 -0
- package/dist/runs/foreground/clarify-invocation.js +129 -38
- package/dist/runs/foreground/clarify-text-editor.js +210 -0
- package/dist/runs/foreground/clarify-view.js +167 -0
- package/dist/runs/foreground/execution-input.js +226 -353
- package/dist/runs/foreground/execution-routing.js +33 -0
- package/dist/runs/foreground/execution-timeout.js +81 -0
- package/dist/runs/foreground/foreground-control.js +222 -763
- package/dist/runs/foreground/foreground-memory.js +99 -0
- package/dist/runs/foreground/invocation-context.js +55 -0
- package/dist/runs/foreground/invocation-execution.js +331 -0
- package/dist/runs/foreground/invocation-expansion.js +48 -0
- package/dist/runs/foreground/management-actions.js +295 -0
- package/dist/runs/foreground/nested-control.js +179 -0
- package/dist/runs/foreground/question-continuation.js +78 -0
- package/dist/runs/foreground/question-control.js +155 -50
- package/dist/runs/foreground/resume-target.js +221 -0
- package/dist/runs/foreground/revival-launch-policy.js +58 -0
- package/dist/runs/foreground/run-async-path.js +190 -178
- package/dist/runs/foreground/run-interrupt.js +164 -0
- package/dist/runs/foreground/run-nudge.js +278 -0
- package/dist/runs/foreground/saved-revival.js +251 -0
- package/dist/runs/foreground/subagent-executor.js +135 -424
- package/dist/runs/foreground/subagent-params.js +67 -30
- package/dist/runs/foreground/wait-observation.js +104 -0
- package/dist/runs/foreground/wait-registration.js +17 -0
- package/dist/runs/foreground/wait-run.js +283 -123
- package/dist/runs/shared/acceptance-contract.js +193 -99
- package/dist/runs/shared/acceptance-defaults.js +1 -0
- package/dist/runs/shared/acceptance-evaluation.js +201 -181
- package/dist/runs/shared/acceptance-finalization.js +256 -180
- package/dist/runs/shared/acceptance-outcome.js +53 -0
- package/dist/runs/shared/acceptance-reports.js +137 -72
- package/dist/runs/shared/acceptance-submission.js +167 -0
- package/dist/runs/shared/acceptance-verification.js +100 -0
- package/dist/runs/shared/chain-outputs.js +80 -52
- package/dist/runs/shared/child-attempt-types.js +1 -0
- package/dist/runs/shared/child-attempt.js +314 -667
- package/dist/runs/shared/child-boundaries.js +173 -0
- package/dist/runs/shared/child-context-filter.js +44 -0
- package/dist/runs/shared/child-event-observer.js +390 -0
- package/dist/runs/shared/child-execution-cwd.js +73 -43
- package/dist/runs/shared/child-execution-outcome.js +72 -0
- package/dist/runs/shared/child-invocation.js +54 -0
- package/dist/runs/shared/child-message-validation.js +215 -0
- package/dist/runs/shared/child-native-accounting.js +106 -0
- package/dist/runs/shared/child-native-observation.js +69 -0
- package/dist/runs/shared/child-observation-spool.js +164 -0
- package/dist/runs/shared/child-observations.js +157 -76
- package/dist/runs/shared/child-presence.js +3 -0
- package/dist/runs/shared/child-route-env.js +119 -0
- package/dist/runs/shared/child-stream-observer.js +221 -0
- package/dist/runs/shared/child-stream-projection.js +137 -0
- package/dist/runs/shared/child-structured-tool.js +27 -0
- package/dist/runs/shared/claude-code.js +121 -158
- package/dist/runs/shared/claude-model.js +42 -0
- package/dist/runs/shared/claude-session.js +83 -0
- package/dist/runs/shared/claude-types.js +1 -0
- package/dist/runs/shared/completion-guard.js +18 -18
- package/dist/runs/shared/control-event-policy.js +8 -0
- package/dist/runs/shared/control-notification-owner.js +13 -0
- package/dist/runs/shared/dynamic-fanout.js +188 -173
- package/dist/runs/shared/dynamic-item.js +102 -0
- package/dist/runs/shared/history-index.js +62 -36
- package/dist/runs/shared/legacy-async-recovery.js +114 -0
- package/dist/runs/shared/legacy-output-recovery.js +148 -0
- package/dist/runs/shared/mcp-config-schema.js +36 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +147 -116
- package/dist/runs/shared/model-fallback.js +97 -38
- package/dist/runs/shared/mutating-tool-guard.js +128 -88
- package/dist/runs/shared/native-finalization-types.js +1 -0
- package/dist/runs/shared/native-finalization.js +155 -76
- package/dist/runs/shared/native-usage.js +238 -98
- package/dist/runs/shared/nested-control-records.js +128 -0
- package/dist/runs/shared/nested-events.js +8 -700
- package/dist/runs/shared/nested-path.js +32 -28
- package/dist/runs/shared/nested-projection.js +155 -0
- package/dist/runs/shared/nested-protocol.js +18 -0
- package/dist/runs/shared/nested-registry.js +191 -0
- package/dist/runs/shared/nested-render.js +106 -58
- package/dist/runs/shared/nested-route.js +220 -0
- package/dist/runs/shared/nested-validation.js +216 -0
- package/dist/runs/shared/owned-child-evidence.js +125 -0
- package/dist/runs/shared/owned-run-list.js +146 -0
- package/dist/runs/shared/owned-run-observation.js +267 -0
- package/dist/runs/shared/owned-run-progress.js +109 -0
- package/dist/runs/shared/owned-run-read-state.js +1 -0
- package/dist/runs/shared/owned-run-results.js +202 -0
- package/dist/runs/shared/owned-run-status.js +180 -0
- package/dist/runs/shared/owned-run-view.js +51 -0
- package/dist/runs/shared/owned-workflow-details.js +17 -0
- package/dist/runs/shared/owner-output.js +43 -0
- package/dist/runs/shared/parallel-utils.js +36 -28
- package/dist/runs/shared/parent-receipts.js +90 -32
- package/dist/runs/shared/parent-usage.js +169 -57
- package/dist/runs/shared/pi-args.js +108 -201
- package/dist/runs/shared/pi-launch-input.js +1 -0
- package/dist/runs/shared/pi-session-launch.js +86 -0
- package/dist/runs/shared/pi-spawn.js +35 -21
- package/dist/runs/shared/run-history-queries.js +126 -35
- package/dist/runs/shared/run-history.js +101 -35
- package/dist/runs/shared/run-metadata-paths.js +14 -0
- package/dist/runs/shared/run-metadata.js +158 -0
- package/dist/runs/shared/run-persistence.js +131 -0
- package/dist/runs/shared/run-records.js +9 -679
- package/dist/runs/shared/run-restoration.js +338 -0
- package/dist/runs/shared/run-state-owner.js +26 -0
- package/dist/runs/shared/session-cwd-preload.js +27 -18
- package/dist/runs/shared/single-output.js +148 -70
- package/dist/runs/shared/stable-value.js +15 -0
- package/dist/runs/shared/streaming-text.js +23 -12
- package/dist/runs/shared/structured-output.js +25 -13
- package/dist/runs/shared/subagent-control.js +167 -100
- package/dist/runs/shared/subagent-prompt-runtime.js +27 -130
- package/dist/runs/shared/subagent-tool-loop-guard.js +81 -52
- package/dist/runs/shared/supervisor-question-state.js +145 -0
- package/dist/runs/shared/supervisor-questions.js +185 -254
- package/dist/runs/shared/workflow-graph.js +259 -167
- package/dist/runs/shared/workflow-policy.js +120 -48
- package/dist/runs/shared/worktree-contract.js +1 -0
- package/dist/runs/shared/worktree-hook.js +138 -0
- package/dist/runs/shared/worktree-location.js +47 -0
- package/dist/runs/shared/worktree-preservation.js +8 -0
- package/dist/runs/shared/worktree-process.js +107 -0
- package/dist/runs/shared/worktree.js +183 -322
- package/dist/shared/agent-context-policy.js +34 -44
- package/dist/shared/agent-dir.js +14 -0
- package/dist/shared/artifacts.js +30 -12
- package/dist/shared/atomic-json.js +4 -1
- package/dist/shared/chain-files.js +72 -0
- package/dist/shared/concurrency.js +18 -0
- package/dist/shared/content-preview.js +115 -0
- package/dist/shared/cooperative.js +5 -2
- package/dist/shared/execution-cwd.js +13 -5
- package/dist/shared/file-coalescer.js +2 -1
- package/dist/shared/fork-context.js +10 -12
- package/dist/shared/formatters.js +45 -19
- package/dist/shared/journal-frames.js +164 -0
- package/dist/shared/journal-projections.js +121 -0
- package/dist/shared/journal-reader.js +6 -407
- package/dist/shared/json-projection.js +191 -0
- package/dist/shared/lazy-tools.js +19 -8
- package/dist/shared/message-output.js +116 -0
- package/dist/shared/model-info.js +31 -14
- package/dist/shared/native-import.js +20 -0
- package/dist/shared/native-journal.js +140 -0
- package/dist/shared/native-session-loader.js +17 -0
- package/dist/shared/native-session.js +2 -13
- package/dist/shared/native-tui.js +7 -2
- package/dist/shared/native-typebox.js +15 -3
- package/dist/shared/output-page.js +63 -0
- package/dist/shared/post-exit-stdio-guard.js +128 -88
- package/dist/shared/prompt-sections.js +2 -0
- package/dist/shared/session-entries.js +70 -35
- package/dist/shared/session-identity.js +5 -3
- package/dist/shared/settings.js +62 -192
- package/dist/shared/status-format.js +97 -37
- package/dist/shared/task-file-policy.js +24 -0
- package/dist/shared/temp-root.js +44 -15
- package/dist/shared/types/acceptance.js +1 -0
- package/dist/shared/types/async.js +1 -0
- package/dist/shared/types/config.js +1 -0
- package/dist/shared/types/details.js +1 -0
- package/dist/shared/types/history.js +1 -0
- package/dist/shared/types/inputs.js +1 -0
- package/dist/shared/types/intercom.js +1 -0
- package/dist/shared/types/launch.js +1 -0
- package/dist/shared/types/messages.js +1 -0
- package/dist/shared/types/nested.js +1 -0
- package/dist/shared/types/owned-runs.js +1 -0
- package/dist/shared/types/progress.js +1 -0
- package/dist/shared/types/questions.js +1 -0
- package/dist/shared/types/results.js +1 -0
- package/dist/shared/types/state.js +1 -0
- package/dist/shared/types/usage.js +1 -0
- package/dist/shared/types/workflow.js +1 -0
- package/dist/shared/types.js +75 -30
- package/dist/shared/unknown.js +52 -0
- package/dist/shared/utils.js +54 -260
- package/dist/slash/command-discovery.js +82 -0
- package/dist/slash/command-input.js +190 -0
- package/dist/slash/prompt-template-bridge.js +94 -270
- package/dist/slash/prompt-template-contract.js +77 -0
- package/dist/slash/prompt-template-results.js +125 -0
- package/dist/slash/request-protocol.js +68 -0
- package/dist/slash/result-delivery.js +237 -0
- package/dist/slash/saved-chain.js +78 -0
- package/dist/slash/slash-bridge.js +123 -89
- package/dist/slash/slash-commands.js +101 -572
- package/dist/slash/slash-live-state.js +68 -47
- package/dist/tui/action-hints.js +50 -22
- package/dist/tui/activity-lines.js +32 -0
- package/dist/tui/agent-browser.js +290 -0
- package/dist/tui/agent-controls.js +289 -0
- package/dist/tui/agent-conversation.js +444 -0
- package/dist/tui/agent-dock.js +171 -0
- package/dist/tui/agent-history.js +50 -183
- package/dist/tui/agent-model.js +62 -0
- package/dist/tui/agent-picker.js +262 -0
- package/dist/tui/agent-view.js +232 -1564
- package/dist/tui/compact-results.js +193 -0
- package/dist/tui/conversation-details.js +190 -0
- package/dist/tui/conversation-frame.js +135 -0
- package/dist/tui/conversation-history.js +201 -0
- package/dist/tui/conversation-menu.js +78 -0
- package/dist/tui/conversation-notices.js +79 -0
- package/dist/tui/conversation-selection.js +58 -0
- package/dist/tui/conversation-viewport.js +129 -0
- package/dist/tui/direction-message.js +27 -0
- package/dist/tui/display.js +282 -0
- package/dist/tui/editor-viewport.js +18 -0
- package/dist/tui/expanded-content.js +87 -0
- package/dist/tui/expanded-multi.js +170 -0
- package/dist/tui/expanded-single.js +65 -0
- package/dist/tui/history-cards.js +216 -0
- package/dist/tui/history-display.js +238 -0
- package/dist/tui/history-json.js +16 -0
- package/dist/tui/history-metadata.js +152 -0
- package/dist/tui/history-record.js +251 -0
- package/dist/tui/history-text.js +34 -0
- package/dist/tui/indexed-history.js +116 -0
- package/dist/tui/nested-widget.js +120 -0
- package/dist/tui/picker-page.js +59 -0
- package/dist/tui/render.js +58 -1501
- package/dist/tui/result-facts.js +64 -0
- package/dist/tui/task-observation.js +59 -0
- package/dist/tui/task-store.js +366 -0
- package/dist/tui/text-values.js +16 -0
- package/dist/tui/view-connections.js +47 -0
- package/dist/tui/view-model.js +94 -0
- package/dist/tui/view-persistence.js +51 -0
- package/dist/tui/view-ports.js +1 -0
- package/dist/tui/view-session.js +37 -0
- package/dist/tui/widget-details.js +144 -0
- package/dist/tui/widget-status.js +245 -0
- package/dist/tui/widget.js +145 -0
- package/dist/tui/workflow-labels.js +223 -0
- package/dist/tui/workflow-outcomes.js +124 -0
- package/docs/code-quality.md +38 -0
- package/docs/development.md +99 -0
- package/docs/intercom-bridge.md +77 -22
- package/docs/intercom.md +132 -106
- package/docs/performance.md +19 -17
- package/docs/reference.md +1472 -0
- package/generated/RunValidators.d.ts +28 -0
- package/generated/RunValidators.js +2 -0
- package/package.json +27 -6
- package/prompts/parallel-cleanup.md +3 -0
- package/prompts/parallel-context-build.md +3 -0
- package/prompts/parallel-handoff-plan.md +6 -0
- package/prompts/parallel-research.md +4 -0
- package/prompts/parallel-review.md +2 -0
- package/prompts/review-loop.md +2 -0
- package/scripts/build.mjs +129 -85
- package/scripts/check-compat.mjs +48 -11
- package/scripts/compat-host.mjs +35 -7
- package/scripts/generate-run-schemas.mjs +210 -0
- package/scripts/intercom-bridge-credentials.mjs +158 -29
- package/scripts/intercom-remote.mjs +213 -52
- package/scripts/local-install-smoke.mjs +47 -37
- package/scripts/native-package-smoke.mjs +135 -25
- package/scripts/package-smoke.mjs +360 -140
- package/scripts/prepare.mjs +54 -34
- package/scripts/real-pi-processes.mjs +261 -0
- package/scripts/real-pi-smoke.mjs +432 -351
- package/scripts/run-tests.mjs +29 -15
- package/skills/pi-intercom/SKILL.md +11 -11
- package/skills/pi-intercom/references/peer-sessions.md +7 -2
- package/skills/pi-subagents/SKILL.md +1 -1
- package/skills/pi-subagents/references/full-orchestration-guide.md +254 -112
- package/src/agents/agent-defaults.ts +13 -0
- package/src/agents/agent-definition.ts +253 -0
- package/src/agents/agent-management.ts +111 -808
- package/src/agents/agent-scope.ts +7 -3
- package/src/agents/agent-selection.ts +26 -16
- package/src/agents/agent-serializer.ts +84 -93
- package/src/agents/agents.ts +199 -906
- package/src/agents/builtin-overrides.ts +297 -0
- package/src/agents/chain-serializer.ts +3 -329
- package/src/agents/config-values.ts +57 -0
- package/src/agents/discovery-paths.ts +134 -0
- package/src/agents/frontmatter.ts +35 -17
- package/src/agents/identity.ts +49 -18
- package/src/agents/json-chain.ts +305 -0
- package/src/agents/management-config.ts +342 -0
- package/src/agents/management-format.ts +243 -0
- package/src/agents/management-persistence.ts +408 -0
- package/src/agents/management-targets.ts +260 -0
- package/src/agents/management-warnings.ts +115 -0
- package/src/agents/markdown-chain.ts +237 -0
- package/src/agents/package-agents.ts +231 -0
- package/src/agents/skill-files.ts +182 -0
- package/src/agents/skill-search-paths.ts +289 -0
- package/src/agents/skill-types.ts +31 -0
- package/src/agents/skills.ts +152 -610
- package/src/extension/acceptance-schema.ts +88 -0
- package/src/extension/chain-schema.ts +276 -0
- package/src/extension/child-runtime.ts +302 -0
- package/src/extension/compact-tools.ts +259 -102
- package/src/extension/config-schema.ts +59 -0
- package/src/extension/config.ts +39 -17
- package/src/extension/control-notice-schema.ts +55 -0
- package/src/extension/control-notices.ts +57 -33
- package/src/extension/delegation-bridges.ts +59 -0
- package/src/extension/doctor.ts +171 -135
- package/src/extension/everyday-schemas.ts +252 -0
- package/src/extension/fanout-child.ts +29 -308
- package/src/extension/index.ts +17 -642
- package/src/extension/message-renderers.ts +334 -0
- package/src/extension/nested-control-inbox.ts +196 -0
- package/src/extension/parent-runtime.ts +405 -0
- package/src/extension/primary-tool.ts +130 -0
- package/src/extension/result-snapshot.ts +35 -0
- package/src/extension/runtime-reload.ts +72 -0
- package/src/extension/runtime-storage.ts +49 -0
- package/src/extension/schema-overrides.ts +114 -0
- package/src/extension/schemas.ts +451 -418
- package/src/extension/tool-input.ts +40 -13
- package/src/extension/tool-result.ts +45 -14
- package/src/history/answer-texts.ts +74 -0
- package/src/history/canonical-result.ts +127 -51
- package/src/history/canonical-selection.ts +54 -0
- package/src/history/database-generation.ts +86 -0
- package/src/history/database.ts +86 -48
- package/src/history/entry-ingestion.ts +129 -0
- package/src/history/index.ts +426 -148
- package/src/history/ingest.ts +372 -342
- package/src/history/output-digests.ts +41 -0
- package/src/history/preview.ts +235 -0
- package/src/history/prune.ts +48 -0
- package/src/history/queries.ts +122 -256
- package/src/history/query-scope.ts +278 -0
- package/src/history/rows.ts +118 -0
- package/src/history/run-projection.ts +164 -0
- package/src/history/run-queries.ts +199 -0
- package/src/history/schema.ts +34 -0
- package/src/history/search-queries.ts +216 -0
- package/src/history/selected-record.ts +140 -0
- package/src/history/source-file.ts +51 -0
- package/src/history/source-watches.ts +156 -0
- package/src/history/store.ts +310 -197
- package/src/history/text-chunks.ts +181 -0
- package/src/history/text.ts +43 -0
- package/src/history/transcript-queries.ts +251 -0
- package/src/history/types.ts +27 -164
- package/src/history/values.ts +36 -0
- package/src/history/wire-inputs.ts +141 -0
- package/src/history/wire-models.ts +51 -0
- package/src/history/worker-runtime.ts +502 -0
- package/src/history/worker.ts +94 -336
- package/src/intercom/intercom-bridge.ts +136 -69
- package/src/intercom/live-intercom.ts +197 -92
- package/src/intercom/nested-result-tree.ts +213 -0
- package/src/intercom/result-delivery.ts +56 -0
- package/src/intercom/result-intercom.ts +364 -382
- package/src/pi-intercom/agent-dir.ts +9 -3
- package/src/pi-intercom/async.ts +33 -0
- package/src/pi-intercom/bridge-ask.ts +64 -0
- package/src/pi-intercom/bridge-config.ts +120 -0
- package/src/pi-intercom/bridge-http.ts +97 -0
- package/src/pi-intercom/bridge-launch-policy.ts +109 -0
- package/src/pi-intercom/bridge-launch.ts +317 -0
- package/src/pi-intercom/bridge-protocol.ts +123 -0
- package/src/pi-intercom/bridge-routes.ts +105 -0
- package/src/pi-intercom/bridge-rpc-process.ts +293 -0
- package/src/pi-intercom/bridge-session.ts +510 -0
- package/src/pi-intercom/bridge.ts +305 -549
- package/src/pi-intercom/broker/broker.ts +326 -263
- package/src/pi-intercom/broker/client.ts +273 -354
- package/src/pi-intercom/broker/connection.ts +192 -0
- package/src/pi-intercom/broker/framing.ts +11 -6
- package/src/pi-intercom/broker/paths.ts +28 -25
- package/src/pi-intercom/broker/pid.ts +68 -24
- package/src/pi-intercom/broker/snapshot-reader.ts +91 -0
- package/src/pi-intercom/broker/snapshots.ts +241 -0
- package/src/pi-intercom/broker/spawn.ts +58 -34
- package/src/pi-intercom/config.ts +113 -108
- package/src/pi-intercom/connection.ts +346 -0
- package/src/pi-intercom/identity.ts +71 -0
- package/src/pi-intercom/inbound-delivery.ts +450 -0
- package/src/pi-intercom/inbound-journal.ts +350 -0
- package/src/pi-intercom/inbound-record.ts +141 -0
- package/src/pi-intercom/inbound.ts +237 -0
- package/src/pi-intercom/index.ts +86 -2923
- package/src/pi-intercom/inspection.ts +82 -0
- package/src/pi-intercom/interview.ts +295 -0
- package/src/pi-intercom/lifecycle.ts +183 -0
- package/src/pi-intercom/message-format.ts +77 -0
- package/src/pi-intercom/outbound.ts +418 -0
- package/src/pi-intercom/peer-awareness.ts +84 -0
- package/src/pi-intercom/reply-tracker.ts +121 -59
- package/src/pi-intercom/reply-wait.ts +247 -0
- package/src/pi-intercom/runtime-types.ts +97 -0
- package/src/pi-intercom/session-events.ts +247 -0
- package/src/pi-intercom/session-targets.ts +124 -72
- package/src/pi-intercom/subagent-events.ts +299 -0
- package/src/pi-intercom/subagent-live-events.ts +212 -109
- package/src/pi-intercom/supervisor.ts +362 -0
- package/src/pi-intercom/tool-arguments.ts +120 -0
- package/src/pi-intercom/tool-registration.ts +121 -0
- package/src/pi-intercom/tool-results.ts +54 -0
- package/src/pi-intercom/tool-schemas.ts +134 -0
- package/src/pi-intercom/tools.ts +80 -0
- package/src/pi-intercom/topic-actions.ts +243 -0
- package/src/pi-intercom/topics.ts +267 -121
- package/src/pi-intercom/transport.ts +14 -0
- package/src/pi-intercom/types.ts +288 -190
- package/src/pi-intercom/ui/compose-paste.ts +109 -0
- package/src/pi-intercom/ui/compose.ts +173 -157
- package/src/pi-intercom/ui/inline-message.ts +80 -62
- package/src/pi-intercom/ui/message-renderers.ts +182 -0
- package/src/pi-intercom/ui/overlay.ts +238 -0
- package/src/pi-intercom/ui/session-format.ts +146 -0
- package/src/pi-intercom/ui/session-list.ts +145 -62
- package/src/pi-intercom/ui/tool-renderers.ts +148 -0
- package/src/pi-intercom/ui/topic-view.ts +107 -0
- package/src/pi-intercom/validation.ts +69 -0
- package/src/runs/background/async-chain-plan.ts +451 -0
- package/src/runs/background/async-control.ts +201 -71
- package/src/runs/background/async-execution.ts +206 -829
- package/src/runs/background/async-job-admission.ts +54 -0
- package/src/runs/background/async-job-projection.ts +225 -0
- package/src/runs/background/async-job-tracker.ts +416 -360
- package/src/runs/background/async-launch.ts +319 -0
- package/src/runs/background/async-plan.ts +285 -0
- package/src/runs/background/async-result-file.ts +79 -37
- package/src/runs/background/async-resume.ts +338 -285
- package/src/runs/background/async-run-discovery.ts +270 -0
- package/src/runs/background/async-run-location.ts +194 -0
- package/src/runs/background/async-run-record.ts +14 -0
- package/src/runs/background/async-run-repair.ts +307 -0
- package/src/runs/background/async-run-summary.ts +71 -0
- package/src/runs/background/async-single-plan.ts +168 -0
- package/src/runs/background/async-status-projection.ts +188 -0
- package/src/runs/background/async-status-render.ts +164 -0
- package/src/runs/background/async-status-validation.ts +238 -0
- package/src/runs/background/async-status.ts +9 -481
- package/src/runs/background/async-value.ts +29 -0
- package/src/runs/background/completion-dedupe.ts +103 -54
- package/src/runs/background/completion-delivery.ts +313 -187
- package/src/runs/background/completion-receipts.ts +309 -0
- package/src/runs/background/legacy-status-acceptance.ts +38 -0
- package/src/runs/background/notify.ts +167 -106
- package/src/runs/background/parallel-groups.ts +95 -46
- package/src/runs/background/result-payload.ts +268 -0
- package/src/runs/background/result-watcher.ts +484 -387
- package/src/runs/background/run-id-resolver.ts +126 -57
- package/src/runs/background/run-schemas.ts +240 -0
- package/src/runs/background/run-status-contracts.ts +46 -0
- package/src/runs/background/run-status-render.ts +364 -0
- package/src/runs/background/run-status.ts +338 -356
- package/src/runs/background/runner-attempt-output.ts +281 -0
- package/src/runs/background/runner-attempt.ts +354 -0
- package/src/runs/background/runner-child-executor.ts +232 -0
- package/src/runs/background/runner-child-observer.ts +270 -0
- package/src/runs/background/runner-completion-summary.ts +106 -0
- package/src/runs/background/runner-completion.ts +274 -0
- package/src/runs/background/runner-config.ts +52 -0
- package/src/runs/background/runner-contract.ts +51 -0
- package/src/runs/background/runner-dynamic.ts +185 -0
- package/src/runs/background/runner-finalization.ts +203 -0
- package/src/runs/background/runner-graph.ts +91 -0
- package/src/runs/background/runner-initial-status.ts +72 -0
- package/src/runs/background/runner-lifecycle.ts +159 -0
- package/src/runs/background/runner-monitor.ts +399 -0
- package/src/runs/background/runner-parallel.ts +280 -0
- package/src/runs/background/runner-prepare-step.ts +128 -0
- package/src/runs/background/runner-reporting.ts +192 -0
- package/src/runs/background/runner-status.ts +220 -0
- package/src/runs/background/runner-step-output.ts +158 -0
- package/src/runs/background/runner-step-record.ts +76 -0
- package/src/runs/background/runner-step.ts +150 -0
- package/src/runs/background/runner-streaming.ts +220 -0
- package/src/runs/background/runner-workflow.ts +284 -0
- package/src/runs/background/stale-run-reconciler.ts +279 -336
- package/src/runs/background/subagent-runner-launcher.ts +67 -35
- package/src/runs/background/subagent-runner.ts +80 -2048
- package/src/runs/background/top-level-async.ts +9 -7
- package/src/runs/foreground/chain-clarify.ts +499 -1334
- package/src/runs/foreground/clarification-draft.ts +92 -0
- package/src/runs/foreground/clarify-contracts.ts +33 -0
- package/src/runs/foreground/clarify-invocation.ts +192 -65
- package/src/runs/foreground/clarify-text-editor.ts +229 -0
- package/src/runs/foreground/clarify-view.ts +222 -0
- package/src/runs/foreground/execution-input.ts +358 -424
- package/src/runs/foreground/execution-routing.ts +41 -0
- package/src/runs/foreground/execution-timeout.ts +92 -0
- package/src/runs/foreground/foreground-control.ts +333 -812
- package/src/runs/foreground/foreground-memory.ts +132 -0
- package/src/runs/foreground/invocation-context.ts +88 -0
- package/src/runs/foreground/invocation-execution.ts +465 -0
- package/src/runs/foreground/invocation-expansion.ts +58 -0
- package/src/runs/foreground/management-actions.ts +360 -0
- package/src/runs/foreground/nested-control.ts +248 -0
- package/src/runs/foreground/question-continuation.ts +112 -0
- package/src/runs/foreground/question-control.ts +297 -70
- package/src/runs/foreground/resume-target.ts +330 -0
- package/src/runs/foreground/revival-launch-policy.ts +112 -0
- package/src/runs/foreground/run-async-path.ts +307 -223
- package/src/runs/foreground/run-interrupt.ts +202 -0
- package/src/runs/foreground/run-nudge.ts +355 -0
- package/src/runs/foreground/saved-revival.ts +356 -0
- package/src/runs/foreground/subagent-executor.ts +195 -493
- package/src/runs/foreground/subagent-params.ts +314 -242
- package/src/runs/foreground/wait-observation.ts +160 -0
- package/src/runs/foreground/wait-registration.ts +18 -0
- package/src/runs/foreground/wait-run.ts +361 -119
- package/src/runs/shared/acceptance-contract.ts +410 -209
- package/src/runs/shared/acceptance-defaults.ts +1 -0
- package/src/runs/shared/acceptance-evaluation.ts +314 -240
- package/src/runs/shared/acceptance-finalization.ts +447 -305
- package/src/runs/shared/acceptance-outcome.ts +69 -0
- package/src/runs/shared/acceptance-reports.ts +220 -126
- package/src/runs/shared/acceptance-submission.ts +251 -0
- package/src/runs/shared/acceptance-verification.ts +118 -0
- package/src/runs/shared/acceptance.ts +25 -25
- package/src/runs/shared/chain-outputs.ts +190 -98
- package/src/runs/shared/child-attempt-types.ts +118 -0
- package/src/runs/shared/child-attempt.ts +372 -608
- package/src/runs/shared/child-boundaries.ts +217 -0
- package/src/runs/shared/child-context-filter.ts +53 -0
- package/src/runs/shared/child-event-observer.ts +496 -0
- package/src/runs/shared/child-execution-cwd.ts +139 -68
- package/src/runs/shared/child-execution-outcome.ts +103 -0
- package/src/runs/shared/child-invocation.ts +74 -0
- package/src/runs/shared/child-message-validation.ts +233 -0
- package/src/runs/shared/child-native-accounting.ts +154 -0
- package/src/runs/shared/child-native-observation.ts +79 -0
- package/src/runs/shared/child-observation-spool.ts +188 -0
- package/src/runs/shared/child-observations.ts +219 -89
- package/src/runs/shared/child-presence.ts +6 -0
- package/src/runs/shared/child-route-env.ts +142 -0
- package/src/runs/shared/child-stream-observer.ts +302 -0
- package/src/runs/shared/child-stream-projection.ts +160 -0
- package/src/runs/shared/child-structured-tool.ts +38 -0
- package/src/runs/shared/claude-code.ts +201 -228
- package/src/runs/shared/claude-model.ts +67 -0
- package/src/runs/shared/claude-session.ts +124 -0
- package/src/runs/shared/claude-types.ts +31 -0
- package/src/runs/shared/completion-guard.ts +35 -26
- package/src/runs/shared/control-event-policy.ts +15 -0
- package/src/runs/shared/control-notification-owner.ts +20 -0
- package/src/runs/shared/dynamic-fanout.ts +402 -238
- package/src/runs/shared/dynamic-item.ts +114 -0
- package/src/runs/shared/history-index.ts +112 -51
- package/src/runs/shared/legacy-async-recovery.ts +153 -0
- package/src/runs/shared/legacy-output-recovery.ts +214 -0
- package/src/runs/shared/mcp-config-schema.ts +46 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +396 -307
- package/src/runs/shared/model-fallback.ts +302 -180
- package/src/runs/shared/mutating-tool-guard.ts +259 -158
- package/src/runs/shared/native-finalization-types.ts +29 -0
- package/src/runs/shared/native-finalization.ts +233 -109
- package/src/runs/shared/native-usage.ts +379 -96
- package/src/runs/shared/nested-control-records.ts +178 -0
- package/src/runs/shared/nested-events.ts +45 -752
- package/src/runs/shared/nested-path.ts +47 -34
- package/src/runs/shared/nested-projection.ts +230 -0
- package/src/runs/shared/nested-protocol.ts +67 -0
- package/src/runs/shared/nested-registry.ts +273 -0
- package/src/runs/shared/nested-render.ts +198 -100
- package/src/runs/shared/nested-route.ts +292 -0
- package/src/runs/shared/nested-validation.ts +291 -0
- package/src/runs/shared/owned-child-evidence.ts +184 -0
- package/src/runs/shared/owned-run-list.ts +187 -0
- package/src/runs/shared/owned-run-observation.ts +368 -0
- package/src/runs/shared/owned-run-progress.ts +172 -0
- package/src/runs/shared/owned-run-read-state.ts +7 -0
- package/src/runs/shared/owned-run-results.ts +289 -0
- package/src/runs/shared/owned-run-status.ts +239 -0
- package/src/runs/shared/owned-run-view.ts +65 -0
- package/src/runs/shared/owned-workflow-details.ts +24 -0
- package/src/runs/shared/owner-output.ts +73 -0
- package/src/runs/shared/parallel-utils.ts +93 -129
- package/src/runs/shared/parent-receipts.ts +177 -50
- package/src/runs/shared/parent-usage.ts +314 -91
- package/src/runs/shared/pi-args.ts +241 -311
- package/src/runs/shared/pi-launch-input.ts +48 -0
- package/src/runs/shared/pi-session-launch.ts +94 -0
- package/src/runs/shared/pi-spawn.ts +57 -33
- package/src/runs/shared/run-history-queries.ts +189 -38
- package/src/runs/shared/run-history.ts +197 -76
- package/src/runs/shared/run-id-ambiguity.ts +7 -3
- package/src/runs/shared/run-metadata-paths.ts +17 -0
- package/src/runs/shared/run-metadata.ts +240 -0
- package/src/runs/shared/run-persistence.ts +181 -0
- package/src/runs/shared/run-records.ts +19 -607
- package/src/runs/shared/run-restoration.ts +455 -0
- package/src/runs/shared/run-state-owner.ts +31 -0
- package/src/runs/shared/session-cwd-preload.ts +39 -19
- package/src/runs/shared/single-output.ts +358 -205
- package/src/runs/shared/stable-value.ts +15 -0
- package/src/runs/shared/streaming-text.ts +39 -12
- package/src/runs/shared/structured-output.ts +76 -46
- package/src/runs/shared/subagent-control.ts +330 -197
- package/src/runs/shared/subagent-prompt-runtime.ts +92 -174
- package/src/runs/shared/subagent-tool-loop-guard.ts +118 -68
- package/src/runs/shared/supervisor-question-state.ts +221 -0
- package/src/runs/shared/supervisor-questions.ts +396 -401
- package/src/runs/shared/workflow-graph.ts +400 -198
- package/src/runs/shared/workflow-policy.ts +245 -110
- package/src/runs/shared/worktree-contract.ts +29 -0
- package/src/runs/shared/worktree-hook.ts +181 -0
- package/src/runs/shared/worktree-location.ts +61 -0
- package/src/runs/shared/worktree-preservation.ts +10 -0
- package/src/runs/shared/worktree-process.ts +132 -0
- package/src/runs/shared/worktree.ts +459 -587
- package/src/shared/agent-context-policy.ts +176 -145
- package/src/shared/agent-dir.ts +15 -0
- package/src/shared/artifacts.ts +99 -68
- package/src/shared/atomic-json.ts +14 -11
- package/src/shared/chain-files.ts +72 -0
- package/src/shared/concurrency.ts +27 -0
- package/src/shared/content-preview.ts +117 -0
- package/src/shared/cooperative.ts +17 -13
- package/src/shared/execution-cwd.ts +34 -9
- package/src/shared/file-coalescer.ts +24 -22
- package/src/shared/fork-context.ts +52 -48
- package/src/shared/formatters.ts +88 -47
- package/src/shared/journal-frames.ts +197 -0
- package/src/shared/journal-projections.ts +133 -0
- package/src/shared/journal-reader.ts +18 -333
- package/src/shared/json-projection.ts +224 -0
- package/src/shared/lazy-tools.ts +76 -27
- package/src/shared/message-output.ts +146 -0
- package/src/shared/model-info.ts +94 -58
- package/src/shared/native-import.ts +25 -0
- package/src/shared/native-journal.ts +172 -0
- package/src/shared/native-session-loader.ts +13 -0
- package/src/shared/native-session.ts +3 -5
- package/src/shared/native-tui.ts +13 -3
- package/src/shared/native-typebox.ts +18 -3
- package/src/shared/output-page.ts +76 -0
- package/src/shared/post-exit-stdio-guard.ts +232 -172
- package/src/shared/prompt-sections.ts +13 -7
- package/src/shared/session-entries.ts +103 -44
- package/src/shared/session-identity.ts +17 -10
- package/src/shared/settings.ts +182 -367
- package/src/shared/status-format.ts +207 -98
- package/src/shared/stream-json.d.ts +10 -0
- package/src/shared/task-file-policy.ts +35 -0
- package/src/shared/temp-root.ts +122 -71
- package/src/shared/types/acceptance.ts +149 -0
- package/src/shared/types/async.ts +247 -0
- package/src/shared/types/config.ts +139 -0
- package/src/shared/types/details.ts +104 -0
- package/src/shared/types/history.ts +217 -0
- package/src/shared/types/inputs.ts +4 -0
- package/src/shared/types/intercom.ts +57 -0
- package/src/shared/types/launch.ts +39 -0
- package/src/shared/types/messages.ts +104 -0
- package/src/shared/types/nested.ts +153 -0
- package/src/shared/types/owned-runs.ts +148 -0
- package/src/shared/types/progress.ts +106 -0
- package/src/shared/types/questions.ts +103 -0
- package/src/shared/types/results.ts +119 -0
- package/src/shared/types/state.ts +56 -0
- package/src/shared/types/usage.ts +36 -0
- package/src/shared/types/workflow.ts +187 -0
- package/src/shared/types.ts +222 -1161
- package/src/shared/ui-mode.ts +1 -1
- package/src/shared/unknown.ts +59 -0
- package/src/shared/utils.ts +111 -305
- package/src/slash/command-discovery.ts +109 -0
- package/src/slash/command-input.ts +232 -0
- package/src/slash/prompt-template-bridge.ts +153 -400
- package/src/slash/prompt-template-contract.ts +152 -0
- package/src/slash/prompt-template-results.ts +189 -0
- package/src/slash/request-protocol.ts +87 -0
- package/src/slash/result-delivery.ts +312 -0
- package/src/slash/saved-chain.ts +115 -0
- package/src/slash/slash-bridge.ts +202 -156
- package/src/slash/slash-commands.ts +176 -598
- package/src/slash/slash-live-state.ts +287 -229
- package/src/tui/action-hints.ts +147 -61
- package/src/tui/activity-lines.ts +40 -0
- package/src/tui/agent-browser.ts +354 -0
- package/src/tui/agent-controls.ts +355 -0
- package/src/tui/agent-conversation.ts +514 -0
- package/src/tui/agent-dock.ts +250 -0
- package/src/tui/agent-history.ts +78 -203
- package/src/tui/agent-model.ts +78 -0
- package/src/tui/agent-picker.ts +325 -0
- package/src/tui/agent-view.ts +391 -1333
- package/src/tui/compact-results.ts +342 -0
- package/src/tui/conversation-details.ts +206 -0
- package/src/tui/conversation-frame.ts +184 -0
- package/src/tui/conversation-history.ts +246 -0
- package/src/tui/conversation-menu.ts +119 -0
- package/src/tui/conversation-notices.ts +116 -0
- package/src/tui/conversation-selection.ts +66 -0
- package/src/tui/conversation-viewport.ts +169 -0
- package/src/tui/direction-message.ts +35 -0
- package/src/tui/display.ts +409 -0
- package/src/tui/editor-viewport.ts +19 -0
- package/src/tui/expanded-content.ts +134 -0
- package/src/tui/expanded-multi.ts +264 -0
- package/src/tui/expanded-single.ts +92 -0
- package/src/tui/history-cards.ts +316 -0
- package/src/tui/history-display.ts +334 -0
- package/src/tui/history-json.ts +17 -0
- package/src/tui/history-metadata.ts +177 -0
- package/src/tui/history-record.ts +321 -0
- package/src/tui/history-text.ts +34 -0
- package/src/tui/indexed-history.ts +149 -0
- package/src/tui/nested-widget.ts +192 -0
- package/src/tui/picker-page.ts +80 -0
- package/src/tui/render.ts +100 -1546
- package/src/tui/result-facts.ts +99 -0
- package/src/tui/task-observation.ts +96 -0
- package/src/tui/task-store.ts +432 -0
- package/src/tui/text-values.ts +20 -0
- package/src/tui/view-connections.ts +64 -0
- package/src/tui/view-model.ts +181 -0
- package/src/tui/view-persistence.ts +57 -0
- package/src/tui/view-ports.ts +55 -0
- package/src/tui/view-session.ts +42 -0
- package/src/tui/widget-details.ts +220 -0
- package/src/tui/widget-status.ts +331 -0
- package/src/tui/widget.ts +208 -0
- package/src/tui/workflow-labels.ts +340 -0
- package/src/tui/workflow-outcomes.ts +150 -0
- package/tsconfig.build.json +11 -10
- package/tsconfig.json +30 -17
|
@@ -1,424 +1,64 @@
|
|
|
1
|
-
import { constants, closeSync, fstatSync, openSync, readFileSync, statSync } from "node:fs";
|
|
2
1
|
import { createServer, type Server } from "node:https";
|
|
3
2
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
4
3
|
import type { Socket } from "node:net";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { TLSSocket } from "node:tls";
|
|
7
6
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { IntercomClient } from "./broker/client.ts";
|
|
9
7
|
import { getBrokerSocketPath } from "./broker/paths.ts";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
8
|
+
import { loadConfig, sameListener, type Config, type Identity } from "./bridge-config.ts";
|
|
9
|
+
import { ASK_TIMEOUT, Fault, fingerprint, log } from "./bridge-protocol.ts";
|
|
10
|
+
import { Session } from "./bridge-session.ts";
|
|
11
|
+
import { operationFor, routeFor, type Operation } from "./bridge-routes.ts";
|
|
12
|
+
import { reply } from "./bridge-http.ts";
|
|
13
|
+
import { Launcher, sameIdentity } from "./bridge-launch.ts";
|
|
12
14
|
|
|
13
|
-
const BODY_LIMIT = 64 * 1024;
|
|
14
|
-
const SEND_TIMEOUT = 8000;
|
|
15
|
-
const LIST_TIMEOUT = 5000;
|
|
16
|
-
const ASK_TIMEOUT = 120000;
|
|
17
|
-
const IDLE_TIMEOUT = 5 * 60 * 1000;
|
|
18
|
-
const MAX_CLIENTS = 64;
|
|
19
15
|
const MAX_CONNECTIONS = 128;
|
|
20
|
-
const MAX_REQUESTS = 8;
|
|
21
|
-
const INBOX_COUNT = 256;
|
|
22
|
-
const INBOX_BYTES = 4 * 1024 * 1024;
|
|
23
|
-
const LABEL_CONTROLS = /[\u0000-\u001f\u007f-\u009f]/;
|
|
24
|
-
const TEXT_CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/;
|
|
25
16
|
|
|
26
|
-
|
|
27
|
-
readonly
|
|
28
|
-
readonly code: string;
|
|
29
|
-
readonly details: Record<string, unknown>;
|
|
30
|
-
constructor(status: number, code: string, message: string, details: Record<string, unknown> = {}) {
|
|
31
|
-
super(message);
|
|
32
|
-
this.status = status;
|
|
33
|
-
this.code = code;
|
|
34
|
-
this.details = details;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface Identity { fingerprint256: string; name: string; cwd: string }
|
|
39
|
-
interface Config {
|
|
40
|
-
port: number;
|
|
41
|
-
ca: string;
|
|
42
|
-
cert: string;
|
|
43
|
-
key: string;
|
|
44
|
-
clients: Map<string, Identity>;
|
|
45
|
-
tls: { ca: Buffer; cert: Buffer; key: Buffer };
|
|
46
|
-
}
|
|
47
|
-
interface Envelope { from: SessionInfo; message: Message }
|
|
48
|
-
interface InboxEntry extends Envelope { bytes: number; replying: boolean }
|
|
49
|
-
interface Connection { fingerprint: string; expires: number; timer: NodeJS.Timeout }
|
|
50
|
-
interface WaitingAsk { id: string; peer?: string; settled: boolean; resolve: (reply: Envelope) => void; reject: (error: Fault) => void }
|
|
51
|
-
|
|
52
|
-
function object(value: unknown, fields: string[]): Record<string, unknown> {
|
|
53
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Fault(400, "invalid_input", "Expected a JSON object.");
|
|
54
|
-
const result = value as Record<string, unknown>;
|
|
55
|
-
if (Object.keys(result).some((key) => !fields.includes(key))) throw new Fault(400, "invalid_input", "Unknown field.");
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function label(value: unknown, field: string, max = 512): string {
|
|
60
|
-
if (typeof value !== "string" || !value.trim() || value.length > max || LABEL_CONTROLS.test(value)) {
|
|
61
|
-
throw new Fault(400, "invalid_input", `Invalid ${field}.`);
|
|
62
|
-
}
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function text(value: unknown): string {
|
|
67
|
-
if (typeof value !== "string" || !value.trim() || TEXT_CONTROLS.test(value)) throw new Fault(400, "invalid_input", "Invalid message.");
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function fingerprint(value: unknown): string {
|
|
72
|
-
if (typeof value !== "string" || !/^(?:[a-fA-F0-9]{64}|(?:[a-fA-F0-9]{2}:){31}[a-fA-F0-9]{2})$/.test(value)) {
|
|
73
|
-
throw new Fault(400, "invalid_input", "Invalid SHA256 certificate fingerprint.");
|
|
74
|
-
}
|
|
75
|
-
return value.replaceAll(":", "").toUpperCase();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function readFile(path: string, privateFile: boolean, limit: number): Buffer {
|
|
79
|
-
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
80
|
-
try {
|
|
81
|
-
const stat = fstatSync(fd);
|
|
82
|
-
if (!stat.isFile() || stat.size > limit) throw new Error("Configuration/TLS file must be a bounded regular file.");
|
|
83
|
-
if (privateFile && (stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o600)) {
|
|
84
|
-
throw new Error("Config and TLS key must be owned by the current user with mode 0600.");
|
|
85
|
-
}
|
|
86
|
-
const data = readFileSync(fd);
|
|
87
|
-
if (data.length > limit) throw new Error("Configuration/TLS file exceeds its size limit.");
|
|
88
|
-
return data;
|
|
89
|
-
} finally {
|
|
90
|
-
closeSync(fd);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function loadConfig(path: string): Config {
|
|
95
|
-
const input = object(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(readFile(path, true, BODY_LIMIT))), ["port", "ca", "cert", "key", "clients"]);
|
|
96
|
-
const port = input.port === undefined ? 9443 : input.port;
|
|
97
|
-
if (!Number.isInteger(port) || typeof port !== "number" || port < 0 || port > 65535) throw new Error("Invalid HTTPS port.");
|
|
98
|
-
const ca = label(input.ca, "ca path", 4096);
|
|
99
|
-
const cert = label(input.cert, "cert path", 4096);
|
|
100
|
-
const key = label(input.key, "key path", 4096);
|
|
101
|
-
if (![ca, cert, key].every(isAbsolute)) throw new Error("TLS paths must be absolute.");
|
|
102
|
-
if (!Array.isArray(input.clients) || input.clients.length > MAX_CLIENTS) throw new Error(`clients must be an array of at most ${MAX_CLIENTS} identities.`);
|
|
103
|
-
const clients = new Map<string, Identity>();
|
|
104
|
-
const names = new Set<string>();
|
|
105
|
-
for (const raw of input.clients) {
|
|
106
|
-
const entry = object(raw, ["fingerprint256", "name", "cwd"]);
|
|
107
|
-
const fingerprint256 = fingerprint(entry.fingerprint256);
|
|
108
|
-
const name = label(entry.name, "name", 128).trim();
|
|
109
|
-
const cwd = label(entry.cwd, "cwd", 4096);
|
|
110
|
-
if (!isAbsolute(cwd) || !statSync(cwd).isDirectory()) throw new Error("Client cwd must be an existing absolute directory.");
|
|
111
|
-
if (clients.has(fingerprint256) || names.has(name.toLowerCase())) throw new Error("Client fingerprints and names must be unique.");
|
|
112
|
-
clients.set(fingerprint256, { fingerprint256, name, cwd });
|
|
113
|
-
names.add(name.toLowerCase());
|
|
114
|
-
}
|
|
115
|
-
const tls = { ca: readFile(ca, false, 1024 * 1024), cert: readFile(cert, false, 1024 * 1024), key: readFile(key, true, 1024 * 1024) };
|
|
116
|
-
createSecureContext({ ...tls, minVersion: "TLSv1.2" });
|
|
117
|
-
return { port, ca, cert, key, clients, tls };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
let logBackpressure = false;
|
|
121
|
-
let lostLogEvents = 0;
|
|
122
|
-
function log(event: string, fields: Record<string, unknown> = {}): void {
|
|
123
|
-
if (logBackpressure) { lostLogEvents = Math.min(Number.MAX_SAFE_INTEGER, lostLogEvents + 1); return; }
|
|
124
|
-
logBackpressure = !process.stdout.write(`${JSON.stringify({ timestamp: new Date().toISOString(), event, ...fields })}\n`);
|
|
125
|
-
if (logBackpressure) process.stdout.once("drain", () => {
|
|
126
|
-
logBackpressure = false;
|
|
127
|
-
if (lostLogEvents) {
|
|
128
|
-
const lostEvents = lostLogEvents;
|
|
129
|
-
lostLogEvents = 0;
|
|
130
|
-
log("log_overflow", { result: "metadata_lost", lostEvents });
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function cancelled(signal: AbortSignal): void {
|
|
136
|
-
if (signal.aborted) throw signal.reason;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function wait<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
140
|
-
if (signal.aborted) {
|
|
141
|
-
void promise.catch(() => {});
|
|
142
|
-
throw signal.reason;
|
|
143
|
-
}
|
|
144
|
-
return new Promise((resolve, reject) => {
|
|
145
|
-
const abort = () => { cleanup(); reject(signal.reason); };
|
|
146
|
-
const cleanup = () => signal.removeEventListener("abort", abort);
|
|
147
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
148
|
-
promise.then((value) => { cleanup(); resolve(value); }, (error: unknown) => { cleanup(); reject(error); });
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function body(req: IncomingMessage, signal: AbortSignal): Promise<unknown> {
|
|
153
|
-
const type = req.headers["content-type"];
|
|
154
|
-
if (typeof type !== "string" || !/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(type)) throw new Fault(415, "content_type", "Use application/json.");
|
|
155
|
-
const length = req.headers["content-length"];
|
|
156
|
-
if (length !== undefined && (!/^\d+$/.test(length) || Number(length) > BODY_LIMIT)) throw new Fault(413, "body_too_large", "JSON body exceeds 64 KiB.");
|
|
157
|
-
const data = await new Promise<Buffer>((resolve, reject) => {
|
|
158
|
-
const chunks: Buffer[] = [];
|
|
159
|
-
let size = 0;
|
|
160
|
-
const cleanup = () => {
|
|
161
|
-
clearTimeout(timer);
|
|
162
|
-
req.off("data", onData);
|
|
163
|
-
req.off("end", onEnd);
|
|
164
|
-
req.off("error", onError);
|
|
165
|
-
signal.removeEventListener("abort", onAbort);
|
|
166
|
-
};
|
|
167
|
-
const fail = (error: unknown) => { cleanup(); req.pause(); reject(error); };
|
|
168
|
-
const onAbort = () => fail(signal.reason);
|
|
169
|
-
const onError = () => fail(new Fault(400, "invalid_body", "Could not read JSON body."));
|
|
170
|
-
const onData = (chunk: Buffer) => {
|
|
171
|
-
size += chunk.length;
|
|
172
|
-
if (size > BODY_LIMIT) { fail(new Fault(413, "body_too_large", "JSON body exceeds 64 KiB.")); return; }
|
|
173
|
-
chunks.push(chunk);
|
|
174
|
-
};
|
|
175
|
-
const onEnd = () => { cleanup(); resolve(Buffer.concat(chunks)); };
|
|
176
|
-
const timer = setTimeout(() => fail(new Fault(408, "body_timeout", "JSON body deadline exceeded.")), 10000);
|
|
177
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
178
|
-
req.on("data", onData);
|
|
179
|
-
req.once("end", onEnd);
|
|
180
|
-
req.once("error", onError);
|
|
181
|
-
if (signal.aborted) onAbort();
|
|
182
|
-
});
|
|
183
|
-
try { return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(data)) as unknown; }
|
|
184
|
-
catch { throw new Fault(400, "invalid_json", "Invalid UTF-8 JSON body."); }
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
class Session {
|
|
188
|
-
readonly bridge: Bridge;
|
|
189
|
-
readonly identity: Identity;
|
|
17
|
+
interface Connection {
|
|
18
|
+
readonly fingerprint: string;
|
|
190
19
|
readonly expires: number;
|
|
191
|
-
|
|
192
|
-
readonly requests = new Set<AbortController>();
|
|
193
|
-
readonly inbox = new Map<string, InboxEntry>();
|
|
194
|
-
inboxBytes = 0;
|
|
195
|
-
pending?: WaitingAsk;
|
|
196
|
-
closed = false;
|
|
197
|
-
private connecting?: Promise<void>;
|
|
198
|
-
private disconnecting?: Promise<void>;
|
|
199
|
-
private lease?: NodeJS.Timeout;
|
|
200
|
-
private closeReason?: Fault;
|
|
201
|
-
|
|
202
|
-
constructor(bridge: Bridge, identity: Identity, expires: number) {
|
|
203
|
-
this.bridge = bridge;
|
|
204
|
-
this.identity = identity;
|
|
205
|
-
this.expires = expires;
|
|
206
|
-
this.client.on("error", () => this.close(new Fault(503, "broker_unavailable", "Broker connection failed.")));
|
|
207
|
-
this.client.on("disconnected", () => this.close(new Fault(503, "broker_disconnected", "Broker disconnected.")));
|
|
208
|
-
this.client.on("_registered", () => {
|
|
209
|
-
// IntercomClient cannot abort socket acquisition; retire a cancelled generation before any application write.
|
|
210
|
-
if (this.closed) void this.disconnect();
|
|
211
|
-
});
|
|
212
|
-
this.client.on("message", (from: SessionInfo, message: Message) => this.receive(from, message));
|
|
213
|
-
this.client.on("session_left", (id: string) => {
|
|
214
|
-
if (this.pending?.peer === id && !this.pending.settled) this.pending.reject(new Fault(409, "peer_offline", "Ask recipient disconnected.", { id: this.pending.id }));
|
|
215
|
-
for (const [messageId, entry] of this.inbox) {
|
|
216
|
-
if (entry.from.id === id && entry.message.expectsReply) { this.remove(messageId); this.bridge.lost(this.identity.fingerprint256); }
|
|
217
|
-
}
|
|
218
|
-
this.presence();
|
|
219
|
-
});
|
|
220
|
-
this.touch();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
guard(signal?: AbortSignal): void {
|
|
224
|
-
if (signal) cancelled(signal);
|
|
225
|
-
if (this.closed) throw this.closeReason;
|
|
226
|
-
if (!this.bridge.allowed(this.identity)) throw new Fault(403, "revoked", "Identity is no longer authorized.");
|
|
227
|
-
if (this.expires <= Date.now()) {
|
|
228
|
-
const error = new Fault(403, "certificate_expired", "Client certificate expired.");
|
|
229
|
-
this.close(error);
|
|
230
|
-
throw error;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
touch(): void {
|
|
235
|
-
clearTimeout(this.lease);
|
|
236
|
-
this.lease = setTimeout(() => this.close(this.expires <= Date.now()
|
|
237
|
-
? new Fault(403, "certificate_expired", "Client certificate expired.")
|
|
238
|
-
: new Fault(410, "lease_expired", "Bridge session idle lease expired.")), Math.max(1, Math.min(IDLE_TIMEOUT, this.expires - Date.now())));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async connect(signal: AbortSignal): Promise<void> {
|
|
242
|
-
this.guard(signal);
|
|
243
|
-
if (!this.connecting) {
|
|
244
|
-
this.connecting = (async () => {
|
|
245
|
-
const projectId = await resolveSessionProjectId(this.identity.cwd);
|
|
246
|
-
this.guard();
|
|
247
|
-
await this.client.connect({ name: this.identity.name, cwd: this.identity.cwd, model: "remote-bridge", projectId, status: "idle", acceptsAsks: true, pendingAsks: 0 }, `remote-${this.identity.fingerprint256.toLowerCase()}`);
|
|
248
|
-
if (this.closed) { await this.disconnect(); return; }
|
|
249
|
-
if (this.client.sessionId !== `remote-${this.identity.fingerprint256.toLowerCase()}`) {
|
|
250
|
-
this.close(new Fault(409, "identity_in_use", "This remote identity is already registered by another connection."));
|
|
251
|
-
throw new Fault(409, "identity_in_use", "This remote identity is already registered by another connection.");
|
|
252
|
-
}
|
|
253
|
-
log("registered", this.fields());
|
|
254
|
-
})().catch((error: unknown) => {
|
|
255
|
-
if (!this.closed) this.close(error instanceof Fault ? error : new Fault(503, "broker_unavailable", "Local broker is unavailable; bridge does not start it."));
|
|
256
|
-
throw error;
|
|
257
|
-
}).finally(() => { if (this.closed) void this.disconnect(); });
|
|
258
|
-
}
|
|
259
|
-
await wait(this.connecting, signal);
|
|
260
|
-
this.guard(signal);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
fields(): Record<string, unknown> { return { name: this.identity.name, fingerprint: this.identity.fingerprint256 }; }
|
|
264
|
-
|
|
265
|
-
private disconnect(): Promise<void> {
|
|
266
|
-
if (!this.disconnecting) {
|
|
267
|
-
this.disconnecting = this.client.disconnect().finally(() => { this.disconnecting = undefined; });
|
|
268
|
-
}
|
|
269
|
-
return this.disconnecting;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
close(error: Fault): void {
|
|
273
|
-
if (this.closed) return;
|
|
274
|
-
this.closed = true;
|
|
275
|
-
this.closeReason = error;
|
|
276
|
-
clearTimeout(this.lease);
|
|
277
|
-
this.pending?.reject(error);
|
|
278
|
-
for (const controller of this.requests) controller.abort(error);
|
|
279
|
-
this.bridge.lost(this.identity.fingerprint256, this.inbox.size);
|
|
280
|
-
this.inbox.clear();
|
|
281
|
-
this.inboxBytes = 0;
|
|
282
|
-
// Keep the retiring generation in the map until socket acquisition/registration has settled.
|
|
283
|
-
// Otherwise rapid aborts could create an unbounded number of unfinished broker connects.
|
|
284
|
-
const disconnect = this.disconnect();
|
|
285
|
-
void (async () => {
|
|
286
|
-
await disconnect;
|
|
287
|
-
try { await this.connecting; } catch { /* The original request receives the connect failure. */ }
|
|
288
|
-
await this.disconnect();
|
|
289
|
-
if (this.bridge.sessions.get(this.identity.fingerprint256) === this) this.bridge.sessions.delete(this.identity.fingerprint256);
|
|
290
|
-
log("session_closed", { ...this.fields(), result: error.code });
|
|
291
|
-
})();
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
private receive(from: SessionInfo, message: Message): void {
|
|
295
|
-
if (this.closed) return;
|
|
296
|
-
try { this.guard(); } catch (error) {
|
|
297
|
-
this.close(error instanceof Fault ? error : new Fault(403, "revoked", "Identity is no longer authorized."));
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
log("received", { ...this.fields(), peer: from.id, messageId: message.id, result: "received" });
|
|
301
|
-
const pending = this.pending;
|
|
302
|
-
if (pending && !pending.settled && pending.id === message.replyTo && pending.peer === from.id) {
|
|
303
|
-
pending.resolve({ from, message });
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (this.inbox.has(message.id)) { this.bridge.lost(this.identity.fingerprint256); return; }
|
|
307
|
-
const bytes = Buffer.byteLength(JSON.stringify({ from, message }));
|
|
308
|
-
if (this.inbox.size >= INBOX_COUNT || this.inboxBytes + bytes > INBOX_BYTES) {
|
|
309
|
-
this.bridge.lost(this.identity.fingerprint256);
|
|
310
|
-
log("inbox_overflow", { ...this.fields(), peer: from.id, messageId: message.id, result: "lost" });
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
// ponytail: inbox is bounded and volatile; use a private durable journal if restart/lease-loss recovery becomes required.
|
|
314
|
-
this.inbox.set(message.id, { from, message, bytes, replying: false });
|
|
315
|
-
this.inboxBytes += bytes;
|
|
316
|
-
this.presence();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
remove(id: string): void {
|
|
320
|
-
const entry = this.inbox.get(id);
|
|
321
|
-
if (entry) { this.inboxBytes -= entry.bytes; this.inbox.delete(id); }
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
presence(): void {
|
|
325
|
-
if (this.closed) return;
|
|
326
|
-
const pendingAsks = [...this.inbox.values()].filter((entry) => entry.message.expectsReply).length;
|
|
327
|
-
this.client.updatePresence({ pendingAsks, acceptsAsks: this.inbox.size < INBOX_COUNT && this.inboxBytes < INBOX_BYTES });
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async peers(signal: AbortSignal): Promise<SessionInfo[]> {
|
|
331
|
-
this.guard(signal);
|
|
332
|
-
let peers: SessionInfo[];
|
|
333
|
-
try { peers = await wait(this.client.listSessions(), signal); }
|
|
334
|
-
catch (error) {
|
|
335
|
-
if (signal.aborted) throw signal.reason;
|
|
336
|
-
if (error instanceof Error && error.message === "List sessions timeout") throw new Fault(504, "list_timeout", "Broker session list deadline exceeded.");
|
|
337
|
-
throw new Fault(503, "broker_unavailable", "Broker session list failed.");
|
|
338
|
-
}
|
|
339
|
-
this.guard(signal);
|
|
340
|
-
return peers;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
async target(to: string, signal: AbortSignal): Promise<SessionInfo> {
|
|
344
|
-
const resolution = resolveSessionTarget(await this.peers(signal), to);
|
|
345
|
-
if (resolution.status === "none") throw new Fault(404, "peer_offline", "Peer is not connected.");
|
|
346
|
-
if (resolution.status !== "found" || !resolution.target) throw new Fault(409, "ambiguous_target", "Use an unambiguous peer name or session ID (prefixes need at least 8 characters).");
|
|
347
|
-
if (resolution.target.id === this.client.sessionId) throw new Fault(400, "self_target", "Choose another peer.");
|
|
348
|
-
return resolution.target;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async send(peer: string, message: string, signal: AbortSignal, requestId: string, options: { messageId?: string; replyTo?: string; expectsReply?: boolean } = {}): Promise<SendResult> {
|
|
352
|
-
this.guard(signal);
|
|
353
|
-
const id = options.messageId ?? randomUUID();
|
|
354
|
-
log("send", { ...this.fields(), requestId, peer, messageId: id, result: "attempt" });
|
|
355
|
-
let result: SendResult;
|
|
356
|
-
try { result = await wait(this.client.send(peer, { text: message, delivery: "steer", ...options, messageId: id }), signal); }
|
|
357
|
-
catch (error) {
|
|
358
|
-
if (signal.aborted) throw signal.reason;
|
|
359
|
-
const fault = new Fault(504, "send_unconfirmed", "Broker acknowledgement was not received; delivery is unknown. Do not automatically retry.", { id, deliveryUnknown: true });
|
|
360
|
-
this.close(fault);
|
|
361
|
-
throw fault;
|
|
362
|
-
}
|
|
363
|
-
this.guard(signal);
|
|
364
|
-
log("send_result", { ...this.fields(), requestId, peer, messageId: id, result: result.accepted ? "accepted" : "rejected" });
|
|
365
|
-
if (!result.accepted) throw new Fault(409, "delivery_failed", result.reason ?? "Broker rejected delivery.", { id, accepted: false, delivered: false });
|
|
366
|
-
return result;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async ask(to: string, message: string, timeoutMs: number, signal: AbortSignal, requestId: string): Promise<Record<string, unknown>> {
|
|
370
|
-
if (this.pending) throw new Fault(409, "ask_in_progress", "Only one concurrent ask is allowed per identity.");
|
|
371
|
-
const id = randomUUID();
|
|
372
|
-
let waiting!: WaitingAsk;
|
|
373
|
-
let timer: NodeJS.Timeout | undefined;
|
|
374
|
-
let receipt: SendResult | undefined;
|
|
375
|
-
const reply = new Promise<Envelope>((resolve, reject) => {
|
|
376
|
-
waiting = { id, settled: false,
|
|
377
|
-
resolve: (answer) => { waiting.settled = true; clearTimeout(timer); resolve(answer); },
|
|
378
|
-
reject: (error) => { waiting.settled = true; clearTimeout(timer); reject(error); },
|
|
379
|
-
};
|
|
380
|
-
});
|
|
381
|
-
// A disconnect may reject the reply while target lookup or send acknowledgement is still pending.
|
|
382
|
-
void reply.catch(() => {});
|
|
383
|
-
this.pending = waiting;
|
|
384
|
-
const abort = () => waiting.reject(signal.reason as Fault);
|
|
385
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
386
|
-
try {
|
|
387
|
-
await this.connect(signal);
|
|
388
|
-
const peer = await this.target(to, signal);
|
|
389
|
-
this.guard(signal);
|
|
390
|
-
waiting.peer = peer.id;
|
|
391
|
-
timer = setTimeout(() => {
|
|
392
|
-
const fault = new Fault(504, "ask_timeout", "Peer did not reply before the ask deadline.", { id, ...(receipt ? { accepted: receipt.accepted, delivered: receipt.delivered } : { deliveryUnknown: true }) });
|
|
393
|
-
if (receipt) waiting.reject(fault);
|
|
394
|
-
else this.close(fault);
|
|
395
|
-
}, timeoutMs);
|
|
396
|
-
receipt = await this.send(peer.id, message, signal, requestId, { messageId: id, expectsReply: true });
|
|
397
|
-
const answer = await wait(reply, signal);
|
|
398
|
-
this.guard(signal);
|
|
399
|
-
return { ...receipt, replied: true, reply: answer };
|
|
400
|
-
} finally {
|
|
401
|
-
clearTimeout(timer);
|
|
402
|
-
signal.removeEventListener("abort", abort);
|
|
403
|
-
if (this.pending === waiting) this.pending = undefined;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
20
|
+
timer: NodeJS.Timeout;
|
|
406
21
|
}
|
|
407
22
|
|
|
408
23
|
class Bridge {
|
|
409
24
|
private config: Config;
|
|
410
25
|
private readonly configPath: string;
|
|
411
|
-
readonly sessions = new Map<string, Session>();
|
|
26
|
+
private readonly sessions = new Map<string, Session>();
|
|
412
27
|
private readonly losses = new Map<string, number>();
|
|
28
|
+
private readonly launcher = new Launcher();
|
|
413
29
|
private readonly sockets = new Set<Socket>();
|
|
414
30
|
private readonly connections = new Map<TLSSocket, Connection>();
|
|
415
31
|
private readonly server: Server;
|
|
416
32
|
private stopping = false;
|
|
33
|
+
private readonly handlers = new Set<Promise<void>>();
|
|
417
34
|
|
|
418
35
|
constructor(config: Config, configPath: string) {
|
|
419
36
|
this.config = config;
|
|
420
37
|
this.configPath = configPath;
|
|
421
|
-
this.server = createServer(
|
|
38
|
+
this.server = createServer(
|
|
39
|
+
{
|
|
40
|
+
...config.tls,
|
|
41
|
+
minVersion: "TLSv1.2",
|
|
42
|
+
requestCert: true,
|
|
43
|
+
rejectUnauthorized: true,
|
|
44
|
+
handshakeTimeout: 10000,
|
|
45
|
+
},
|
|
46
|
+
(req, res) => {
|
|
47
|
+
const handler = this.handle(req, res).catch(() => {
|
|
48
|
+
// A response write can fail after route handling; close transport and retain metadata only.
|
|
49
|
+
res.destroy();
|
|
50
|
+
log("request", { result: "transport_failed" });
|
|
51
|
+
});
|
|
52
|
+
this.handlers.add(handler);
|
|
53
|
+
handler
|
|
54
|
+
.finally(() => {
|
|
55
|
+
this.handlers.delete(handler);
|
|
56
|
+
})
|
|
57
|
+
.catch(() => {
|
|
58
|
+
res.destroy();
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
);
|
|
422
62
|
this.server.maxConnections = MAX_CONNECTIONS;
|
|
423
63
|
this.server.requestTimeout = 10000;
|
|
424
64
|
this.server.headersTimeout = 10000;
|
|
@@ -426,94 +66,230 @@ class Bridge {
|
|
|
426
66
|
this.server.maxRequestsPerSocket = 100;
|
|
427
67
|
this.server.maxHeadersCount = 32;
|
|
428
68
|
this.server.on("connection", (socket: Socket) => {
|
|
429
|
-
if (this.stopping || this.sockets.size >= MAX_CONNECTIONS) {
|
|
69
|
+
if (this.stopping || this.sockets.size >= MAX_CONNECTIONS) {
|
|
70
|
+
socket.destroy();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
430
73
|
this.sockets.add(socket);
|
|
431
|
-
socket.once("close", () =>
|
|
74
|
+
socket.once("close", () => {
|
|
75
|
+
this.sockets.delete(socket);
|
|
76
|
+
});
|
|
432
77
|
});
|
|
433
78
|
this.server.on("secureConnection", (socket: TLSSocket) => this.authenticate(socket));
|
|
434
|
-
this.server.on("tlsClientError", () =>
|
|
435
|
-
|
|
436
|
-
|
|
79
|
+
this.server.on("tlsClientError", () =>
|
|
80
|
+
log("connection_rejected", { result: "tls_authentication_failed" }),
|
|
81
|
+
);
|
|
82
|
+
this.server.on("clientError", (_error, socket) => {
|
|
83
|
+
socket.destroy();
|
|
84
|
+
});
|
|
85
|
+
this.server.on("error", () => {
|
|
86
|
+
log("server_error", { result: "listener_failed" });
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
this.stop();
|
|
89
|
+
});
|
|
437
90
|
}
|
|
438
|
-
|
|
439
|
-
allowed(identity: Identity): boolean {
|
|
91
|
+
private allowed(identity: Identity): boolean {
|
|
440
92
|
const current = this.config.clients.get(identity.fingerprint256);
|
|
441
|
-
return !this.stopping &&
|
|
93
|
+
return !this.stopping && sameIdentity(identity, current);
|
|
442
94
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
95
|
+
private lost(fp: string, count = 1): void {
|
|
96
|
+
if (count > 0 && this.config.clients.has(fp)) {
|
|
97
|
+
this.losses.set(fp, Math.min(Number.MAX_SAFE_INTEGER, (this.losses.get(fp) ?? 0) + count));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
private session(identity: Identity, expires: number): Session {
|
|
101
|
+
let session = this.sessions.get(identity.fingerprint256);
|
|
102
|
+
if (!session) {
|
|
103
|
+
session = new Session(
|
|
104
|
+
{
|
|
105
|
+
allowed: (candidate) => this.allowed(candidate),
|
|
106
|
+
lost: (fp, count) => this.lost(fp, count),
|
|
107
|
+
losses: (fp) => this.losses.get(fp) ?? 0,
|
|
108
|
+
retired: (fp, generation) => {
|
|
109
|
+
if (this.sessions.get(fp)?.generation === generation) {
|
|
110
|
+
this.sessions.delete(fp);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
identity,
|
|
115
|
+
expires,
|
|
116
|
+
);
|
|
117
|
+
this.sessions.set(identity.fingerprint256, session);
|
|
118
|
+
}
|
|
119
|
+
return session;
|
|
446
120
|
}
|
|
447
|
-
|
|
448
121
|
private authenticate(socket: TLSSocket): void {
|
|
449
122
|
const cert = socket.getPeerCertificate();
|
|
450
123
|
let fp: string;
|
|
451
|
-
try {
|
|
124
|
+
try {
|
|
125
|
+
fp = fingerprint(cert.fingerprint256);
|
|
126
|
+
} catch {
|
|
127
|
+
socket.destroy();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
452
130
|
const identity = this.config.clients.get(fp);
|
|
453
131
|
const expires = Date.parse(cert.valid_to);
|
|
454
132
|
const starts = Date.parse(cert.valid_from);
|
|
455
|
-
if (
|
|
133
|
+
if (
|
|
134
|
+
!socket.authorized ||
|
|
135
|
+
!identity ||
|
|
136
|
+
this.stopping ||
|
|
137
|
+
!Number.isFinite(expires) ||
|
|
138
|
+
!Number.isFinite(starts) ||
|
|
139
|
+
starts > Date.now() ||
|
|
140
|
+
expires <= Date.now()
|
|
141
|
+
) {
|
|
456
142
|
log("connection_rejected", { fingerprint: fp, result: "unauthorized" });
|
|
457
143
|
socket.destroy();
|
|
458
144
|
return;
|
|
459
145
|
}
|
|
460
|
-
// Timers longer than INT32_MAX fire immediately in Node; recheck long-lived certificates in bounded slices.
|
|
461
146
|
const expiry = () => {
|
|
462
|
-
if (expires <= Date.now()) {
|
|
147
|
+
if (expires <= Date.now()) {
|
|
148
|
+
this.drop(fp, new Fault(403, "certificate_expired", "Client certificate expired."));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
463
151
|
connection.timer = setTimeout(expiry, Math.min(expires - Date.now(), 0x7fffffff));
|
|
464
152
|
};
|
|
465
|
-
|
|
153
|
+
// Node timers must recheck long-lived certificates in INT32_MAX-bounded slices.
|
|
154
|
+
const connection: Connection = {
|
|
155
|
+
fingerprint: fp,
|
|
156
|
+
expires,
|
|
157
|
+
timer: setTimeout(expiry, Math.min(expires - Date.now(), 0x7fffffff)),
|
|
158
|
+
};
|
|
466
159
|
this.connections.set(socket, connection);
|
|
467
|
-
socket.once("close", () => {
|
|
160
|
+
socket.once("close", () => {
|
|
161
|
+
clearTimeout(connection.timer);
|
|
162
|
+
this.connections.delete(socket);
|
|
163
|
+
});
|
|
468
164
|
log("connected", { name: identity.name, fingerprint: fp, result: "authenticated" });
|
|
469
165
|
}
|
|
470
|
-
|
|
471
|
-
private drop(fp: string, error: Fault): void {
|
|
166
|
+
private drop(fp: string, error: Readonly<Fault>): void {
|
|
472
167
|
this.sessions.get(fp)?.close(error);
|
|
473
|
-
for (const [socket, connection] of this.connections)
|
|
168
|
+
for (const [socket, connection] of this.connections) {
|
|
169
|
+
if (connection.fingerprint === fp) {
|
|
170
|
+
socket.destroy();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
474
173
|
}
|
|
475
|
-
|
|
476
174
|
start(): void {
|
|
477
175
|
this.server.listen(this.config.port, "127.0.0.1", () => {
|
|
478
176
|
const address = this.server.address();
|
|
479
|
-
log("listening", {
|
|
177
|
+
log("listening", {
|
|
178
|
+
result: "ready",
|
|
179
|
+
host: "127.0.0.1",
|
|
180
|
+
pid: process.pid,
|
|
181
|
+
port: typeof address === "object" && address ? address.port : this.config.port,
|
|
182
|
+
brokerSocket: getBrokerSocketPath(),
|
|
183
|
+
});
|
|
480
184
|
});
|
|
481
185
|
process.on("SIGHUP", () => this.reload());
|
|
482
|
-
|
|
483
|
-
process.
|
|
186
|
+
// Retain listeners during dispatch: selected SDK dependencies use signal-exit's listener check.
|
|
187
|
+
process.on("SIGINT", () => this.stop());
|
|
188
|
+
process.on("SIGTERM", () => this.stop());
|
|
484
189
|
}
|
|
485
|
-
|
|
486
190
|
private reload(): void {
|
|
487
|
-
if (this.stopping)
|
|
191
|
+
if (this.stopping) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
488
194
|
try {
|
|
489
195
|
const next = loadConfig(this.configPath);
|
|
490
|
-
if (next
|
|
491
|
-
|| !next.tls.ca.equals(this.config.tls.ca) || !next.tls.cert.equals(this.config.tls.cert) || !next.tls.key.equals(this.config.tls.key)) {
|
|
196
|
+
if (!sameListener(next, this.config)) {
|
|
492
197
|
throw new Error("TLS/listener rotation requires restarting the helper.");
|
|
493
198
|
}
|
|
494
199
|
this.config = next;
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
for (const fp of this.losses.keys()) if (!next.clients.has(fp)) this.losses.delete(fp);
|
|
200
|
+
this.revokeRemoved();
|
|
201
|
+
this.launcher.revoke(next.clients).catch(() => log("launch_cleanup", { result: "failed" }));
|
|
498
202
|
log("allowlist_reloaded", { result: "accepted", clients: next.clients.size });
|
|
499
203
|
} catch {
|
|
500
|
-
this.config.clients
|
|
501
|
-
|
|
502
|
-
|
|
204
|
+
this.config = { ...this.config, clients: new Map() };
|
|
205
|
+
this.launcher
|
|
206
|
+
.revoke(this.config.clients)
|
|
207
|
+
.catch(() => log("launch_cleanup", { result: "failed" }));
|
|
208
|
+
for (const session of this.sessions.values()) {
|
|
209
|
+
session.close(
|
|
210
|
+
new Fault(
|
|
211
|
+
503,
|
|
212
|
+
"configuration_invalid",
|
|
213
|
+
"Configuration reload failed; all identities denied.",
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
for (const socket of this.sockets) {
|
|
218
|
+
socket.destroy();
|
|
219
|
+
}
|
|
503
220
|
this.losses.clear();
|
|
504
221
|
log("allowlist_reloaded", { result: "failed_closed" });
|
|
505
222
|
}
|
|
506
223
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
224
|
+
private revokeRemoved(): void {
|
|
225
|
+
for (const [fp, session] of this.sessions) {
|
|
226
|
+
if (!this.allowed(session.identity)) {
|
|
227
|
+
this.drop(
|
|
228
|
+
fp,
|
|
229
|
+
new Fault(403, "revoked", "Identity revoked or changed by configuration reload."),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const connection of this.connections.values()) {
|
|
234
|
+
if (!this.config.clients.has(connection.fingerprint)) {
|
|
235
|
+
this.drop(connection.fingerprint, new Fault(403, "revoked", "Identity revoked."));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const fp of this.losses.keys()) {
|
|
239
|
+
if (!this.config.clients.has(fp)) {
|
|
240
|
+
this.losses.delete(fp);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
private stop(): void {
|
|
245
|
+
this.shutdown().catch(() => {
|
|
246
|
+
process.exitCode = 1;
|
|
247
|
+
log("shutdown", { result: "cleanup_failed" });
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
private async shutdown(): Promise<void> {
|
|
251
|
+
if (this.stopping) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
510
254
|
this.stopping = true;
|
|
511
255
|
this.server.close();
|
|
512
|
-
|
|
513
|
-
for (const
|
|
256
|
+
const sessions = [...this.sessions.values()];
|
|
257
|
+
for (const session of sessions) {
|
|
258
|
+
session.close(new Fault(503, "shutting_down", "Bridge is shutting down."));
|
|
259
|
+
}
|
|
260
|
+
for (const socket of this.sockets) {
|
|
261
|
+
socket.destroy();
|
|
262
|
+
}
|
|
263
|
+
await Promise.all([
|
|
264
|
+
...this.handlers,
|
|
265
|
+
...sessions.map((session) => session.settled()),
|
|
266
|
+
this.launcher.shutdown(),
|
|
267
|
+
]);
|
|
514
268
|
log("shutdown", { result: "closed" });
|
|
515
269
|
}
|
|
516
|
-
|
|
270
|
+
private admission(req: IncomingMessage): {
|
|
271
|
+
readonly identity: Identity;
|
|
272
|
+
readonly expires: number;
|
|
273
|
+
} {
|
|
274
|
+
const socket = req.socket;
|
|
275
|
+
const connection = socket instanceof TLSSocket ? this.connections.get(socket) : undefined;
|
|
276
|
+
const identity = connection && this.config.clients.get(connection.fingerprint);
|
|
277
|
+
if (
|
|
278
|
+
!connection ||
|
|
279
|
+
!identity ||
|
|
280
|
+
!(socket instanceof TLSSocket) ||
|
|
281
|
+
!socket.authorized ||
|
|
282
|
+
this.stopping
|
|
283
|
+
) {
|
|
284
|
+
throw new Fault(403, "unauthorized", "Client certificate is not authorized.");
|
|
285
|
+
}
|
|
286
|
+
if (connection.expires <= Date.now()) {
|
|
287
|
+
const fault = new Fault(403, "certificate_expired", "Client certificate expired.");
|
|
288
|
+
this.drop(connection.fingerprint, fault);
|
|
289
|
+
throw fault;
|
|
290
|
+
}
|
|
291
|
+
return { identity, expires: connection.expires };
|
|
292
|
+
}
|
|
517
293
|
private async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
518
294
|
const requestId = randomUUID();
|
|
519
295
|
const started = Date.now();
|
|
@@ -521,126 +297,103 @@ class Bridge {
|
|
|
521
297
|
let controller: AbortController | undefined;
|
|
522
298
|
let deadline: NodeJS.Timeout | undefined;
|
|
523
299
|
let peer: string | undefined;
|
|
524
|
-
const abort = () =>
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
res.
|
|
530
|
-
|
|
300
|
+
const abort = () =>
|
|
301
|
+
session?.close(
|
|
302
|
+
new Fault(499, "client_aborted", "HTTP client disconnected; delivery may be unknown."),
|
|
303
|
+
);
|
|
304
|
+
const onClose = () => {
|
|
305
|
+
if (!res.writableFinished) {
|
|
306
|
+
abort();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const setDeadline = (ms: number) => {
|
|
310
|
+
clearTimeout(deadline);
|
|
311
|
+
deadline = setTimeout(
|
|
312
|
+
() =>
|
|
313
|
+
session?.close(
|
|
314
|
+
new Fault(
|
|
315
|
+
504,
|
|
316
|
+
"request_timeout",
|
|
317
|
+
"HTTP operation deadline exceeded; delivery may be unknown.",
|
|
318
|
+
),
|
|
319
|
+
),
|
|
320
|
+
Math.max(1, ms - (Date.now() - started)),
|
|
321
|
+
);
|
|
531
322
|
};
|
|
532
323
|
try {
|
|
533
|
-
const
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (connection.expires <= Date.now()) { this.drop(connection.fingerprint, new Fault(403, "certificate_expired", "Client certificate expired.")); throw new Fault(403, "certificate_expired", "Client certificate expired."); }
|
|
538
|
-
if (req.headers.origin !== undefined || Object.keys(req.headers).some((key) => key.startsWith("sec-fetch-"))) throw new Fault(403, "browser_forbidden", "Browser access is not supported.");
|
|
539
|
-
const route = `${req.method} ${req.url}`;
|
|
540
|
-
if (!["GET /v1/list", "GET /v1/inbox", "POST /v1/register", "POST /v1/send", "POST /v1/ask", "POST /v1/reply", "POST /v1/ack"].includes(route)) throw new Fault(404, "not_found", "Unknown method or path.");
|
|
541
|
-
if (req.method === "GET" && (req.headers["transfer-encoding"] !== undefined || req.headers["content-length"] !== undefined && req.headers["content-length"] !== "0")) throw new Fault(400, "invalid_input", "GET requests must not have a body.");
|
|
542
|
-
session = this.sessions.get(identity.fingerprint256);
|
|
543
|
-
if (!session) { session = new Session(this, identity, connection.expires); this.sessions.set(identity.fingerprint256, session); }
|
|
544
|
-
session.guard();
|
|
545
|
-
if (session.requests.size >= MAX_REQUESTS) throw new Fault(429, "too_many_requests", "Too many concurrent requests for this identity.");
|
|
546
|
-
controller = new AbortController();
|
|
324
|
+
const { identity, expires } = this.admission(req);
|
|
325
|
+
const route = routeFor(req);
|
|
326
|
+
session = this.session(identity, expires);
|
|
327
|
+
controller = session.begin();
|
|
547
328
|
const signal = controller.signal;
|
|
548
|
-
session.requests.add(controller);
|
|
549
|
-
session.touch();
|
|
550
329
|
req.once("aborted", abort);
|
|
551
330
|
res.once("close", onClose);
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
let input: Record<string, unknown> = {};
|
|
558
|
-
if (req.method === "POST") {
|
|
559
|
-
const fields = route === "POST /v1/register" ? [] : route === "POST /v1/ack" ? ["ids"] : route === "POST /v1/reply" ? ["replyTo", "message"] : route === "POST /v1/ask" ? ["to", "message", "timeoutMs"] : ["to", "message"];
|
|
560
|
-
input = object(await body(req, signal), fields);
|
|
561
|
-
}
|
|
562
|
-
let message: string | undefined;
|
|
563
|
-
let to: string | undefined;
|
|
564
|
-
let replyTo: string | undefined;
|
|
565
|
-
let ids: string[] = [];
|
|
566
|
-
let timeoutMs = ASK_TIMEOUT;
|
|
567
|
-
if (["POST /v1/send", "POST /v1/ask", "POST /v1/reply"].includes(route)) message = text(input.message);
|
|
568
|
-
if (["POST /v1/send", "POST /v1/ask"].includes(route)) to = label(input.to, "to").trim();
|
|
569
|
-
if (route === "POST /v1/reply") replyTo = label(input.replyTo, "replyTo");
|
|
570
|
-
if (route === "POST /v1/ask") {
|
|
571
|
-
const timeout = input.timeoutMs === undefined ? ASK_TIMEOUT : input.timeoutMs;
|
|
572
|
-
if (typeof timeout !== "number" || !Number.isInteger(timeout) || timeout < 100 || timeout > ASK_TIMEOUT) throw new Fault(400, "invalid_input", "timeoutMs must be an integer from 100 to 120000.");
|
|
573
|
-
timeoutMs = timeout;
|
|
574
|
-
setDeadline(timeoutMs + 15000);
|
|
575
|
-
}
|
|
576
|
-
if (route === "POST /v1/ack") {
|
|
577
|
-
if (!Array.isArray(input.ids) || input.ids.length > INBOX_COUNT) throw new Fault(400, "invalid_input", "ids must be an array of at most 256 message IDs.");
|
|
578
|
-
ids = [...new Set(input.ids.map((id) => label(id, "message ID")))];
|
|
579
|
-
}
|
|
580
|
-
session.guard(signal);
|
|
581
|
-
if (route !== "POST /v1/ask") await session.connect(signal);
|
|
582
|
-
let data: Record<string, unknown>;
|
|
583
|
-
switch (route) {
|
|
584
|
-
case "POST /v1/register": data = { sessionId: session.client.sessionId, name: identity.name }; break;
|
|
585
|
-
case "GET /v1/list": {
|
|
586
|
-
const sessions = await session.peers(signal);
|
|
587
|
-
data = { sessionId: session.client.sessionId, sessions: sessions.map(({ topics: _topics, subscriptions: _subscriptions, ...entry }) => ({ ...entry, target: formatSessionTarget(entry, sessions) })) };
|
|
588
|
-
break;
|
|
589
|
-
}
|
|
590
|
-
case "GET /v1/inbox": {
|
|
591
|
-
const lostMessages = this.losses.get(identity.fingerprint256) ?? 0;
|
|
592
|
-
data = { messages: [...session.inbox.values()].map(({ from, message }) => ({ from, message })), overflow: lostMessages > 0, lostMessages };
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
case "POST /v1/ack": {
|
|
596
|
-
const acked: string[] = [];
|
|
597
|
-
const retained: string[] = [];
|
|
598
|
-
for (const id of ids) {
|
|
599
|
-
const entry = session.inbox.get(id);
|
|
600
|
-
if (entry?.message.expectsReply) retained.push(id);
|
|
601
|
-
else if (entry) { session.remove(id); acked.push(id); }
|
|
602
|
-
}
|
|
603
|
-
session.presence();
|
|
604
|
-
data = { acked, retained };
|
|
605
|
-
break;
|
|
606
|
-
}
|
|
607
|
-
case "POST /v1/send": {
|
|
608
|
-
peer = (await session.target(to!, signal)).id;
|
|
609
|
-
data = { ...await session.send(peer, message!, signal, requestId) };
|
|
610
|
-
break;
|
|
611
|
-
}
|
|
612
|
-
case "POST /v1/ask": data = await session.ask(to!, message!, timeoutMs, signal, requestId); break;
|
|
613
|
-
case "POST /v1/reply": {
|
|
614
|
-
const entry = session.inbox.get(replyTo!);
|
|
615
|
-
if (!entry?.message.expectsReply) throw new Fault(404, "unknown_ask", "replyTo must identify an unanswered inbound ask.");
|
|
616
|
-
if (entry.replying) throw new Fault(409, "reply_in_progress", "This ask already has a reply in flight.");
|
|
617
|
-
entry.replying = true;
|
|
618
|
-
peer = entry.from.id;
|
|
619
|
-
try {
|
|
620
|
-
const peers = await session.peers(signal);
|
|
621
|
-
if (!peers.some((candidate) => candidate.id === peer)) throw new Fault(409, "peer_offline", "Original ask sender is disconnected.");
|
|
622
|
-
session.guard(signal);
|
|
623
|
-
if (session.inbox.get(replyTo!) !== entry) throw new Fault(409, "ask_retired", "Inbound ask is no longer replyable.");
|
|
624
|
-
data = { ...await session.send(peer, message!, signal, requestId, { replyTo }) };
|
|
625
|
-
session.remove(replyTo!);
|
|
626
|
-
session.presence();
|
|
627
|
-
} finally { entry.replying = false; }
|
|
628
|
-
break;
|
|
629
|
-
}
|
|
630
|
-
default: throw new Fault(404, "not_found", "Unknown method or path.");
|
|
331
|
+
const duration = route === "POST /v1/ask" ? ASK_TIMEOUT + 15000 : 15000;
|
|
332
|
+
setDeadline(route === "POST /v1/start" ? 75000 : duration);
|
|
333
|
+
const operation = await operationFor(req, route, signal);
|
|
334
|
+
if (operation.route === "POST /v1/ask") {
|
|
335
|
+
setDeadline(operation.timeoutMs + 15000);
|
|
631
336
|
}
|
|
337
|
+
const data = await this.execute(operation, session, {
|
|
338
|
+
signal,
|
|
339
|
+
requestId,
|
|
340
|
+
expires,
|
|
341
|
+
onPeer: (id) => {
|
|
342
|
+
peer = id;
|
|
343
|
+
},
|
|
344
|
+
});
|
|
632
345
|
session.guard(signal);
|
|
633
346
|
log("request", { ...session.fields(), requestId, peer, result: "ok" });
|
|
634
|
-
reply(200, { ok: true, ...data });
|
|
347
|
+
reply(res, requestId, 200, { ok: true, ...data });
|
|
635
348
|
} catch (error) {
|
|
636
|
-
const fault =
|
|
349
|
+
const fault =
|
|
350
|
+
error instanceof Fault
|
|
351
|
+
? error
|
|
352
|
+
: new Fault(503, "broker_unavailable", "Local broker operation failed.");
|
|
637
353
|
log("request", { ...session?.fields(), requestId, peer, result: fault.code });
|
|
638
|
-
reply(
|
|
354
|
+
reply(res, requestId, fault.status, {
|
|
355
|
+
ok: false,
|
|
356
|
+
error: { code: fault.code, message: fault.message, ...fault.details },
|
|
357
|
+
});
|
|
639
358
|
} finally {
|
|
640
359
|
clearTimeout(deadline);
|
|
641
360
|
req.off("aborted", abort);
|
|
642
361
|
res.off("close", onClose);
|
|
643
|
-
if (controller
|
|
362
|
+
if (controller && session) {
|
|
363
|
+
session.end(controller);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
private async execute(
|
|
368
|
+
operation: Operation,
|
|
369
|
+
session: Readonly<Session>,
|
|
370
|
+
context: {
|
|
371
|
+
readonly signal: AbortSignal;
|
|
372
|
+
readonly requestId: string;
|
|
373
|
+
readonly expires: number;
|
|
374
|
+
readonly onPeer: (id: string) => void;
|
|
375
|
+
},
|
|
376
|
+
) {
|
|
377
|
+
switch (operation.route) {
|
|
378
|
+
case "POST /v1/start":
|
|
379
|
+
return this.launcher.start(operation.name, {
|
|
380
|
+
identity: session.identity,
|
|
381
|
+
expires: context.expires,
|
|
382
|
+
signal: context.signal,
|
|
383
|
+
peers: (signal) => session.connectedPeers(signal),
|
|
384
|
+
});
|
|
385
|
+
case "GET /v1/sessions":
|
|
386
|
+
return this.launcher.list(session.identity);
|
|
387
|
+
case "POST /v1/stop":
|
|
388
|
+
return this.launcher.stop(session.identity, operation.sessionId);
|
|
389
|
+
case "GET /v1/list":
|
|
390
|
+
case "GET /v1/inbox":
|
|
391
|
+
case "POST /v1/register":
|
|
392
|
+
case "POST /v1/ask":
|
|
393
|
+
case "POST /v1/send":
|
|
394
|
+
case "POST /v1/reply":
|
|
395
|
+
case "POST /v1/ack":
|
|
396
|
+
return session.execute(operation, context);
|
|
644
397
|
}
|
|
645
398
|
}
|
|
646
399
|
}
|
|
@@ -648,10 +401,12 @@ class Bridge {
|
|
|
648
401
|
function main(): void {
|
|
649
402
|
const args = process.argv.slice(2);
|
|
650
403
|
if (args.length === 1 && (args[0] === "-h" || args[0] === "--help")) {
|
|
651
|
-
process.stdout.write(
|
|
404
|
+
process.stdout.write(
|
|
405
|
+
"Usage: node dist/pi-intercom/bridge.js --config PATH\n\nOptional loopback HTTPS/mTLS bridge for the local pi-intercom broker.\nConfig and TLS key must be owned by you with mode 0600. Never starts the broker.\nSIGHUP reloads the client allowlist (invalid reload denies all clients).\nRestart this helper for TLS/listener rotation. SIGINT/SIGTERM disconnect peers.\n\nExample: node dist/pi-intercom/bridge.js --config ~/.pi/agent/intercom/bridge.json\n",
|
|
406
|
+
);
|
|
652
407
|
return;
|
|
653
408
|
}
|
|
654
|
-
if (args.length !== 2 || args[0] !== "--config" ||
|
|
409
|
+
if (args.length !== 2 || args[0] !== "--config" || args[1] === "") {
|
|
655
410
|
process.stderr.write("Usage: node dist/pi-intercom/bridge.js --config PATH (or --help)\n");
|
|
656
411
|
process.exitCode = 2;
|
|
657
412
|
return;
|
|
@@ -660,9 +415,10 @@ function main(): void {
|
|
|
660
415
|
const path = resolve(args[1]);
|
|
661
416
|
new Bridge(loadConfig(path), path).start();
|
|
662
417
|
} catch {
|
|
663
|
-
process.stderr.write(
|
|
418
|
+
process.stderr.write(
|
|
419
|
+
"Bridge startup failed: check config schema, file ownership/mode 0600, absolute TLS/cwd paths, TLS credentials, and launch policy (0700 sessionDir, Pi packageRoot, extension paths).\n",
|
|
420
|
+
);
|
|
664
421
|
process.exitCode = 1;
|
|
665
422
|
}
|
|
666
423
|
}
|
|
667
|
-
|
|
668
424
|
main();
|