@cuna_labs/cli 0.1.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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +169 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/api/capability-evidence.d.ts +19 -0
- package/dist/api/capability-evidence.d.ts.map +1 -0
- package/dist/api/capability-evidence.js +38 -0
- package/dist/api/capability-evidence.js.map +1 -0
- package/dist/api/client.d.ts +167 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +937 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/contracts.d.ts +345 -0
- package/dist/api/contracts.d.ts.map +1 -0
- package/dist/api/contracts.js +976 -0
- package/dist/api/contracts.js.map +1 -0
- package/dist/api/execution-workspaces.d.ts +17 -0
- package/dist/api/execution-workspaces.d.ts.map +1 -0
- package/dist/api/execution-workspaces.js +46 -0
- package/dist/api/execution-workspaces.js.map +1 -0
- package/dist/api/http.d.ts +76 -0
- package/dist/api/http.d.ts.map +1 -0
- package/dist/api/http.js +838 -0
- package/dist/api/http.js.map +1 -0
- package/dist/api/managed-executions.d.ts +37 -0
- package/dist/api/managed-executions.d.ts.map +1 -0
- package/dist/api/managed-executions.js +80 -0
- package/dist/api/managed-executions.js.map +1 -0
- package/dist/api/observer-v2-schema.d.ts +206 -0
- package/dist/api/observer-v2-schema.d.ts.map +1 -0
- package/dist/api/observer-v2-schema.js +264 -0
- package/dist/api/observer-v2-schema.js.map +1 -0
- package/dist/api/observer-v2.d.ts +33 -0
- package/dist/api/observer-v2.d.ts.map +1 -0
- package/dist/api/observer-v2.js +60 -0
- package/dist/api/observer-v2.js.map +1 -0
- package/dist/api/owner-observe-grants-v2-schema.d.ts +678 -0
- package/dist/api/owner-observe-grants-v2-schema.d.ts.map +1 -0
- package/dist/api/owner-observe-grants-v2-schema.js +887 -0
- package/dist/api/owner-observe-grants-v2-schema.js.map +1 -0
- package/dist/api/owner-observe-grants-v2.d.ts +268 -0
- package/dist/api/owner-observe-grants-v2.d.ts.map +1 -0
- package/dist/api/owner-observe-grants-v2.js +604 -0
- package/dist/api/owner-observe-grants-v2.js.map +1 -0
- package/dist/api/provider-v2-schema.d.ts +293 -0
- package/dist/api/provider-v2-schema.d.ts.map +1 -0
- package/dist/api/provider-v2-schema.js +381 -0
- package/dist/api/provider-v2-schema.js.map +1 -0
- package/dist/api/provider-v2.d.ts +55 -0
- package/dist/api/provider-v2.d.ts.map +1 -0
- package/dist/api/provider-v2.js +54 -0
- package/dist/api/provider-v2.js.map +1 -0
- package/dist/api/remote-workspace.d.ts +29 -0
- package/dist/api/remote-workspace.d.ts.map +1 -0
- package/dist/api/remote-workspace.js +62 -0
- package/dist/api/remote-workspace.js.map +1 -0
- package/dist/api/supervisor-live-update.d.ts +248 -0
- package/dist/api/supervisor-live-update.d.ts.map +1 -0
- package/dist/api/supervisor-live-update.js +434 -0
- package/dist/api/supervisor-live-update.js.map +1 -0
- package/dist/api/terminal-reason.d.ts +5 -0
- package/dist/api/terminal-reason.d.ts.map +1 -0
- package/dist/api/terminal-reason.js +12 -0
- package/dist/api/terminal-reason.js.map +1 -0
- package/dist/auth/browser-handoff.d.ts +59 -0
- package/dist/auth/browser-handoff.d.ts.map +1 -0
- package/dist/auth/browser-handoff.js +33 -0
- package/dist/auth/browser-handoff.js.map +1 -0
- package/dist/auth/browser.d.ts +11 -0
- package/dist/auth/browser.d.ts.map +1 -0
- package/dist/auth/browser.js +47 -0
- package/dist/auth/browser.js.map +1 -0
- package/dist/auth/human-client.d.ts +39 -0
- package/dist/auth/human-client.d.ts.map +1 -0
- package/dist/auth/human-client.js +66 -0
- package/dist/auth/human-client.js.map +1 -0
- package/dist/auth/human-contracts.d.ts +52 -0
- package/dist/auth/human-contracts.d.ts.map +1 -0
- package/dist/auth/human-contracts.js +229 -0
- package/dist/auth/human-contracts.js.map +1 -0
- package/dist/auth/human-session.d.ts +48 -0
- package/dist/auth/human-session.d.ts.map +1 -0
- package/dist/auth/human-session.js +902 -0
- package/dist/auth/human-session.js.map +1 -0
- package/dist/auth/onboarding-state.d.ts +31 -0
- package/dist/auth/onboarding-state.d.ts.map +1 -0
- package/dist/auth/onboarding-state.js +52 -0
- package/dist/auth/onboarding-state.js.map +1 -0
- package/dist/auth/pkce.d.ts +15 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +19 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/bin/cuna.d.ts +3 -0
- package/dist/bin/cuna.d.ts.map +1 -0
- package/dist/bin/cuna.js +4 -0
- package/dist/bin/cuna.js.map +1 -0
- package/dist/build-identity.d.ts +20 -0
- package/dist/build-identity.d.ts.map +1 -0
- package/dist/build-identity.js +77 -0
- package/dist/build-identity.js.map +1 -0
- package/dist/cli/command-help.d.ts +9 -0
- package/dist/cli/command-help.d.ts.map +1 -0
- package/dist/cli/command-help.js +324 -0
- package/dist/cli/command-help.js.map +1 -0
- package/dist/cli/help.d.ts +40 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +256 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/output.d.ts +28 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +95 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/parser.d.ts +65 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +294 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/process-entrypoint.d.ts +18 -0
- package/dist/cli/process-entrypoint.d.ts.map +1 -0
- package/dist/cli/process-entrypoint.js +26 -0
- package/dist/cli/process-entrypoint.js.map +1 -0
- package/dist/cli/run.d.ts +114 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +1969 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/commands/commands.d.ts +74 -0
- package/dist/commands/commands.d.ts.map +1 -0
- package/dist/commands/commands.js +2814 -0
- package/dist/commands/commands.js.map +1 -0
- package/dist/config/config.d.ts +86 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +323 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/infra-contract-witness.d.ts +29 -0
- package/dist/config/infra-contract-witness.d.ts.map +1 -0
- package/dist/config/infra-contract-witness.js +30 -0
- package/dist/config/infra-contract-witness.js.map +1 -0
- package/dist/core/browser-url.d.ts +2 -0
- package/dist/core/browser-url.d.ts.map +1 -0
- package/dist/core/browser-url.js +20 -0
- package/dist/core/browser-url.js.map +1 -0
- package/dist/core/deployed-wire-compatibility.d.ts +15 -0
- package/dist/core/deployed-wire-compatibility.d.ts.map +1 -0
- package/dist/core/deployed-wire-compatibility.js +15 -0
- package/dist/core/deployed-wire-compatibility.js.map +1 -0
- package/dist/core/errors.d.ts +59 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +82 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/exit-codes.d.ts +68 -0
- package/dist/core/exit-codes.d.ts.map +1 -0
- package/dist/core/exit-codes.js +84 -0
- package/dist/core/exit-codes.js.map +1 -0
- package/dist/core/instant.d.ts +60 -0
- package/dist/core/instant.d.ts.map +1 -0
- package/dist/core/instant.js +90 -0
- package/dist/core/instant.js.map +1 -0
- package/dist/core/namespace.d.ts +171 -0
- package/dist/core/namespace.d.ts.map +1 -0
- package/dist/core/namespace.js +227 -0
- package/dist/core/namespace.js.map +1 -0
- package/dist/core/observation-budget.d.ts +185 -0
- package/dist/core/observation-budget.d.ts.map +1 -0
- package/dist/core/observation-budget.js +209 -0
- package/dist/core/observation-budget.js.map +1 -0
- package/dist/core/product-web.d.ts +81 -0
- package/dist/core/product-web.d.ts.map +1 -0
- package/dist/core/product-web.js +87 -0
- package/dist/core/product-web.js.map +1 -0
- package/dist/core/validation.d.ts +90 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +199 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/credentials/contracts.d.ts +82 -0
- package/dist/credentials/contracts.d.ts.map +1 -0
- package/dist/credentials/contracts.js +2 -0
- package/dist/credentials/contracts.js.map +1 -0
- package/dist/credentials/errors.d.ts +19 -0
- package/dist/credentials/errors.d.ts.map +1 -0
- package/dist/credentials/errors.js +16 -0
- package/dist/credentials/errors.js.map +1 -0
- package/dist/credentials/index.d.ts +6 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +6 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/local-session.d.ts +101 -0
- package/dist/credentials/local-session.d.ts.map +1 -0
- package/dist/credentials/local-session.js +839 -0
- package/dist/credentials/local-session.js.map +1 -0
- package/dist/credentials/secret-material.d.ts +17 -0
- package/dist/credentials/secret-material.d.ts.map +1 -0
- package/dist/credentials/secret-material.js +49 -0
- package/dist/credentials/secret-material.js.map +1 -0
- package/dist/credentials/vault.d.ts +41 -0
- package/dist/credentials/vault.d.ts.map +1 -0
- package/dist/credentials/vault.js +719 -0
- package/dist/credentials/vault.js.map +1 -0
- package/dist/daemon/endpoint.d.ts +29 -0
- package/dist/daemon/endpoint.d.ts.map +1 -0
- package/dist/daemon/endpoint.js +66 -0
- package/dist/daemon/endpoint.js.map +1 -0
- package/dist/daemon/journal.d.ts +62 -0
- package/dist/daemon/journal.d.ts.map +1 -0
- package/dist/daemon/journal.js +299 -0
- package/dist/daemon/journal.js.map +1 -0
- package/dist/daemon/lease.d.ts +25 -0
- package/dist/daemon/lease.d.ts.map +1 -0
- package/dist/daemon/lease.js +94 -0
- package/dist/daemon/lease.js.map +1 -0
- package/dist/daemon/lifecycle.d.ts +14 -0
- package/dist/daemon/lifecycle.d.ts.map +1 -0
- package/dist/daemon/lifecycle.js +29 -0
- package/dist/daemon/lifecycle.js.map +1 -0
- package/dist/daemon/protocol.d.ts +65 -0
- package/dist/daemon/protocol.d.ts.map +1 -0
- package/dist/daemon/protocol.js +194 -0
- package/dist/daemon/protocol.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +29 -0
- package/dist/daemon/supervisor.d.ts.map +1 -0
- package/dist/daemon/supervisor.js +87 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/daemon/views.d.ts +31 -0
- package/dist/daemon/views.d.ts.map +1 -0
- package/dist/daemon/views.js +86 -0
- package/dist/daemon/views.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/journey/api-effects.d.ts +59 -0
- package/dist/journey/api-effects.d.ts.map +1 -0
- package/dist/journey/api-effects.js +386 -0
- package/dist/journey/api-effects.js.map +1 -0
- package/dist/journey/derived-identity.d.ts +91 -0
- package/dist/journey/derived-identity.d.ts.map +1 -0
- package/dist/journey/derived-identity.js +123 -0
- package/dist/journey/derived-identity.js.map +1 -0
- package/dist/journey/index.d.ts +6 -0
- package/dist/journey/index.d.ts.map +1 -0
- package/dist/journey/index.js +6 -0
- package/dist/journey/index.js.map +1 -0
- package/dist/journey/intent.d.ts +36 -0
- package/dist/journey/intent.d.ts.map +1 -0
- package/dist/journey/intent.js +191 -0
- package/dist/journey/intent.js.map +1 -0
- package/dist/journey/orchestrator.d.ts +140 -0
- package/dist/journey/orchestrator.d.ts.map +1 -0
- package/dist/journey/orchestrator.js +415 -0
- package/dist/journey/orchestrator.js.map +1 -0
- package/dist/journey/provider-launch-intent.d.ts +12 -0
- package/dist/journey/provider-launch-intent.d.ts.map +1 -0
- package/dist/journey/provider-launch-intent.js +40 -0
- package/dist/journey/provider-launch-intent.js.map +1 -0
- package/dist/journey/remote-workspace.d.ts +34 -0
- package/dist/journey/remote-workspace.d.ts.map +1 -0
- package/dist/journey/remote-workspace.js +91 -0
- package/dist/journey/remote-workspace.js.map +1 -0
- package/dist/journey/root-entry.d.ts +56 -0
- package/dist/journey/root-entry.d.ts.map +1 -0
- package/dist/journey/root-entry.js +19 -0
- package/dist/journey/root-entry.js.map +1 -0
- package/dist/journey/selection.d.ts +165 -0
- package/dist/journey/selection.d.ts.map +1 -0
- package/dist/journey/selection.js +565 -0
- package/dist/journey/selection.js.map +1 -0
- package/dist/journey/session-failure.d.ts +4 -0
- package/dist/journey/session-failure.d.ts.map +1 -0
- package/dist/journey/session-failure.js +28 -0
- package/dist/journey/session-failure.js.map +1 -0
- package/dist/journey/workspace-effects.d.ts +30 -0
- package/dist/journey/workspace-effects.d.ts.map +1 -0
- package/dist/journey/workspace-effects.js +348 -0
- package/dist/journey/workspace-effects.js.map +1 -0
- package/dist/local-actions/broker.d.ts +32 -0
- package/dist/local-actions/broker.d.ts.map +1 -0
- package/dist/local-actions/broker.js +400 -0
- package/dist/local-actions/broker.js.map +1 -0
- package/dist/local-actions/browser-action.d.ts +78 -0
- package/dist/local-actions/browser-action.d.ts.map +1 -0
- package/dist/local-actions/browser-action.js +307 -0
- package/dist/local-actions/browser-action.js.map +1 -0
- package/dist/local-actions/callback-relay.d.ts +45 -0
- package/dist/local-actions/callback-relay.d.ts.map +1 -0
- package/dist/local-actions/callback-relay.js +265 -0
- package/dist/local-actions/callback-relay.js.map +1 -0
- package/dist/local-actions/clipboard.d.ts +3 -0
- package/dist/local-actions/clipboard.d.ts.map +1 -0
- package/dist/local-actions/clipboard.js +18 -0
- package/dist/local-actions/clipboard.js.map +1 -0
- package/dist/local-actions/contracts.d.ts +63 -0
- package/dist/local-actions/contracts.d.ts.map +1 -0
- package/dist/local-actions/contracts.js +18 -0
- package/dist/local-actions/contracts.js.map +1 -0
- package/dist/local-actions/desktop-presentation.d.ts +141 -0
- package/dist/local-actions/desktop-presentation.d.ts.map +1 -0
- package/dist/local-actions/desktop-presentation.js +253 -0
- package/dist/local-actions/desktop-presentation.js.map +1 -0
- package/dist/local-actions/device-selection.d.ts +43 -0
- package/dist/local-actions/device-selection.d.ts.map +1 -0
- package/dist/local-actions/device-selection.js +154 -0
- package/dist/local-actions/device-selection.js.map +1 -0
- package/dist/local-actions/executor.d.ts +49 -0
- package/dist/local-actions/executor.d.ts.map +1 -0
- package/dist/local-actions/executor.js +329 -0
- package/dist/local-actions/executor.js.map +1 -0
- package/dist/local-actions/file-transfers.d.ts +141 -0
- package/dist/local-actions/file-transfers.d.ts.map +1 -0
- package/dist/local-actions/file-transfers.js +446 -0
- package/dist/local-actions/file-transfers.js.map +1 -0
- package/dist/local-actions/foreground-notifications.d.ts +45 -0
- package/dist/local-actions/foreground-notifications.d.ts.map +1 -0
- package/dist/local-actions/foreground-notifications.js +163 -0
- package/dist/local-actions/foreground-notifications.js.map +1 -0
- package/dist/local-actions/git-signing.d.ts +39 -0
- package/dist/local-actions/git-signing.d.ts.map +1 -0
- package/dist/local-actions/git-signing.js +135 -0
- package/dist/local-actions/git-signing.js.map +1 -0
- package/dist/local-actions/index.d.ts +19 -0
- package/dist/local-actions/index.d.ts.map +1 -0
- package/dist/local-actions/index.js +19 -0
- package/dist/local-actions/index.js.map +1 -0
- package/dist/local-actions/local-service-actions.d.ts +87 -0
- package/dist/local-actions/local-service-actions.d.ts.map +1 -0
- package/dist/local-actions/local-service-actions.js +347 -0
- package/dist/local-actions/local-service-actions.js.map +1 -0
- package/dist/local-actions/loopback.d.ts +5 -0
- package/dist/local-actions/loopback.d.ts.map +1 -0
- package/dist/local-actions/loopback.js +15 -0
- package/dist/local-actions/loopback.js.map +1 -0
- package/dist/local-actions/policy-store.d.ts +7 -0
- package/dist/local-actions/policy-store.d.ts.map +1 -0
- package/dist/local-actions/policy-store.js +70 -0
- package/dist/local-actions/policy-store.js.map +1 -0
- package/dist/local-actions/policy.d.ts +21 -0
- package/dist/local-actions/policy.d.ts.map +1 -0
- package/dist/local-actions/policy.js +43 -0
- package/dist/local-actions/policy.js.map +1 -0
- package/dist/local-actions/private-port-forward.d.ts +36 -0
- package/dist/local-actions/private-port-forward.d.ts.map +1 -0
- package/dist/local-actions/private-port-forward.js +154 -0
- package/dist/local-actions/private-port-forward.js.map +1 -0
- package/dist/local-actions/provider-mcp-config.d.ts +101 -0
- package/dist/local-actions/provider-mcp-config.d.ts.map +1 -0
- package/dist/local-actions/provider-mcp-config.js +166 -0
- package/dist/local-actions/provider-mcp-config.js.map +1 -0
- package/dist/local-actions/providers.d.ts +12 -0
- package/dist/local-actions/providers.d.ts.map +1 -0
- package/dist/local-actions/providers.js +42 -0
- package/dist/local-actions/providers.js.map +1 -0
- package/dist/local-actions/schemas.d.ts +4 -0
- package/dist/local-actions/schemas.d.ts.map +1 -0
- package/dist/local-actions/schemas.js +314 -0
- package/dist/local-actions/schemas.js.map +1 -0
- package/dist/local-actions/sensitive-consent.d.ts +32 -0
- package/dist/local-actions/sensitive-consent.d.ts.map +1 -0
- package/dist/local-actions/sensitive-consent.js +47 -0
- package/dist/local-actions/sensitive-consent.js.map +1 -0
- package/dist/machines/command-launch.d.ts +17 -0
- package/dist/machines/command-launch.d.ts.map +1 -0
- package/dist/machines/command-launch.js +52 -0
- package/dist/machines/command-launch.js.map +1 -0
- package/dist/machines/execution-receipt.d.ts +18 -0
- package/dist/machines/execution-receipt.d.ts.map +1 -0
- package/dist/machines/execution-receipt.js +87 -0
- package/dist/machines/execution-receipt.js.map +1 -0
- package/dist/machines/executions-screen.d.ts +6 -0
- package/dist/machines/executions-screen.d.ts.map +1 -0
- package/dist/machines/executions-screen.js +410 -0
- package/dist/machines/executions-screen.js.map +1 -0
- package/dist/machines/explorer.d.ts +75 -0
- package/dist/machines/explorer.d.ts.map +1 -0
- package/dist/machines/explorer.js +1726 -0
- package/dist/machines/explorer.js.map +1 -0
- package/dist/machines/live-supervisor-update.d.ts +325 -0
- package/dist/machines/live-supervisor-update.d.ts.map +1 -0
- package/dist/machines/live-supervisor-update.js +513 -0
- package/dist/machines/live-supervisor-update.js.map +1 -0
- package/dist/machines/machine-first.d.ts +123 -0
- package/dist/machines/machine-first.d.ts.map +1 -0
- package/dist/machines/machine-first.js +119 -0
- package/dist/machines/machine-first.js.map +1 -0
- package/dist/machines/opencode-supervisor.d.ts +51 -0
- package/dist/machines/opencode-supervisor.d.ts.map +1 -0
- package/dist/machines/opencode-supervisor.js +124 -0
- package/dist/machines/opencode-supervisor.js.map +1 -0
- package/dist/machines/pagination.d.ts +4 -0
- package/dist/machines/pagination.d.ts.map +1 -0
- package/dist/machines/pagination.js +7 -0
- package/dist/machines/pagination.js.map +1 -0
- package/dist/machines/provider-availability.d.ts +42 -0
- package/dist/machines/provider-availability.d.ts.map +1 -0
- package/dist/machines/provider-availability.js +82 -0
- package/dist/machines/provider-availability.js.map +1 -0
- package/dist/machines/provider-screen.d.ts +12 -0
- package/dist/machines/provider-screen.d.ts.map +1 -0
- package/dist/machines/provider-screen.js +129 -0
- package/dist/machines/provider-screen.js.map +1 -0
- package/dist/machines/session-actionability.d.ts +86 -0
- package/dist/machines/session-actionability.d.ts.map +1 -0
- package/dist/machines/session-actionability.js +125 -0
- package/dist/machines/session-actionability.js.map +1 -0
- package/dist/machines/session-visibility.d.ts +139 -0
- package/dist/machines/session-visibility.d.ts.map +1 -0
- package/dist/machines/session-visibility.js +109 -0
- package/dist/machines/session-visibility.js.map +1 -0
- package/dist/platform/adapter.d.ts +62 -0
- package/dist/platform/adapter.d.ts.map +1 -0
- package/dist/platform/adapter.js +240 -0
- package/dist/platform/adapter.js.map +1 -0
- package/dist/platform/support.d.ts +12 -0
- package/dist/platform/support.d.ts.map +1 -0
- package/dist/platform/support.js +25 -0
- package/dist/platform/support.js.map +1 -0
- package/dist/pty/contracts.d.ts +45 -0
- package/dist/pty/contracts.d.ts.map +1 -0
- package/dist/pty/contracts.js +2 -0
- package/dist/pty/contracts.js.map +1 -0
- package/dist/pty/evidence-gate.d.ts +14 -0
- package/dist/pty/evidence-gate.d.ts.map +1 -0
- package/dist/pty/evidence-gate.js +35 -0
- package/dist/pty/evidence-gate.js.map +1 -0
- package/dist/pty/index.d.ts +5 -0
- package/dist/pty/index.d.ts.map +1 -0
- package/dist/pty/index.js +5 -0
- package/dist/pty/index.js.map +1 -0
- package/dist/pty/node-host-terminal.d.ts +14 -0
- package/dist/pty/node-host-terminal.d.ts.map +1 -0
- package/dist/pty/node-host-terminal.js +173 -0
- package/dist/pty/node-host-terminal.js.map +1 -0
- package/dist/pty/node-process.d.ts +26 -0
- package/dist/pty/node-process.d.ts.map +1 -0
- package/dist/pty/node-process.js +102 -0
- package/dist/pty/node-process.js.map +1 -0
- package/dist/runtime/api-terminal-control-plane.d.ts +7 -0
- package/dist/runtime/api-terminal-control-plane.d.ts.map +1 -0
- package/dist/runtime/api-terminal-control-plane.js +95 -0
- package/dist/runtime/api-terminal-control-plane.js.map +1 -0
- package/dist/runtime/backoff.d.ts +17 -0
- package/dist/runtime/backoff.d.ts.map +1 -0
- package/dist/runtime/backoff.js +61 -0
- package/dist/runtime/backoff.js.map +1 -0
- package/dist/runtime/boundary.d.ts +171 -0
- package/dist/runtime/boundary.d.ts.map +1 -0
- package/dist/runtime/boundary.js +1858 -0
- package/dist/runtime/boundary.js.map +1 -0
- package/dist/runtime/capability-gate.d.ts +25 -0
- package/dist/runtime/capability-gate.d.ts.map +1 -0
- package/dist/runtime/capability-gate.js +77 -0
- package/dist/runtime/capability-gate.js.map +1 -0
- package/dist/runtime/contracts.d.ts +25 -0
- package/dist/runtime/contracts.d.ts.map +1 -0
- package/dist/runtime/contracts.js +66 -0
- package/dist/runtime/contracts.js.map +1 -0
- package/dist/runtime/errors.d.ts +20 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/errors.js +22 -0
- package/dist/runtime/errors.js.map +1 -0
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +9 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/node-foreground-session.d.ts +50 -0
- package/dist/runtime/node-foreground-session.d.ts.map +1 -0
- package/dist/runtime/node-foreground-session.js +431 -0
- package/dist/runtime/node-foreground-session.js.map +1 -0
- package/dist/runtime/node-websocket-connector.d.ts +6 -0
- package/dist/runtime/node-websocket-connector.d.ts.map +1 -0
- package/dist/runtime/node-websocket-connector.js +344 -0
- package/dist/runtime/node-websocket-connector.js.map +1 -0
- package/dist/runtime/observer-screen.d.ts +6 -0
- package/dist/runtime/observer-screen.d.ts.map +1 -0
- package/dist/runtime/observer-screen.js +144 -0
- package/dist/runtime/observer-screen.js.map +1 -0
- package/dist/runtime/owner-grant-operations.d.ts +136 -0
- package/dist/runtime/owner-grant-operations.d.ts.map +1 -0
- package/dist/runtime/owner-grant-operations.js +216 -0
- package/dist/runtime/owner-grant-operations.js.map +1 -0
- package/dist/runtime/owner-grants-screen.d.ts +40 -0
- package/dist/runtime/owner-grants-screen.d.ts.map +1 -0
- package/dist/runtime/owner-grants-screen.js +987 -0
- package/dist/runtime/owner-grants-screen.js.map +1 -0
- package/dist/runtime/terminal-transport.d.ts +85 -0
- package/dist/runtime/terminal-transport.d.ts.map +1 -0
- package/dist/runtime/terminal-transport.js +152 -0
- package/dist/runtime/terminal-transport.js.map +1 -0
- package/dist/sync/continuous-sync-supervisor.d.ts +107 -0
- package/dist/sync/continuous-sync-supervisor.d.ts.map +1 -0
- package/dist/sync/continuous-sync-supervisor.js +1117 -0
- package/dist/sync/continuous-sync-supervisor.js.map +1 -0
- package/dist/sync/index.d.ts +9 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +9 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/journal.d.ts +57 -0
- package/dist/sync/journal.d.ts.map +1 -0
- package/dist/sync/journal.js +685 -0
- package/dist/sync/journal.js.map +1 -0
- package/dist/sync/revisions.d.ts +118 -0
- package/dist/sync/revisions.d.ts.map +1 -0
- package/dist/sync/revisions.js +364 -0
- package/dist/sync/revisions.js.map +1 -0
- package/dist/sync/supervisor.d.ts +107 -0
- package/dist/sync/supervisor.d.ts.map +1 -0
- package/dist/sync/supervisor.js +246 -0
- package/dist/sync/supervisor.js.map +1 -0
- package/dist/sync/workspace-sync-client.d.ts +18 -0
- package/dist/sync/workspace-sync-client.d.ts.map +1 -0
- package/dist/sync/workspace-sync-client.js +150 -0
- package/dist/sync/workspace-sync-client.js.map +1 -0
- package/dist/sync/workspace-sync-coordinator.d.ts +76 -0
- package/dist/sync/workspace-sync-coordinator.d.ts.map +1 -0
- package/dist/sync/workspace-sync-coordinator.js +621 -0
- package/dist/sync/workspace-sync-coordinator.js.map +1 -0
- package/dist/sync/workspace-sync-product-service.d.ts +79 -0
- package/dist/sync/workspace-sync-product-service.d.ts.map +1 -0
- package/dist/sync/workspace-sync-product-service.js +398 -0
- package/dist/sync/workspace-sync-product-service.js.map +1 -0
- package/dist/sync/workspace-sync-protocol.d.ts +145 -0
- package/dist/sync/workspace-sync-protocol.d.ts.map +1 -0
- package/dist/sync/workspace-sync-protocol.js +347 -0
- package/dist/sync/workspace-sync-protocol.js.map +1 -0
- package/dist/terminal/appbar.d.ts +40 -0
- package/dist/terminal/appbar.d.ts.map +1 -0
- package/dist/terminal/appbar.js +55 -0
- package/dist/terminal/appbar.js.map +1 -0
- package/dist/terminal/cell-width.d.ts +3 -0
- package/dist/terminal/cell-width.d.ts.map +1 -0
- package/dist/terminal/cell-width.js +53 -0
- package/dist/terminal/cell-width.js.map +1 -0
- package/dist/terminal/codec.d.ts +107 -0
- package/dist/terminal/codec.d.ts.map +1 -0
- package/dist/terminal/codec.js +538 -0
- package/dist/terminal/codec.js.map +1 -0
- package/dist/terminal/foreground.d.ts +135 -0
- package/dist/terminal/foreground.d.ts.map +1 -0
- package/dist/terminal/foreground.js +2097 -0
- package/dist/terminal/foreground.js.map +1 -0
- package/dist/terminal/generated/terminal-wire-v1.d.ts +26 -0
- package/dist/terminal/generated/terminal-wire-v1.d.ts.map +1 -0
- package/dist/terminal/generated/terminal-wire-v1.js +27 -0
- package/dist/terminal/generated/terminal-wire-v1.js.map +1 -0
- package/dist/terminal/mode.d.ts +33 -0
- package/dist/terminal/mode.d.ts.map +1 -0
- package/dist/terminal/mode.js +99 -0
- package/dist/terminal/mode.js.map +1 -0
- package/dist/terminal/passthrough.d.ts +47 -0
- package/dist/terminal/passthrough.d.ts.map +1 -0
- package/dist/terminal/passthrough.js +666 -0
- package/dist/terminal/passthrough.js.map +1 -0
- package/dist/terminal/resume.d.ts +58 -0
- package/dist/terminal/resume.d.ts.map +1 -0
- package/dist/terminal/resume.js +185 -0
- package/dist/terminal/resume.js.map +1 -0
- package/dist/terminal/viewport.d.ts +93 -0
- package/dist/terminal/viewport.d.ts.map +1 -0
- package/dist/terminal/viewport.js +266 -0
- package/dist/terminal/viewport.js.map +1 -0
- package/dist/terminal/workbench.d.ts +32 -0
- package/dist/terminal/workbench.d.ts.map +1 -0
- package/dist/terminal/workbench.js +316 -0
- package/dist/terminal/workbench.js.map +1 -0
- package/dist/terminal/xterm-vte.d.ts +49 -0
- package/dist/terminal/xterm-vte.d.ts.map +1 -0
- package/dist/terminal/xterm-vte.js +602 -0
- package/dist/terminal/xterm-vte.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace/binding-path.d.ts +17 -0
- package/dist/workspace/binding-path.d.ts.map +1 -0
- package/dist/workspace/binding-path.js +91 -0
- package/dist/workspace/binding-path.js.map +1 -0
- package/dist/workspace/binding-store.d.ts +107 -0
- package/dist/workspace/binding-store.d.ts.map +1 -0
- package/dist/workspace/binding-store.js +783 -0
- package/dist/workspace/binding-store.js.map +1 -0
- package/dist/workspace/binding.d.ts +35 -0
- package/dist/workspace/binding.d.ts.map +1 -0
- package/dist/workspace/binding.js +70 -0
- package/dist/workspace/binding.js.map +1 -0
- package/dist/workspace/errors.d.ts +4 -0
- package/dist/workspace/errors.d.ts.map +1 -0
- package/dist/workspace/errors.js +19 -0
- package/dist/workspace/errors.js.map +1 -0
- package/dist/workspace/exclusion.d.ts +21 -0
- package/dist/workspace/exclusion.d.ts.map +1 -0
- package/dist/workspace/exclusion.js +243 -0
- package/dist/workspace/exclusion.js.map +1 -0
- package/dist/workspace/index.d.ts +8 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +8 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/lifecycle.d.ts +56 -0
- package/dist/workspace/lifecycle.d.ts.map +1 -0
- package/dist/workspace/lifecycle.js +94 -0
- package/dist/workspace/lifecycle.js.map +1 -0
- package/dist/workspace/manifest.d.ts +47 -0
- package/dist/workspace/manifest.d.ts.map +1 -0
- package/dist/workspace/manifest.js +243 -0
- package/dist/workspace/manifest.js.map +1 -0
- package/dist/workspace/paths.d.ts +14 -0
- package/dist/workspace/paths.d.ts.map +1 -0
- package/dist/workspace/paths.js +67 -0
- package/dist/workspace/paths.js.map +1 -0
- package/dist/workspace/schema.d.ts +15 -0
- package/dist/workspace/schema.d.ts.map +1 -0
- package/dist/workspace/schema.js +33 -0
- package/dist/workspace/schema.js.map +1 -0
- package/dist/workspace/selection-screen.d.ts +4 -0
- package/dist/workspace/selection-screen.d.ts.map +1 -0
- package/dist/workspace/selection-screen.js +216 -0
- package/dist/workspace/selection-screen.js.map +1 -0
- package/dist/workspace/selection-service.d.ts +16 -0
- package/dist/workspace/selection-service.d.ts.map +1 -0
- package/dist/workspace/selection-service.js +65 -0
- package/dist/workspace/selection-service.js.map +1 -0
- package/node_modules/@xterm/headless/README.md +31 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js +2 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.js.map +1 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
- package/node_modules/@xterm/headless/lib-headless/xterm-headless.mjs.map +7 -0
- package/node_modules/@xterm/headless/package.json +29 -0
- package/node_modules/@xterm/headless/typings/xterm-headless.d.ts +1385 -0
- package/package.json +75 -0
|
@@ -0,0 +1,1858 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { DaemonLifecycle } from "../daemon/lifecycle.js";
|
|
3
|
+
import { LocalClientViewRegistry } from "../daemon/views.js";
|
|
4
|
+
import { DurableSyncJournal } from "../sync/journal.js";
|
|
5
|
+
import { LocalSyncSupervisor, SyncSupervisorRegistry, } from "../sync/supervisor.js";
|
|
6
|
+
import { TERMINAL_PROTOCOL, TERMINAL_LOCAL_ACTION_KINDS, TerminalFrameDecoder, TerminalProtocolError, assertTerminalFrameLegal, decodeTerminalControl, decodeTerminalFrame, encodeTerminalControl, encodeTerminalFrame, isLocalActionFrameType, negotiateTerminalLocalActions, } from "../terminal/codec.js";
|
|
7
|
+
import { admitCapability, writerTransferCapability } from "./capability-gate.js";
|
|
8
|
+
import { RuntimeBoundaryError, runtimeFailure, terminalHistoryGap } from "./errors.js";
|
|
9
|
+
import { assertReadyPayloadMatches, assertRemoteAgentSessionEvidence, validateTerminalGrant, } from "./terminal-transport.js";
|
|
10
|
+
const MAX_RUNTIME_EVIDENCE_TTL_MS = 5 * 60_000;
|
|
11
|
+
export class CunaRuntimeBoundary {
|
|
12
|
+
#options;
|
|
13
|
+
#clock;
|
|
14
|
+
#idempotencyKey;
|
|
15
|
+
#daemon;
|
|
16
|
+
#mode;
|
|
17
|
+
#views = new LocalClientViewRegistry();
|
|
18
|
+
#syncRegistry = new SyncSupervisorRegistry();
|
|
19
|
+
#terminals = new Map();
|
|
20
|
+
#pendingConnectionRequests = new Map();
|
|
21
|
+
#pendingTerminalTabs = new Set();
|
|
22
|
+
#pendingAgentSessions = new Set();
|
|
23
|
+
#pendingAttaches = new Map();
|
|
24
|
+
#pendingReconnects = new Map();
|
|
25
|
+
#pendingSyncOpens = new Map();
|
|
26
|
+
#syncHandles = new Map();
|
|
27
|
+
#activeTabId;
|
|
28
|
+
#startupEvidenceExpiresAt = 0;
|
|
29
|
+
#foreground;
|
|
30
|
+
#closed = false;
|
|
31
|
+
#shutdownComplete = false;
|
|
32
|
+
#shutdownFlight;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
assertIdentifier(options.terminalCapabilityId, "terminal capability ID");
|
|
35
|
+
assertIdentifier(options.clientInstanceId, "client instance ID");
|
|
36
|
+
if (options.allowedCunaOrigins.length === 0) {
|
|
37
|
+
throw runtimeFailure("grant_invalid", "At least one exact Cuna HTTPS origin is required.");
|
|
38
|
+
}
|
|
39
|
+
this.#options = Object.freeze({ ...options, allowedCunaOrigins: Object.freeze([...options.allowedCunaOrigins]) });
|
|
40
|
+
this.#clock = options.clock ?? Date.now;
|
|
41
|
+
this.#idempotencyKey = options.idempotencyKey ?? randomUUID;
|
|
42
|
+
const mode = options.mode ?? "daemon";
|
|
43
|
+
if (mode !== "daemon" && mode !== "foreground") {
|
|
44
|
+
throw new RangeError("Runtime boundary mode must be daemon or foreground.");
|
|
45
|
+
}
|
|
46
|
+
this.#mode = mode;
|
|
47
|
+
this.#daemon = new DaemonLifecycle(this.#clock());
|
|
48
|
+
this.#foreground = Object.freeze({ state: "absent", reason: "not_started", updatedAt: this.#clock() });
|
|
49
|
+
}
|
|
50
|
+
get daemon() {
|
|
51
|
+
return this.#daemon.snapshot();
|
|
52
|
+
}
|
|
53
|
+
get activeTabId() {
|
|
54
|
+
return this.#activeTabId;
|
|
55
|
+
}
|
|
56
|
+
get mode() {
|
|
57
|
+
return this.#mode;
|
|
58
|
+
}
|
|
59
|
+
get foreground() {
|
|
60
|
+
return this.#foreground;
|
|
61
|
+
}
|
|
62
|
+
start(evidence) {
|
|
63
|
+
this.#assertOpen();
|
|
64
|
+
if (this.#mode !== "daemon") {
|
|
65
|
+
throw runtimeFailure("session_conflict", "Foreground runtime mode cannot claim daemon readiness.");
|
|
66
|
+
}
|
|
67
|
+
if (this.#daemon.snapshot().state !== "absent" && this.#daemon.snapshot().state !== "stopped") {
|
|
68
|
+
throw runtimeFailure("session_conflict", "The local runtime is already started.");
|
|
69
|
+
}
|
|
70
|
+
const now = this.#clock();
|
|
71
|
+
this.#daemon.transition("starting", "runtime_start_requested", now);
|
|
72
|
+
if (evidence.endpointOwnership !== "verified" ||
|
|
73
|
+
evidence.durableState !== "verified" ||
|
|
74
|
+
evidence.source.length === 0 ||
|
|
75
|
+
!Number.isFinite(evidence.observedAt) ||
|
|
76
|
+
!Number.isFinite(evidence.expiresAt) ||
|
|
77
|
+
evidence.observedAt > now ||
|
|
78
|
+
evidence.expiresAt < evidence.observedAt ||
|
|
79
|
+
evidence.expiresAt - evidence.observedAt > MAX_RUNTIME_EVIDENCE_TTL_MS ||
|
|
80
|
+
evidence.expiresAt <= now) {
|
|
81
|
+
this.#daemon.transition("recovery_required", "local_runtime_evidence_unproven", this.#clock());
|
|
82
|
+
throw runtimeFailure("remote_state_unproven", "The local runtime endpoint or durable state is not verified.");
|
|
83
|
+
}
|
|
84
|
+
this.#startupEvidenceExpiresAt = evidence.expiresAt;
|
|
85
|
+
return this.#daemon.transition("ready", "local_runtime_verified", this.#clock());
|
|
86
|
+
}
|
|
87
|
+
startForeground() {
|
|
88
|
+
this.#assertOpen();
|
|
89
|
+
if (this.#mode !== "foreground") {
|
|
90
|
+
throw runtimeFailure("session_conflict", "Daemon runtime mode cannot claim foreground readiness.");
|
|
91
|
+
}
|
|
92
|
+
if (this.#foreground.state !== "absent") {
|
|
93
|
+
throw runtimeFailure("session_conflict", "The foreground runtime is already started.");
|
|
94
|
+
}
|
|
95
|
+
this.#foreground = Object.freeze({
|
|
96
|
+
state: "ready",
|
|
97
|
+
reason: "foreground_process_owns_runtime",
|
|
98
|
+
updatedAt: this.#clock(),
|
|
99
|
+
});
|
|
100
|
+
return this.#foreground;
|
|
101
|
+
}
|
|
102
|
+
async attach(input) {
|
|
103
|
+
this.#assertReady();
|
|
104
|
+
assertIdentifier(input.tabId, "tab ID");
|
|
105
|
+
assertIdentifier(input.agentSessionId, "AgentSession ID");
|
|
106
|
+
assertDimensions(input.columns, input.rows);
|
|
107
|
+
if (this.#terminals.has(input.tabId) || this.#pendingTerminalTabs.has(input.tabId)) {
|
|
108
|
+
throw runtimeFailure("session_conflict", "The terminal tab already exists.");
|
|
109
|
+
}
|
|
110
|
+
if (this.#pendingAgentSessions.has(input.agentSessionId) ||
|
|
111
|
+
[...this.#terminals.values()].some((entry) => entry.observation.agentSessionId === input.agentSessionId && entry.state !== "closed")) {
|
|
112
|
+
throw runtimeFailure("session_conflict", "The AgentSession is already attached by this runtime.");
|
|
113
|
+
}
|
|
114
|
+
this.#pendingTerminalTabs.add(input.tabId);
|
|
115
|
+
this.#pendingAgentSessions.add(input.agentSessionId);
|
|
116
|
+
const attachAbort = new AbortController();
|
|
117
|
+
let removeInputAbort = () => undefined;
|
|
118
|
+
if (input.signal !== undefined) {
|
|
119
|
+
const forwardAbort = () => attachAbort.abort(input.signal?.reason);
|
|
120
|
+
if (input.signal.aborted)
|
|
121
|
+
forwardAbort();
|
|
122
|
+
else {
|
|
123
|
+
input.signal.addEventListener("abort", forwardAbort, { once: true });
|
|
124
|
+
removeInputAbort = () => input.signal?.removeEventListener("abort", forwardAbort);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let settleAttach = (_failure) => undefined;
|
|
128
|
+
const attachCompletion = new Promise((resolve) => { settleAttach = resolve; });
|
|
129
|
+
this.#pendingAttaches.set(input.tabId, {
|
|
130
|
+
abort: attachAbort,
|
|
131
|
+
completion: attachCompletion,
|
|
132
|
+
settle: settleAttach,
|
|
133
|
+
removeInputAbort,
|
|
134
|
+
});
|
|
135
|
+
let connection;
|
|
136
|
+
let entry;
|
|
137
|
+
let completionFailure;
|
|
138
|
+
try {
|
|
139
|
+
throwIfAborted(attachAbort.signal, "Terminal attachment was cancelled.");
|
|
140
|
+
await this.#cancelConnectionRequests(input.agentSessionId);
|
|
141
|
+
const admitted = await this.#admitRemoteTerminal(input.agentSessionId, attachAbort.signal);
|
|
142
|
+
if (input.expectedAdmission !== undefined) {
|
|
143
|
+
this.#assertAttachmentAdmissionContinuity(input.expectedAdmission, admitted, "preflight");
|
|
144
|
+
}
|
|
145
|
+
this.#assertOpen();
|
|
146
|
+
throwIfAborted(attachAbort.signal, "Terminal attachment was cancelled.");
|
|
147
|
+
const grant = await this.#createGrant(admitted.observation, admitted.capability, undefined, undefined, attachAbort.signal);
|
|
148
|
+
this.#assertOpen();
|
|
149
|
+
throwIfAborted(attachAbort.signal, "Terminal attachment was cancelled.");
|
|
150
|
+
const revalidated = await this.#admitRemoteTerminal(input.agentSessionId, attachAbort.signal);
|
|
151
|
+
this.#assertAttachmentAdmissionContinuity(admitted, revalidated, "post_grant");
|
|
152
|
+
if (input.expectedAdmission !== undefined) {
|
|
153
|
+
this.#assertAttachmentAdmissionContinuity(input.expectedAdmission, revalidated, "preflight");
|
|
154
|
+
}
|
|
155
|
+
this.#assertOpen();
|
|
156
|
+
throwIfAborted(attachAbort.signal, "Terminal attachment was cancelled.");
|
|
157
|
+
connection = await this.#options.terminalConnector.connect({
|
|
158
|
+
...(this.#options.canonicalTerminalViews === true ? { terminalViewProtocol: "cuna.terminal-view.v1" } : {}),
|
|
159
|
+
url: grant.connectUrl,
|
|
160
|
+
token: grant.connectToken,
|
|
161
|
+
protocol: TERMINAL_PROTOCOL,
|
|
162
|
+
signal: attachAbort.signal,
|
|
163
|
+
});
|
|
164
|
+
this.#assertOpen();
|
|
165
|
+
throwIfAborted(attachAbort.signal, "Terminal attachment was cancelled.");
|
|
166
|
+
if (connection.connectionId !== grant.terminalSessionId) {
|
|
167
|
+
throw runtimeFailure("grant_scope_mismatch", "The terminal transport accepted a different Cuna terminal session.");
|
|
168
|
+
}
|
|
169
|
+
entry = {
|
|
170
|
+
capabilitySnapshot: revalidated.capabilitySnapshot,
|
|
171
|
+
capabilityReadRevision: 0,
|
|
172
|
+
tabId: input.tabId,
|
|
173
|
+
viewId: `pending:${grant.terminalSessionId}`,
|
|
174
|
+
observation: revalidated.observation,
|
|
175
|
+
state: "attaching",
|
|
176
|
+
connection,
|
|
177
|
+
decoder: new TerminalFrameDecoder(),
|
|
178
|
+
fencingGeneration: 0,
|
|
179
|
+
capabilities: grant.capabilities,
|
|
180
|
+
resizeCapability: "initial_resize_only",
|
|
181
|
+
wireSequence: 0n,
|
|
182
|
+
inputSequence: 0n,
|
|
183
|
+
acknowledgedInputSequence: 0n,
|
|
184
|
+
inputContinuity: "none",
|
|
185
|
+
pendingInputSequences: new Set(),
|
|
186
|
+
historicalInputUncertainty: false,
|
|
187
|
+
retiredInputSequence: 0n,
|
|
188
|
+
outputSequence: 0n,
|
|
189
|
+
outputContinuity: "unknown",
|
|
190
|
+
lastHeartbeatAt: this.#clock(),
|
|
191
|
+
resumeHandle: grant.resumeHandle,
|
|
192
|
+
localActionsNegotiated: false,
|
|
193
|
+
localActionAcceptance: undefined,
|
|
194
|
+
accessMode: "observer",
|
|
195
|
+
writerEpoch: 0,
|
|
196
|
+
writerClientInstanceId: null,
|
|
197
|
+
wireWriterNotice: undefined,
|
|
198
|
+
geometry: null,
|
|
199
|
+
heldSeat: false,
|
|
200
|
+
sendTail: Promise.resolve(),
|
|
201
|
+
connectionRevision: 1,
|
|
202
|
+
heartbeatSequence: 0n,
|
|
203
|
+
heartbeatSendPending: false,
|
|
204
|
+
outputAbort: new AbortController(),
|
|
205
|
+
};
|
|
206
|
+
const iterator = connection.receive()[Symbol.asyncIterator]();
|
|
207
|
+
const ready = await this.#awaitReady(entry, iterator, attachAbort.signal);
|
|
208
|
+
if (ready.payload.terminalViewProtocol !== undefined) {
|
|
209
|
+
if (this.#options.canonicalTerminalViews !== true)
|
|
210
|
+
throw runtimeFailure("terminal_protocol_error", "Unrequested terminal view protocol.");
|
|
211
|
+
entry.terminalView = { viewId: null, ready: false };
|
|
212
|
+
}
|
|
213
|
+
this.#assertOpen();
|
|
214
|
+
entry.lastHeartbeatAt = this.#clock();
|
|
215
|
+
entry.heartbeatSequence = 0n;
|
|
216
|
+
entry.fencingGeneration = ready.payload.fencingGeneration;
|
|
217
|
+
entry.resizeCapability = ready.payload.resizeCapability;
|
|
218
|
+
entry.accessMode = ready.payload.accessMode;
|
|
219
|
+
entry.writerEpoch = ready.payload.writerEpoch;
|
|
220
|
+
entry.writerClientInstanceId = ready.payload.accessMode === "writer" ? this.#options.clientInstanceId : null;
|
|
221
|
+
entry.wireWriterNotice = ready.payload.accessMode === "writer" ? Object.freeze({
|
|
222
|
+
writerEpoch: ready.payload.writerEpoch, writerClientInstanceId: this.#options.clientInstanceId, accessMode: "writer",
|
|
223
|
+
}) : undefined;
|
|
224
|
+
if (entry.writerTransfer !== undefined && ready.payload.writerEpoch > entry.writerTransfer.expectedWriterEpoch) {
|
|
225
|
+
delete entry.writerTransfer;
|
|
226
|
+
}
|
|
227
|
+
entry.heldSeat = ready.payload.accessMode === "writer";
|
|
228
|
+
entry.localActionAcceptance = this.#localActionAcceptance(ready.payload.localActionProtocol, entry.observation.agentSessionId);
|
|
229
|
+
entry.viewId = viewId(input.tabId, ready.payload.fencingGeneration);
|
|
230
|
+
this.#views.open({
|
|
231
|
+
viewId: entry.viewId,
|
|
232
|
+
binding: {
|
|
233
|
+
userId: revalidated.observation.userId,
|
|
234
|
+
machineId: revalidated.observation.machineId,
|
|
235
|
+
agentSessionId: revalidated.observation.agentSessionId,
|
|
236
|
+
processEpoch: revalidated.observation.processEpoch,
|
|
237
|
+
fencingGeneration: ready.payload.fencingGeneration,
|
|
238
|
+
},
|
|
239
|
+
state: "active",
|
|
240
|
+
columns: input.columns,
|
|
241
|
+
rows: input.rows,
|
|
242
|
+
});
|
|
243
|
+
entry.state = "active";
|
|
244
|
+
entry.outputContinuity = entry.terminalView === undefined ? "complete" : "unknown";
|
|
245
|
+
this.#terminals.set(input.tabId, entry);
|
|
246
|
+
this.#activeTabId ??= input.tabId;
|
|
247
|
+
// READY proves that the PTY exists; it does not prove that its default
|
|
248
|
+
// geometry matches this host. Establish the admitted dimensions before
|
|
249
|
+
// requesting retained output: provider TUIs may have rendered that
|
|
250
|
+
// output for the old geometry, and replay-before-resize makes a wide
|
|
251
|
+
// local terminal deterministically display the stale wrapping.
|
|
252
|
+
this.#requireGrantCapability(entry, "live_resize");
|
|
253
|
+
if (entry.resizeCapability !== "live") {
|
|
254
|
+
throw runtimeFailure("capability_unsupported", "This terminal cannot establish its initial dimensions.");
|
|
255
|
+
}
|
|
256
|
+
// Only the writing seat sets the PTY's geometry. An observer renders
|
|
257
|
+
// whatever size the writer chose; a RESIZE from it would close the
|
|
258
|
+
// attachment at the gateway (observer_control_rejected).
|
|
259
|
+
if (entry.accessMode === "writer") {
|
|
260
|
+
entry.wireSequence += 1n;
|
|
261
|
+
await connection.send(encodeTerminalControl("resize", entry.wireSequence, {
|
|
262
|
+
columns: input.columns,
|
|
263
|
+
rows: input.rows,
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
// The supervisor starts and drains the provider PTY before a terminal
|
|
267
|
+
// client necessarily attaches. READY proves the live binding but does
|
|
268
|
+
// not include retained output, so request replay only after the ordered
|
|
269
|
+
// resize has reached the same fenced attachment generation.
|
|
270
|
+
entry.wireSequence += 1n;
|
|
271
|
+
entry.replayBoundarySequence = entry.wireSequence;
|
|
272
|
+
entry.replayBoundaryObserved = false;
|
|
273
|
+
await connection.send(encodeTerminalControl("resume", entry.wireSequence, {
|
|
274
|
+
resumeHandle: entry.resumeHandle,
|
|
275
|
+
afterOutputSequence: entry.outputSequence.toString(),
|
|
276
|
+
...(entry.terminalView === undefined ? {} : { terminalViewProtocol: { name: "cuna.terminal-view.v1", operation: "new" } }),
|
|
277
|
+
...(entry.localActionAcceptance === undefined ? {} : { localActionProtocol: entry.localActionAcceptance }),
|
|
278
|
+
}));
|
|
279
|
+
entry.localActionsNegotiated = entry.localActionAcceptance !== undefined;
|
|
280
|
+
await this.#options.onTerminalReady?.(snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock()));
|
|
281
|
+
this.#armTerminalViewDeadline(entry);
|
|
282
|
+
this.#assertOpen();
|
|
283
|
+
for (const frame of ready.bufferedFrames)
|
|
284
|
+
await this.#handleAttachedFrame(entry, frame);
|
|
285
|
+
this.#assertOpen();
|
|
286
|
+
this.#scheduleHeartbeatWatchdog(entry, connection, entry.connectionRevision);
|
|
287
|
+
this.#publish(entry);
|
|
288
|
+
entry.pump = this.#pump(entry, connection, entry.connectionRevision, iterator);
|
|
289
|
+
this.#forgetConnectionRequests(input.agentSessionId);
|
|
290
|
+
return snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock());
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
let reportedError = error;
|
|
294
|
+
if (entry !== undefined && !this.#closed && entry.state !== "closed" && entry.state !== "detached") {
|
|
295
|
+
entry.state = "failed";
|
|
296
|
+
entry.outputContinuity = "unknown";
|
|
297
|
+
entry.reason = "terminal_attach_composition_failed";
|
|
298
|
+
try {
|
|
299
|
+
this.#views.detach(entry.viewId);
|
|
300
|
+
}
|
|
301
|
+
catch { /* the fenced view may not have opened */ }
|
|
302
|
+
this.#publish(entry);
|
|
303
|
+
if (this.#activeTabId === entry.tabId)
|
|
304
|
+
this.#activeTabId = this.#nextActiveTab(entry.tabId);
|
|
305
|
+
this.#terminals.delete(input.tabId);
|
|
306
|
+
}
|
|
307
|
+
if (connection !== undefined) {
|
|
308
|
+
try {
|
|
309
|
+
await connection.close({ code: 1008, reason: "cuna_attach_rejected" });
|
|
310
|
+
}
|
|
311
|
+
catch (cleanupError) {
|
|
312
|
+
completionFailure = cleanupError;
|
|
313
|
+
reportedError = new AggregateError([error, cleanupError], "Terminal attachment failed and its transport cleanup was incomplete.");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
await this.#cancelConnectionRequests(input.agentSessionId);
|
|
318
|
+
}
|
|
319
|
+
catch (cleanupError) {
|
|
320
|
+
completionFailure = cleanupError;
|
|
321
|
+
reportedError = new AggregateError([reportedError, cleanupError], "Terminal attachment failed and issuance cancellation is unconfirmed.");
|
|
322
|
+
}
|
|
323
|
+
throw reportedError;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
const pending = this.#pendingAttaches.get(input.tabId);
|
|
327
|
+
pending?.removeInputAbort();
|
|
328
|
+
pending?.settle(completionFailure);
|
|
329
|
+
this.#pendingAttaches.delete(input.tabId);
|
|
330
|
+
this.#pendingTerminalTabs.delete(input.tabId);
|
|
331
|
+
this.#pendingAgentSessions.delete(input.agentSessionId);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
switchActive(tabId) {
|
|
335
|
+
this.#assertReady();
|
|
336
|
+
const entry = this.#requireTerminal(tabId);
|
|
337
|
+
if (entry.state === "closed" || entry.state === "detached" || entry.state === "failed") {
|
|
338
|
+
throw runtimeFailure("terminal_disconnected", "The selected terminal tab is not attachable.");
|
|
339
|
+
}
|
|
340
|
+
this.#activeTabId = tabId;
|
|
341
|
+
return snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock());
|
|
342
|
+
}
|
|
343
|
+
refreshTerminalLiveness(tabId = this.#activeTabId) {
|
|
344
|
+
this.#assertReady();
|
|
345
|
+
const entry = this.#requireTerminal(tabId ?? "");
|
|
346
|
+
this.#assertHeartbeatFresh(entry);
|
|
347
|
+
return snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock());
|
|
348
|
+
}
|
|
349
|
+
async sendInput(bytes, tabId = this.#activeTabId, expectedBinding) {
|
|
350
|
+
this.#assertReady();
|
|
351
|
+
const entry = this.#requireActiveTerminal(tabId);
|
|
352
|
+
if (entry.accessMode !== "writer") {
|
|
353
|
+
// An observer's keystrokes never reach the wire: the gateway would
|
|
354
|
+
// close the attachment on the first one. Refuse here, by name, and
|
|
355
|
+
// leave the attachment observing.
|
|
356
|
+
throw runtimeFailure("terminal_observer", "This attachment observes the terminal; input is disabled.");
|
|
357
|
+
}
|
|
358
|
+
if (expectedBinding !== undefined && !sameEntryBinding(entry, expectedBinding)) {
|
|
359
|
+
throw runtimeFailure("grant_scope_mismatch", "Terminal input targets a replaced attachment generation.");
|
|
360
|
+
}
|
|
361
|
+
await this.#sendTerminalBytes(entry, bytes);
|
|
362
|
+
}
|
|
363
|
+
async sendTerminalResponse(response) {
|
|
364
|
+
this.#assertReady();
|
|
365
|
+
const entry = this.#requireActiveTerminal(response.tabId);
|
|
366
|
+
if (response.binding.userId !== entry.observation.userId ||
|
|
367
|
+
response.binding.machineId !== entry.observation.machineId ||
|
|
368
|
+
response.binding.agentSessionId !== entry.observation.agentSessionId ||
|
|
369
|
+
response.binding.processEpoch !== entry.observation.processEpoch ||
|
|
370
|
+
response.binding.fencingGeneration !== entry.fencingGeneration) {
|
|
371
|
+
throw runtimeFailure("grant_scope_mismatch", "The terminal response targets another attachment authority.");
|
|
372
|
+
}
|
|
373
|
+
// The writer's headless xterm is the one that answers the PTY's queries
|
|
374
|
+
// (DA1, DSR, ...). An observer mirrors the same bytes and would answer
|
|
375
|
+
// them too, onto a PTY it does not hold; the gateway closes an observer's
|
|
376
|
+
// attachment for that. The answer is the terminal's, not the user's, so
|
|
377
|
+
// there is nothing to report: drop it.
|
|
378
|
+
if (entry.accessMode !== "writer")
|
|
379
|
+
return;
|
|
380
|
+
await this.#sendTerminalBytes(entry, response.bytes);
|
|
381
|
+
}
|
|
382
|
+
async #sendTerminalBytes(entry, bytes) {
|
|
383
|
+
if (entry.terminalView !== undefined && !entry.terminalView.ready)
|
|
384
|
+
throw runtimeFailure("terminal_protocol_error", "The current terminal view is not ready for input.");
|
|
385
|
+
const payload = bytes.slice();
|
|
386
|
+
await this.#enqueueTerminalSend(entry, async (authority) => {
|
|
387
|
+
if (entry.terminalView !== undefined && !entry.terminalView.ready)
|
|
388
|
+
throw runtimeFailure("terminal_protocol_error", "The current terminal view is not ready for input.");
|
|
389
|
+
if (entry.pendingInputSequences.size >= 4_096) {
|
|
390
|
+
entry.connectionRevision += 1;
|
|
391
|
+
entry.outputAbort.abort(runtimeFailure("terminal_disconnected", "Terminal input acknowledgement window was exhausted."));
|
|
392
|
+
entry.state = "interrupted";
|
|
393
|
+
retireInputAcceptance(entry);
|
|
394
|
+
entry.inputContinuity = "uncertain";
|
|
395
|
+
entry.outputContinuity = "unknown";
|
|
396
|
+
entry.reason = "input_ack_window_exhausted";
|
|
397
|
+
this.#publish(entry);
|
|
398
|
+
void authority.connection.close({ code: 1001, reason: "cuna_input_ack_window_exhausted" }).catch(() => undefined);
|
|
399
|
+
throw runtimeFailure("terminal_disconnected", "Terminal input acknowledgements exceeded the bounded uncertainty window.");
|
|
400
|
+
}
|
|
401
|
+
this.#views.routeInput(authority.viewId, authority.fencingGeneration);
|
|
402
|
+
const inputSequence = entry.wireSequence + 1n;
|
|
403
|
+
const frame = encodeTerminalFrame({
|
|
404
|
+
type: "input",
|
|
405
|
+
critical: true,
|
|
406
|
+
sequence: inputSequence,
|
|
407
|
+
payload,
|
|
408
|
+
});
|
|
409
|
+
entry.wireSequence = inputSequence;
|
|
410
|
+
entry.inputSequence = inputSequence;
|
|
411
|
+
entry.pendingInputSequences.add(entry.inputSequence);
|
|
412
|
+
entry.inputContinuity = "uncertain";
|
|
413
|
+
await authority.connection.send(frame);
|
|
414
|
+
this.#publish(entry);
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
async resize(columns, rows, tabId = this.#activeTabId) {
|
|
418
|
+
const entry = this.#requireActiveTerminal(tabId);
|
|
419
|
+
this.#requireWriterSeat(entry);
|
|
420
|
+
this.#requireGrantCapability(entry, "live_resize");
|
|
421
|
+
if (entry.resizeCapability !== "live") {
|
|
422
|
+
throw runtimeFailure("capability_unsupported", "This terminal supports only its initial dimensions.");
|
|
423
|
+
}
|
|
424
|
+
await this.#enqueueTerminalSend(entry, async (authority) => {
|
|
425
|
+
this.#views.resize(authority.viewId, columns, rows);
|
|
426
|
+
entry.wireSequence += 1n;
|
|
427
|
+
await authority.connection.send(encodeTerminalControl("resize", entry.wireSequence, { columns, rows }));
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
async signal(signal, tabId = this.#activeTabId) {
|
|
431
|
+
const entry = this.#requireActiveTerminal(tabId);
|
|
432
|
+
this.#requireWriterSeat(entry);
|
|
433
|
+
this.#requireGrantCapability(entry, "signals");
|
|
434
|
+
await this.#enqueueTerminalSend(entry, async (authority) => {
|
|
435
|
+
this.#views.routeInput(authority.viewId, authority.fencingGeneration);
|
|
436
|
+
entry.wireSequence += 1n;
|
|
437
|
+
await authority.connection.send(encodeTerminalControl("signal", entry.wireSequence, { signal }));
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
async sendLocalActionControl(type, payload, tabId = this.#activeTabId) {
|
|
441
|
+
const entry = this.#requireActiveTerminal(tabId);
|
|
442
|
+
if (!entry.localActionsNegotiated) {
|
|
443
|
+
throw runtimeFailure("capability_unsupported", "This attachment did not negotiate local actions.");
|
|
444
|
+
}
|
|
445
|
+
assertTerminalFrameLegal("attached", "client_to_server", type, true);
|
|
446
|
+
await this.#enqueueTerminalSend(entry, async (authority) => {
|
|
447
|
+
const sequence = entry.wireSequence + 1n;
|
|
448
|
+
const wire = encodeTerminalControl(type, sequence, payload);
|
|
449
|
+
const decoded = decodeTerminalFrame(wire);
|
|
450
|
+
if (decoded === undefined)
|
|
451
|
+
throw runtimeFailure("terminal_protocol_error", "The local action frame could not be decoded.");
|
|
452
|
+
const validated = decodeTerminalControl(decoded);
|
|
453
|
+
if (type === "local_action_result") {
|
|
454
|
+
if (validated.message !== "outcome") {
|
|
455
|
+
throw runtimeFailure("terminal_protocol_error", "The CLI may send only local action outcomes.");
|
|
456
|
+
}
|
|
457
|
+
const result = validated.result;
|
|
458
|
+
const identity = result.identity;
|
|
459
|
+
if (identity.userId !== entry.observation.userId ||
|
|
460
|
+
identity.machineId !== entry.observation.machineId ||
|
|
461
|
+
identity.workspaceBindingId !== entry.observation.workspaceBindingId ||
|
|
462
|
+
identity.workspaceBindingGeneration !== entry.observation.workspaceBindingGeneration ||
|
|
463
|
+
identity.agentSessionId !== entry.observation.agentSessionId ||
|
|
464
|
+
identity.processEpoch !== entry.observation.processEpoch ||
|
|
465
|
+
identity.fencingGeneration !== entry.fencingGeneration ||
|
|
466
|
+
!entry.localActionAcceptance?.acceptedKinds.includes(result.kind))
|
|
467
|
+
throw runtimeFailure("grant_scope_mismatch", "The local action outcome targets another attachment authority.");
|
|
468
|
+
}
|
|
469
|
+
if (entry.connection !== authority.connection ||
|
|
470
|
+
entry.connectionRevision !== authority.connectionRevision ||
|
|
471
|
+
entry.fencingGeneration !== authority.fencingGeneration)
|
|
472
|
+
throw runtimeFailure("terminal_disconnected", "The local action attachment authority changed before send.");
|
|
473
|
+
entry.wireSequence = sequence;
|
|
474
|
+
await authority.connection.send(wire);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
async #enqueueTerminalSend(entry, operation) {
|
|
478
|
+
const authority = Object.freeze({
|
|
479
|
+
connection: entry.connection,
|
|
480
|
+
connectionRevision: entry.connectionRevision,
|
|
481
|
+
viewId: entry.viewId,
|
|
482
|
+
fencingGeneration: entry.fencingGeneration,
|
|
483
|
+
});
|
|
484
|
+
const queued = entry.sendTail.then(async () => {
|
|
485
|
+
if (entry.state !== "active" ||
|
|
486
|
+
entry.connection !== authority.connection ||
|
|
487
|
+
entry.connectionRevision !== authority.connectionRevision ||
|
|
488
|
+
entry.viewId !== authority.viewId ||
|
|
489
|
+
entry.fencingGeneration !== authority.fencingGeneration) {
|
|
490
|
+
throw runtimeFailure("terminal_disconnected", "Queued terminal input was fenced by a newer attachment.", { retryable: true });
|
|
491
|
+
}
|
|
492
|
+
await operation(authority);
|
|
493
|
+
});
|
|
494
|
+
entry.sendTail = queued.then(() => undefined, () => undefined);
|
|
495
|
+
await queued;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Ask for the terminal's writing seat. The durable transfer is confirmed by
|
|
499
|
+
* the API; the seat this attachment holds changes only when the server's
|
|
500
|
+
* writer_epoch notice arrives, because the gateway fences input on that
|
|
501
|
+
* notice and a locally-assumed promotion would be closed on its first byte.
|
|
502
|
+
*/
|
|
503
|
+
async takeWriter(input) {
|
|
504
|
+
this.#assertReady();
|
|
505
|
+
const entry = this.#requireActiveTerminal(input.tabId);
|
|
506
|
+
if (entry.accessMode === "writer")
|
|
507
|
+
return snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock());
|
|
508
|
+
const operation = entry.writerTransfer ?? { operationId: randomUUID(), expectedWriterEpoch: entry.writerEpoch };
|
|
509
|
+
entry.writerTransfer = operation;
|
|
510
|
+
if (operation.inFlight !== undefined)
|
|
511
|
+
return operation.inFlight;
|
|
512
|
+
operation.inFlight = Promise.resolve().then(async () => {
|
|
513
|
+
try {
|
|
514
|
+
const revision = entry.connectionRevision;
|
|
515
|
+
const capabilityReadRevision = ++entry.capabilityReadRevision;
|
|
516
|
+
const evidence = await this.#options.controlPlane.discoverCapabilities("agent_session", entry.observation.agentSessionId, input.signal);
|
|
517
|
+
this.#assertReady();
|
|
518
|
+
if (entry.state !== "active" || entry.connectionRevision !== revision)
|
|
519
|
+
throw runtimeFailure("terminal_disconnected", "The terminal changed while writer capability was checked.");
|
|
520
|
+
if (entry.writerTransfer !== operation) {
|
|
521
|
+
if (entry.accessMode === "writer")
|
|
522
|
+
return snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock());
|
|
523
|
+
throw runtimeFailure("session_conflict", "A writer notice superseded this control request. Read the current terminal state.");
|
|
524
|
+
}
|
|
525
|
+
if (entry.capabilityReadRevision !== capabilityReadRevision)
|
|
526
|
+
throw runtimeFailure("capability_unknown", "A newer capability observation superseded this read.");
|
|
527
|
+
entry.capabilitySnapshot = evidence;
|
|
528
|
+
this.#publish(entry);
|
|
529
|
+
const capabilityEvidence = admitCapability(evidence, { id: "terminal_writers.transfer", scope: "agent_session", subjectId: entry.observation.agentSessionId, interaction: "native" }, this.#clock());
|
|
530
|
+
if (input.signal !== undefined)
|
|
531
|
+
throwIfAborted(input.signal, "Writer transfer was cancelled before dispatch.");
|
|
532
|
+
const state = await this.#options.controlPlane.transferTerminalWriter({
|
|
533
|
+
capabilityEvidence,
|
|
534
|
+
agentSessionId: entry.observation.agentSessionId,
|
|
535
|
+
clientInstanceId: this.#options.clientInstanceId,
|
|
536
|
+
expectedWriterEpoch: operation.expectedWriterEpoch,
|
|
537
|
+
operationId: operation.operationId,
|
|
538
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
539
|
+
});
|
|
540
|
+
if (state.agentSessionId !== entry.observation.agentSessionId ||
|
|
541
|
+
state.processEpoch !== entry.observation.processEpoch ||
|
|
542
|
+
state.writerClientInstanceId !== this.#options.clientInstanceId ||
|
|
543
|
+
state.operationId !== operation.operationId || state.operationState !== "committed" ||
|
|
544
|
+
state.writerEpoch !== operation.expectedWriterEpoch + 1) {
|
|
545
|
+
throw runtimeFailure("grant_scope_mismatch", "The writer transfer answered for another terminal, operation, or epoch.");
|
|
546
|
+
}
|
|
547
|
+
// A later server notice may already have superseded this HTTP reply.
|
|
548
|
+
if (entry.writerTransfer === operation && state.writerEpoch >= entry.writerEpoch) {
|
|
549
|
+
if (state.writerEpoch !== entry.writerEpoch) {
|
|
550
|
+
retireInputAcceptance(entry);
|
|
551
|
+
entry.geometry = null;
|
|
552
|
+
}
|
|
553
|
+
entry.writerEpoch = state.writerEpoch;
|
|
554
|
+
entry.writerClientInstanceId = state.writerClientInstanceId;
|
|
555
|
+
if (state.transferPending)
|
|
556
|
+
entry.reason = "writer_transfer_pending";
|
|
557
|
+
this.#publish(entry);
|
|
558
|
+
}
|
|
559
|
+
return snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock());
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
const reason = typeof error === "object" && error !== null
|
|
563
|
+
? error.details?.reason : undefined;
|
|
564
|
+
// A missing response or an in-progress operation retains its identity.
|
|
565
|
+
// A definitive refusal ends this request, without granting input rights.
|
|
566
|
+
if (entry.writerTransfer === operation && typeof reason === "string" &&
|
|
567
|
+
["terminal_writer_cancelled", "terminal_writer_stale", "terminal_writer_unheld",
|
|
568
|
+
"terminal_writer_already_held", "terminal_writer_operation_mismatch", "invalid_terminal_writer_request"].includes(reason)) {
|
|
569
|
+
delete entry.writerTransfer;
|
|
570
|
+
}
|
|
571
|
+
throw error;
|
|
572
|
+
}
|
|
573
|
+
}).finally(() => { delete operation.inFlight; });
|
|
574
|
+
return operation.inFlight;
|
|
575
|
+
}
|
|
576
|
+
async reconnect(input) {
|
|
577
|
+
this.#assertReady();
|
|
578
|
+
const entry = this.#requireTerminal(input.tabId);
|
|
579
|
+
if (entry.state !== "interrupted") {
|
|
580
|
+
throw runtimeFailure("session_conflict", "Only an interrupted terminal can reconnect.");
|
|
581
|
+
}
|
|
582
|
+
entry.state = "reconnecting";
|
|
583
|
+
entry.outputAbort.abort(runtimeFailure("terminal_disconnected", "The previous terminal attachment was interrupted."));
|
|
584
|
+
const reconnectRevision = entry.connectionRevision + 1;
|
|
585
|
+
entry.connectionRevision = reconnectRevision;
|
|
586
|
+
retireInputAcceptance(entry);
|
|
587
|
+
entry.outputContinuity = "unknown";
|
|
588
|
+
delete entry.reason;
|
|
589
|
+
this.#publish(entry);
|
|
590
|
+
const reconnectAbort = new AbortController();
|
|
591
|
+
let removeInputAbort = () => undefined;
|
|
592
|
+
if (input.signal !== undefined) {
|
|
593
|
+
const forwardAbort = () => reconnectAbort.abort(input.signal?.reason);
|
|
594
|
+
if (input.signal.aborted)
|
|
595
|
+
forwardAbort();
|
|
596
|
+
else {
|
|
597
|
+
input.signal.addEventListener("abort", forwardAbort, { once: true });
|
|
598
|
+
removeInputAbort = () => input.signal?.removeEventListener("abort", forwardAbort);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
let settleReconnect = (_failure) => undefined;
|
|
602
|
+
const reconnectCompletion = new Promise((resolve) => { settleReconnect = resolve; });
|
|
603
|
+
this.#pendingReconnects.set(input.tabId, {
|
|
604
|
+
abort: reconnectAbort,
|
|
605
|
+
completion: reconnectCompletion,
|
|
606
|
+
settle: settleReconnect,
|
|
607
|
+
removeInputAbort,
|
|
608
|
+
});
|
|
609
|
+
let connection;
|
|
610
|
+
let completionFailure;
|
|
611
|
+
try {
|
|
612
|
+
throwIfAborted(reconnectAbort.signal, "Terminal reconnection was cancelled.");
|
|
613
|
+
await this.#cancelConnectionRequests(entry.observation.agentSessionId);
|
|
614
|
+
entry.reconnectIdempotencyKey ??= this.#idempotencyKey();
|
|
615
|
+
const admitted = await this.#admitRemoteTerminal(entry.observation.agentSessionId, reconnectAbort.signal);
|
|
616
|
+
throwIfAborted(reconnectAbort.signal, "Terminal reconnection was cancelled.");
|
|
617
|
+
if (entry.connectionRevision !== reconnectRevision || entry.state !== "reconnecting" || this.#closed) {
|
|
618
|
+
throw runtimeFailure("terminal_disconnected", "Terminal reconnection was superseded during admission.");
|
|
619
|
+
}
|
|
620
|
+
if (admitted.observation.userId !== entry.observation.userId ||
|
|
621
|
+
admitted.observation.machineId !== entry.observation.machineId ||
|
|
622
|
+
admitted.observation.workspaceBindingId !== entry.observation.workspaceBindingId ||
|
|
623
|
+
admitted.observation.workspaceBindingGeneration !== entry.observation.workspaceBindingGeneration ||
|
|
624
|
+
admitted.observation.agentSessionId !== entry.observation.agentSessionId ||
|
|
625
|
+
admitted.observation.processEpoch !== entry.observation.processEpoch) {
|
|
626
|
+
entry.state = "failed";
|
|
627
|
+
entry.outputContinuity = "incomplete";
|
|
628
|
+
entry.reason = "session_authority_changed";
|
|
629
|
+
this.#publish(entry);
|
|
630
|
+
throw runtimeFailure("session_discontinuous", "The remote AgentSession authority changed; this terminal cannot be resumed.");
|
|
631
|
+
}
|
|
632
|
+
await entry.connection.close({ code: 1001, reason: "cuna_reconnect" });
|
|
633
|
+
throwIfAborted(reconnectAbort.signal, "Terminal reconnection was cancelled.");
|
|
634
|
+
const grant = await this.#createGrant(admitted.observation, admitted.capability, undefined, entry.reconnectIdempotencyKey, reconnectAbort.signal,
|
|
635
|
+
// Reclaim the seat the durable row names for this client, at the epoch
|
|
636
|
+
// it last saw: a confirmed transfer (takeWriter) names this client
|
|
637
|
+
// before the promotion notice arrives, and a reconnect in that window
|
|
638
|
+
// must ask as the writer, not as the observer it still renders as. If
|
|
639
|
+
// the seat moved meanwhile the server says so and the reconnect lands
|
|
640
|
+
// as an observer, which is the truth of it.
|
|
641
|
+
entry.accessMode === "writer" || entry.writerClientInstanceId === this.#options.clientInstanceId
|
|
642
|
+
? { accessMode: "writer", expectedWriterEpoch: entry.writerEpoch }
|
|
643
|
+
: { accessMode: "observer" });
|
|
644
|
+
// A ConnectionGrant is one-use. Once the control plane has returned a
|
|
645
|
+
// replacement grant, retrying that mutation key after the WebSocket has
|
|
646
|
+
// consumed it can only return idempotency_consumed. A reconnect gets a
|
|
647
|
+
// fresh grant-scoped resume handle; retained output remains scoped to the
|
|
648
|
+
// same AgentSession and is selected by afterOutputSequence below.
|
|
649
|
+
delete entry.reconnectIdempotencyKey;
|
|
650
|
+
throwIfAborted(reconnectAbort.signal, "Terminal reconnection was cancelled.");
|
|
651
|
+
const revalidated = await this.#admitRemoteTerminal(entry.observation.agentSessionId, reconnectAbort.signal);
|
|
652
|
+
this.#assertAttachmentAdmissionContinuity(admitted, revalidated, "post_grant");
|
|
653
|
+
try {
|
|
654
|
+
this.#assertObservationContinuity(entry.observation, revalidated.observation, "reconnect");
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
entry.state = "failed";
|
|
658
|
+
entry.outputContinuity = "incomplete";
|
|
659
|
+
entry.reason = "session_authority_changed";
|
|
660
|
+
this.#publish(entry);
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
throwIfAborted(reconnectAbort.signal, "Terminal reconnection was cancelled.");
|
|
664
|
+
if (entry.connectionRevision !== reconnectRevision || entry.state !== "reconnecting" || this.#closed) {
|
|
665
|
+
throw runtimeFailure("terminal_disconnected", "Terminal reconnection was superseded before transport creation.");
|
|
666
|
+
}
|
|
667
|
+
connection = await this.#options.terminalConnector.connect({
|
|
668
|
+
url: grant.connectUrl,
|
|
669
|
+
token: grant.connectToken,
|
|
670
|
+
protocol: TERMINAL_PROTOCOL,
|
|
671
|
+
...(this.#options.canonicalTerminalViews === true ? { terminalViewProtocol: "cuna.terminal-view.v1" } : {}),
|
|
672
|
+
signal: reconnectAbort.signal,
|
|
673
|
+
});
|
|
674
|
+
throwIfAborted(reconnectAbort.signal, "Terminal reconnection was cancelled.");
|
|
675
|
+
if (connection.connectionId !== grant.terminalSessionId) {
|
|
676
|
+
throw runtimeFailure("grant_scope_mismatch", "The terminal transport accepted a different Cuna terminal session.");
|
|
677
|
+
}
|
|
678
|
+
const iterator = connection.receive()[Symbol.asyncIterator]();
|
|
679
|
+
const previousViewId = entry.viewId;
|
|
680
|
+
const nextDecoder = new TerminalFrameDecoder();
|
|
681
|
+
const candidate = {
|
|
682
|
+
...entry,
|
|
683
|
+
capabilitySnapshot: revalidated.capabilitySnapshot,
|
|
684
|
+
observation: revalidated.observation,
|
|
685
|
+
connection,
|
|
686
|
+
decoder: nextDecoder,
|
|
687
|
+
capabilities: grant.capabilities,
|
|
688
|
+
resumeHandle: grant.resumeHandle,
|
|
689
|
+
lastHeartbeatAt: this.#clock(),
|
|
690
|
+
heartbeatSequence: 0n,
|
|
691
|
+
heartbeatSendPending: false,
|
|
692
|
+
outputAbort: new AbortController(),
|
|
693
|
+
localActionsNegotiated: false,
|
|
694
|
+
localActionAcceptance: undefined,
|
|
695
|
+
accessMode: "observer",
|
|
696
|
+
writerEpoch: 0,
|
|
697
|
+
writerClientInstanceId: null,
|
|
698
|
+
wireWriterNotice: undefined,
|
|
699
|
+
geometry: null,
|
|
700
|
+
heldSeat: false,
|
|
701
|
+
};
|
|
702
|
+
const ready = await this.#awaitReady(candidate, iterator, reconnectAbort.signal);
|
|
703
|
+
if (entry.terminalView !== undefined && ready.payload.terminalViewProtocol === undefined)
|
|
704
|
+
throw runtimeFailure("terminal_protocol_error", "A canonical view cannot resume as a raw stream.");
|
|
705
|
+
if (ready.payload.terminalViewProtocol !== undefined && this.#options.canonicalTerminalViews !== true)
|
|
706
|
+
throw runtimeFailure("terminal_protocol_error", "Unrequested terminal view protocol.");
|
|
707
|
+
candidate.lastHeartbeatAt = this.#clock();
|
|
708
|
+
candidate.localActionAcceptance = this.#localActionAcceptance(ready.payload.localActionProtocol, candidate.observation.agentSessionId);
|
|
709
|
+
if (entry.connectionRevision !== reconnectRevision || entry.state !== "reconnecting" || this.#closed) {
|
|
710
|
+
throw runtimeFailure("terminal_disconnected", "Terminal reconnection was superseded by detach or shutdown.");
|
|
711
|
+
}
|
|
712
|
+
if (ready.payload.fencingGeneration <= entry.fencingGeneration) {
|
|
713
|
+
throw runtimeFailure("grant_invalid", "The reconnect readiness frame did not advance the attachment fence.");
|
|
714
|
+
}
|
|
715
|
+
const nextViewId = viewId(entry.tabId, ready.payload.fencingGeneration);
|
|
716
|
+
this.#requireGrantCapability(candidate, "live_resize");
|
|
717
|
+
if (candidate.resizeCapability !== "live") {
|
|
718
|
+
throw runtimeFailure("capability_unsupported", "The reconnected terminal cannot restore its dimensions.");
|
|
719
|
+
}
|
|
720
|
+
const previous = this.#views.require(previousViewId);
|
|
721
|
+
// As on attach: an observer never resizes the PTY. The seat that
|
|
722
|
+
// decides is the one this READY names, not the one held before the
|
|
723
|
+
// interruption: a writer may land back as an observer (the gateway
|
|
724
|
+
// would close the attachment on the RESIZE) and an observer may land
|
|
725
|
+
// as the writer (the PTY geometry must then be restored).
|
|
726
|
+
const landsAsWriter = ready.payload.accessMode === "writer";
|
|
727
|
+
const resizeSequence = landsAsWriter ? entry.wireSequence + 1n : entry.wireSequence;
|
|
728
|
+
if (landsAsWriter) {
|
|
729
|
+
await connection.send(encodeTerminalControl("resize", resizeSequence, {
|
|
730
|
+
columns: previous.columns,
|
|
731
|
+
rows: previous.rows,
|
|
732
|
+
}));
|
|
733
|
+
}
|
|
734
|
+
const resumeSequence = resizeSequence + 1n;
|
|
735
|
+
await connection.send(encodeTerminalControl("resume", resumeSequence, {
|
|
736
|
+
resumeHandle: grant.resumeHandle,
|
|
737
|
+
afterOutputSequence: ready.payload.terminalViewProtocol === undefined ? entry.outputSequence.toString() : "0",
|
|
738
|
+
...(ready.payload.terminalViewProtocol === undefined ? {} : { terminalViewProtocol: { name: "cuna.terminal-view.v1", operation: "new" } }),
|
|
739
|
+
...(candidate.localActionAcceptance === undefined ? {} : { localActionProtocol: candidate.localActionAcceptance }),
|
|
740
|
+
}));
|
|
741
|
+
if (entry.connectionRevision !== reconnectRevision || entry.state !== "reconnecting" || this.#closed) {
|
|
742
|
+
throw runtimeFailure("terminal_disconnected", "Terminal reconnection was superseded by detach or shutdown.");
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
this.#views.detach(previousViewId);
|
|
746
|
+
}
|
|
747
|
+
catch { /* the prior view may already be detached */ }
|
|
748
|
+
this.#views.open({
|
|
749
|
+
viewId: nextViewId,
|
|
750
|
+
binding: {
|
|
751
|
+
userId: revalidated.observation.userId,
|
|
752
|
+
machineId: revalidated.observation.machineId,
|
|
753
|
+
agentSessionId: revalidated.observation.agentSessionId,
|
|
754
|
+
processEpoch: revalidated.observation.processEpoch,
|
|
755
|
+
fencingGeneration: ready.payload.fencingGeneration,
|
|
756
|
+
},
|
|
757
|
+
state: "active",
|
|
758
|
+
columns: previous.columns,
|
|
759
|
+
rows: previous.rows,
|
|
760
|
+
});
|
|
761
|
+
entry.connection = connection;
|
|
762
|
+
if (ready.payload.terminalViewProtocol !== undefined) {
|
|
763
|
+
entry.terminalView = { viewId: null, ready: false };
|
|
764
|
+
entry.outputSequence = 0n;
|
|
765
|
+
}
|
|
766
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
767
|
+
entry.observation = revalidated.observation;
|
|
768
|
+
entry.capabilitySnapshot = revalidated.capabilitySnapshot;
|
|
769
|
+
entry.decoder = nextDecoder;
|
|
770
|
+
entry.fencingGeneration = ready.payload.fencingGeneration;
|
|
771
|
+
entry.capabilities = grant.capabilities;
|
|
772
|
+
entry.resizeCapability = ready.payload.resizeCapability;
|
|
773
|
+
// A writer that reconnects and lands as an observer was demoted by a
|
|
774
|
+
// transfer while it was away: the notice frame that would have said so
|
|
775
|
+
// was queued to the attachment that closed. Say it from the durable
|
|
776
|
+
// fact instead, so the former writer is never left observing in
|
|
777
|
+
// silence.
|
|
778
|
+
entry.accessMode = ready.payload.accessMode;
|
|
779
|
+
entry.writerEpoch = ready.payload.writerEpoch;
|
|
780
|
+
entry.writerClientInstanceId = ready.payload.accessMode === "writer" ? this.#options.clientInstanceId : null;
|
|
781
|
+
entry.wireWriterNotice = ready.payload.accessMode === "writer" ? Object.freeze({
|
|
782
|
+
writerEpoch: ready.payload.writerEpoch, writerClientInstanceId: this.#options.clientInstanceId, accessMode: "writer",
|
|
783
|
+
}) : undefined;
|
|
784
|
+
entry.geometry = null;
|
|
785
|
+
if (entry.writerTransfer !== undefined && ready.payload.writerEpoch > entry.writerTransfer.expectedWriterEpoch) {
|
|
786
|
+
delete entry.writerTransfer;
|
|
787
|
+
}
|
|
788
|
+
if (ready.payload.accessMode === "writer")
|
|
789
|
+
entry.heldSeat = true;
|
|
790
|
+
else if (entry.heldSeat)
|
|
791
|
+
entry.reason = "writer_transferred";
|
|
792
|
+
else
|
|
793
|
+
delete entry.reason;
|
|
794
|
+
entry.viewId = nextViewId;
|
|
795
|
+
entry.resumeHandle = grant.resumeHandle;
|
|
796
|
+
entry.localActionAcceptance = candidate.localActionAcceptance;
|
|
797
|
+
entry.localActionsNegotiated = candidate.localActionAcceptance !== undefined;
|
|
798
|
+
entry.wireSequence = resumeSequence;
|
|
799
|
+
entry.replayBoundarySequence = resumeSequence;
|
|
800
|
+
entry.replayBoundaryObserved = false;
|
|
801
|
+
entry.heartbeatSequence = 0n;
|
|
802
|
+
entry.heartbeatSendPending = false;
|
|
803
|
+
entry.outputAbort = candidate.outputAbort;
|
|
804
|
+
entry.lastHeartbeatAt = candidate.lastHeartbeatAt;
|
|
805
|
+
entry.state = "active";
|
|
806
|
+
entry.outputContinuity = "unknown";
|
|
807
|
+
await this.#options.onTerminalReady?.(snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock()));
|
|
808
|
+
this.#armTerminalViewDeadline(entry);
|
|
809
|
+
for (const frame of ready.bufferedFrames)
|
|
810
|
+
await this.#handleAttachedFrame(entry, frame);
|
|
811
|
+
this.#scheduleHeartbeatWatchdog(entry, connection, reconnectRevision);
|
|
812
|
+
this.#publish(entry);
|
|
813
|
+
entry.pump = this.#pump(entry, connection, reconnectRevision, iterator);
|
|
814
|
+
this.#forgetConnectionRequests(entry.observation.agentSessionId);
|
|
815
|
+
return snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock());
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
let reportedError = error;
|
|
819
|
+
if (connection !== undefined) {
|
|
820
|
+
try {
|
|
821
|
+
await connection.close({ code: 1008, reason: "cuna_resume_rejected" });
|
|
822
|
+
}
|
|
823
|
+
catch (cleanupError) {
|
|
824
|
+
completionFailure = cleanupError;
|
|
825
|
+
reportedError = new AggregateError([error, cleanupError], "Terminal reconnection failed and its replacement transport cleanup was incomplete.");
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
await this.#cancelConnectionRequests(entry.observation.agentSessionId);
|
|
830
|
+
}
|
|
831
|
+
catch (cleanupError) {
|
|
832
|
+
completionFailure = cleanupError;
|
|
833
|
+
reportedError = new AggregateError([reportedError, cleanupError], "Terminal reconnect failed and issuance cancellation is unconfirmed.");
|
|
834
|
+
}
|
|
835
|
+
if (entry.connectionRevision === reconnectRevision &&
|
|
836
|
+
entry.state !== "failed") {
|
|
837
|
+
entry.state = isPermanentInputRecoveryFailure(error) || isHistoryGap(error) ? "failed" : "interrupted";
|
|
838
|
+
entry.outputContinuity = "unknown";
|
|
839
|
+
entry.reason = safeReason(isPermanentInputRecoveryFailure(error) || isHistoryGap(error) ? error : reportedError);
|
|
840
|
+
this.#publish(entry);
|
|
841
|
+
}
|
|
842
|
+
throw reportedError;
|
|
843
|
+
}
|
|
844
|
+
finally {
|
|
845
|
+
const pending = this.#pendingReconnects.get(input.tabId);
|
|
846
|
+
pending?.removeInputAbort();
|
|
847
|
+
pending?.settle(completionFailure);
|
|
848
|
+
this.#pendingReconnects.delete(input.tabId);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async detach(tabId) {
|
|
852
|
+
const entry = this.#requireTerminal(tabId);
|
|
853
|
+
if (entry.state === "closed") {
|
|
854
|
+
this.#terminals.delete(tabId);
|
|
855
|
+
if (this.#activeTabId === tabId)
|
|
856
|
+
this.#activeTabId = this.#nextActiveTab(tabId);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const pendingReconnect = this.#pendingReconnects.get(tabId);
|
|
860
|
+
pendingReconnect?.abort.abort(runtimeFailure("terminal_disconnected", "The terminal reconnection was detached."));
|
|
861
|
+
entry.connectionRevision += 1;
|
|
862
|
+
entry.outputAbort.abort(runtimeFailure("terminal_disconnected", "The terminal attachment was detached."));
|
|
863
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
864
|
+
try {
|
|
865
|
+
this.#views.detach(entry.viewId);
|
|
866
|
+
}
|
|
867
|
+
catch { /* attach may have failed before view creation */ }
|
|
868
|
+
entry.state = "detached";
|
|
869
|
+
entry.reason = "explicit_detach";
|
|
870
|
+
this.#publish(entry);
|
|
871
|
+
const sendDrain = entry.sendTail;
|
|
872
|
+
const pumpDrain = entry.pump;
|
|
873
|
+
await entry.connection.close({ code: 1000, reason: "cuna_detach" });
|
|
874
|
+
await sendDrain;
|
|
875
|
+
if (pumpDrain !== undefined)
|
|
876
|
+
await withOutputDeadline(pumpDrain, this.#options.outputDeliveryTimeoutMs ?? 5_000);
|
|
877
|
+
if (pendingReconnect !== undefined) {
|
|
878
|
+
await withOutputDeadline(pendingReconnect.completion, this.#options.readyTimeoutMs ?? 10_000);
|
|
879
|
+
}
|
|
880
|
+
if (this.#activeTabId === tabId)
|
|
881
|
+
this.#activeTabId = this.#nextActiveTab(tabId);
|
|
882
|
+
this.#terminals.delete(tabId);
|
|
883
|
+
}
|
|
884
|
+
listTerminals() {
|
|
885
|
+
return Object.freeze([...this.#terminals.values()].map((entry) => snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock())));
|
|
886
|
+
}
|
|
887
|
+
async openSync(input) {
|
|
888
|
+
if (this.#mode !== "daemon") {
|
|
889
|
+
throw runtimeFailure("capability_unsupported", "Foreground runtime mode cannot own workspace synchronization.");
|
|
890
|
+
}
|
|
891
|
+
this.#assertReady();
|
|
892
|
+
if (this.#syncHandles.has(input.configuration.bindingId) || this.#pendingSyncOpens.has(input.configuration.bindingId)) {
|
|
893
|
+
throw runtimeFailure("session_conflict", "This runtime already owns the workspace sync binding.");
|
|
894
|
+
}
|
|
895
|
+
let settleOpen = (_failure) => undefined;
|
|
896
|
+
const openCompletion = new Promise((resolve) => { settleOpen = resolve; });
|
|
897
|
+
this.#pendingSyncOpens.set(input.configuration.bindingId, openCompletion);
|
|
898
|
+
let journal;
|
|
899
|
+
let completionFailure;
|
|
900
|
+
try {
|
|
901
|
+
journal = await DurableSyncJournal.open({
|
|
902
|
+
directory: input.journalDirectory,
|
|
903
|
+
bindingId: input.configuration.bindingId,
|
|
904
|
+
bindingGeneration: input.configuration.bindingGeneration,
|
|
905
|
+
ownerId: input.ownerId,
|
|
906
|
+
...(input.leaseMs === undefined ? {} : { leaseMs: input.leaseMs }),
|
|
907
|
+
clock: this.#clock,
|
|
908
|
+
});
|
|
909
|
+
this.#assertOpen();
|
|
910
|
+
if (this.#syncHandles.has(input.configuration.bindingId)) {
|
|
911
|
+
throw runtimeFailure("session_conflict", "This runtime already owns the workspace sync binding.");
|
|
912
|
+
}
|
|
913
|
+
const openedJournal = journal;
|
|
914
|
+
const { supervisor } = this.#syncRegistry.connect(input.configuration, this.#clock);
|
|
915
|
+
supervisor.beginReconciliation("runtime_start_requires_authoritative_manifest");
|
|
916
|
+
let closed = false;
|
|
917
|
+
let closing;
|
|
918
|
+
const handle = Object.freeze({
|
|
919
|
+
bindingId: input.configuration.bindingId,
|
|
920
|
+
fence: openedJournal.fence,
|
|
921
|
+
supervisor,
|
|
922
|
+
close: async () => {
|
|
923
|
+
if (closed)
|
|
924
|
+
return;
|
|
925
|
+
closing ??= (async () => {
|
|
926
|
+
await openedJournal.close();
|
|
927
|
+
this.#syncRegistry.release(input.configuration.bindingId, supervisor);
|
|
928
|
+
this.#syncHandles.delete(input.configuration.bindingId);
|
|
929
|
+
closed = true;
|
|
930
|
+
})();
|
|
931
|
+
try {
|
|
932
|
+
await closing;
|
|
933
|
+
}
|
|
934
|
+
finally {
|
|
935
|
+
if (!closed)
|
|
936
|
+
closing = undefined;
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
this.#syncHandles.set(input.configuration.bindingId, handle);
|
|
941
|
+
return handle;
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
if (journal !== undefined) {
|
|
945
|
+
try {
|
|
946
|
+
await journal.close();
|
|
947
|
+
}
|
|
948
|
+
catch (cleanupError) {
|
|
949
|
+
completionFailure = new AggregateError([error, cleanupError], "The sync journal failed to close after an open failure.");
|
|
950
|
+
throw completionFailure;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
955
|
+
finally {
|
|
956
|
+
settleOpen(completionFailure);
|
|
957
|
+
this.#pendingSyncOpens.delete(input.configuration.bindingId);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async shutdown() {
|
|
961
|
+
if (this.#shutdownComplete)
|
|
962
|
+
return;
|
|
963
|
+
if (this.#shutdownFlight !== undefined)
|
|
964
|
+
return await this.#shutdownFlight;
|
|
965
|
+
this.#closed = true;
|
|
966
|
+
const attempt = this.#performShutdown();
|
|
967
|
+
this.#shutdownFlight = attempt;
|
|
968
|
+
try {
|
|
969
|
+
await attempt;
|
|
970
|
+
this.#shutdownComplete = true;
|
|
971
|
+
}
|
|
972
|
+
finally {
|
|
973
|
+
if (this.#shutdownFlight === attempt)
|
|
974
|
+
this.#shutdownFlight = undefined;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async #performShutdown() {
|
|
978
|
+
if (this.#mode === "foreground" && this.#foreground.state === "ready") {
|
|
979
|
+
this.#foreground = Object.freeze({
|
|
980
|
+
state: "quiescing",
|
|
981
|
+
reason: "foreground_runtime_shutdown",
|
|
982
|
+
updatedAt: this.#clock(),
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
const state = this.#daemon.snapshot().state;
|
|
986
|
+
if (state === "ready" || state === "degraded" || state === "reconciling") {
|
|
987
|
+
this.#daemon.transition("quiescing", "runtime_shutdown", this.#clock());
|
|
988
|
+
}
|
|
989
|
+
const failures = [];
|
|
990
|
+
const pendingAttaches = [...this.#pendingAttaches.values()];
|
|
991
|
+
const pendingReconnects = [...this.#pendingReconnects.values()];
|
|
992
|
+
const pendingSyncOpens = [...this.#pendingSyncOpens.values()];
|
|
993
|
+
for (const pending of pendingAttaches) {
|
|
994
|
+
pending.abort.abort(runtimeFailure("terminal_disconnected", "The terminal runtime was shut down."));
|
|
995
|
+
}
|
|
996
|
+
for (const pending of pendingReconnects) {
|
|
997
|
+
pending.abort.abort(runtimeFailure("terminal_disconnected", "The terminal runtime was shut down."));
|
|
998
|
+
}
|
|
999
|
+
for (const [tabId, entry] of this.#terminals) {
|
|
1000
|
+
entry.connectionRevision += 1;
|
|
1001
|
+
entry.outputAbort.abort(runtimeFailure("terminal_disconnected", "The terminal runtime was shut down."));
|
|
1002
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
1003
|
+
const sendDrain = entry.sendTail;
|
|
1004
|
+
const pumpDrain = entry.pump;
|
|
1005
|
+
try {
|
|
1006
|
+
await entry.connection.close({ code: 1000, reason: "cuna_shutdown" });
|
|
1007
|
+
await sendDrain;
|
|
1008
|
+
if (pumpDrain !== undefined)
|
|
1009
|
+
await withOutputDeadline(pumpDrain, this.#options.outputDeliveryTimeoutMs ?? 5_000);
|
|
1010
|
+
}
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
failures.push(error);
|
|
1013
|
+
entry.state = "failed";
|
|
1014
|
+
entry.reason = "runtime_shutdown_cleanup_failed";
|
|
1015
|
+
this.#publish(entry);
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
entry.state = "closed";
|
|
1019
|
+
entry.reason = "runtime_shutdown";
|
|
1020
|
+
try {
|
|
1021
|
+
this.#views.detach(entry.viewId);
|
|
1022
|
+
}
|
|
1023
|
+
catch { /* the view may already be detached */ }
|
|
1024
|
+
this.#publish(entry);
|
|
1025
|
+
this.#terminals.delete(tabId);
|
|
1026
|
+
}
|
|
1027
|
+
this.#activeTabId = this.#terminals.size === 0 ? undefined : this.#nextActiveTab("");
|
|
1028
|
+
for (const pending of pendingAttaches) {
|
|
1029
|
+
try {
|
|
1030
|
+
const failure = await withOutputDeadline(pending.completion, this.#options.readyTimeoutMs ?? 10_000);
|
|
1031
|
+
if (failure !== undefined)
|
|
1032
|
+
failures.push(failure);
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
failures.push(error);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
for (const pending of pendingReconnects) {
|
|
1039
|
+
try {
|
|
1040
|
+
const failure = await withOutputDeadline(pending.completion, this.#options.readyTimeoutMs ?? 10_000);
|
|
1041
|
+
if (failure !== undefined)
|
|
1042
|
+
failures.push(failure);
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
failures.push(error);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
for (const pending of pendingSyncOpens) {
|
|
1049
|
+
try {
|
|
1050
|
+
const failure = await withOutputDeadline(pending, this.#options.readyTimeoutMs ?? 10_000, () => runtimeFailure("runtime_cleanup_timeout", "Workspace synchronization is still closing. Retry shutdown to finish cleanup.", { retryable: true }));
|
|
1051
|
+
if (failure !== undefined)
|
|
1052
|
+
failures.push(failure);
|
|
1053
|
+
}
|
|
1054
|
+
catch (error) {
|
|
1055
|
+
failures.push(error);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
for (const handle of this.#syncHandles.values()) {
|
|
1059
|
+
try {
|
|
1060
|
+
await handle.close();
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
failures.push(error);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
for (const subject of new Set([...this.#pendingConnectionRequests.values()].map(request => request.agentSessionId))) {
|
|
1067
|
+
try {
|
|
1068
|
+
await this.#cancelConnectionRequests(subject);
|
|
1069
|
+
}
|
|
1070
|
+
catch (error) {
|
|
1071
|
+
failures.push(error);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (this.#mode === "foreground") {
|
|
1075
|
+
this.#foreground = Object.freeze({
|
|
1076
|
+
state: failures.length === 0 ? "stopped" : "cleanup_failed",
|
|
1077
|
+
reason: failures.length === 0 ? "foreground_runtime_stopped" : "foreground_runtime_cleanup_failed",
|
|
1078
|
+
updatedAt: this.#clock(),
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
const after = this.#daemon.snapshot().state;
|
|
1083
|
+
if (failures.length === 0) {
|
|
1084
|
+
if (after === "quiescing" || after === "recovery_required" || after === "starting") {
|
|
1085
|
+
this.#daemon.transition("stopped", "runtime_stopped", this.#clock());
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
else if (after === "quiescing" || after === "starting") {
|
|
1089
|
+
this.#daemon.transition("recovery_required", "runtime_shutdown_cleanup_failed", this.#clock());
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
if (failures.length > 0)
|
|
1093
|
+
throw new AggregateError(failures, "The Cuna runtime stopped with cleanup failures.");
|
|
1094
|
+
}
|
|
1095
|
+
async #admitRemoteTerminal(agentSessionId, signal) {
|
|
1096
|
+
const snapshot = await this.#options.controlPlane.discoverCapabilities("agent_session", agentSessionId, signal);
|
|
1097
|
+
const capability = admitCapability(snapshot, {
|
|
1098
|
+
id: this.#options.terminalCapabilityId,
|
|
1099
|
+
scope: "agent_session",
|
|
1100
|
+
subjectId: agentSessionId,
|
|
1101
|
+
surface: "cli",
|
|
1102
|
+
interaction: "native",
|
|
1103
|
+
}, this.#clock());
|
|
1104
|
+
const observation = assertRemoteAgentSessionEvidence({
|
|
1105
|
+
evidence: await this.#options.controlPlane.observeAgentSession(agentSessionId, signal),
|
|
1106
|
+
expectedAgentSessionId: agentSessionId,
|
|
1107
|
+
now: this.#clock(),
|
|
1108
|
+
});
|
|
1109
|
+
return Object.freeze({ capability, observation, capabilitySnapshot: snapshot });
|
|
1110
|
+
}
|
|
1111
|
+
#assertAttachmentAdmissionContinuity(expected, actual, phase) {
|
|
1112
|
+
this.#assertObservationContinuity(expected.observation, actual.observation, phase);
|
|
1113
|
+
const now = this.#clock();
|
|
1114
|
+
if (expected.capability.capabilityId !== actual.capability.capabilityId ||
|
|
1115
|
+
expected.capability.scope !== actual.capability.scope ||
|
|
1116
|
+
expected.capability.subjectId !== actual.capability.subjectId) {
|
|
1117
|
+
throw runtimeFailure("capability_scope_mismatch", `Terminal capability scope changed during ${phase === "preflight" ? "preflight" : "post-grant"} admission.`);
|
|
1118
|
+
}
|
|
1119
|
+
if (expected.capability.expiresAt <= now || actual.capability.expiresAt <= now) {
|
|
1120
|
+
throw runtimeFailure("capability_snapshot_expired", `Terminal capability authority expired during ${phase === "preflight" ? "preflight" : "post-grant"} admission.`);
|
|
1121
|
+
}
|
|
1122
|
+
if (expected.capability.snapshotEtag !== actual.capability.snapshotEtag) {
|
|
1123
|
+
throw runtimeFailure("capability_unknown", `Terminal capability authority changed during ${phase === "preflight" ? "preflight" : "post-grant"} admission.`);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
#assertObservationContinuity(expected, actual, phase) {
|
|
1127
|
+
if (expected.authority !== actual.authority ||
|
|
1128
|
+
expected.userId !== actual.userId ||
|
|
1129
|
+
expected.machineId !== actual.machineId ||
|
|
1130
|
+
expected.workspaceBindingId !== actual.workspaceBindingId ||
|
|
1131
|
+
expected.workspaceBindingGeneration !== actual.workspaceBindingGeneration ||
|
|
1132
|
+
expected.agentSessionId !== actual.agentSessionId ||
|
|
1133
|
+
expected.processEpoch !== actual.processEpoch) {
|
|
1134
|
+
throw runtimeFailure("session_discontinuous", `The AgentSession machine or process generation changed during ${phase === "post_grant" ? "post-grant" : phase} admission.`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
async #createGrant(observation, capability, resumeHandle, idempotencyKey = this.#idempotencyKey(), signal, seat = { accessMode: "writer" }) {
|
|
1138
|
+
const request = (accessMode, key) => {
|
|
1139
|
+
const ownedRequest = {
|
|
1140
|
+
agentSessionId: observation.agentSessionId,
|
|
1141
|
+
protocol: TERMINAL_PROTOCOL,
|
|
1142
|
+
clientInstanceId: this.#options.clientInstanceId,
|
|
1143
|
+
idempotencyKey: key,
|
|
1144
|
+
capabilityEvidence: capability,
|
|
1145
|
+
accessMode,
|
|
1146
|
+
...(resumeHandle === undefined ? {} : { resumeHandle }),
|
|
1147
|
+
...(accessMode === "writer" && seat.expectedWriterEpoch !== undefined
|
|
1148
|
+
? { expectedWriterEpoch: seat.expectedWriterEpoch }
|
|
1149
|
+
: {}),
|
|
1150
|
+
...(signal === undefined ? {} : { signal }),
|
|
1151
|
+
};
|
|
1152
|
+
this.#pendingConnectionRequests.set(key, ownedRequest);
|
|
1153
|
+
return this.#options.controlPlane.createTerminalConnection(ownedRequest);
|
|
1154
|
+
};
|
|
1155
|
+
let grant;
|
|
1156
|
+
try {
|
|
1157
|
+
grant = await request(seat.accessMode, idempotencyKey);
|
|
1158
|
+
}
|
|
1159
|
+
catch (error) {
|
|
1160
|
+
// The writing seat belongs to another client of this principal, or it
|
|
1161
|
+
// moved while this one was away. Attach as an observer instead of
|
|
1162
|
+
// failing: the terminal is still visible, and Ctrl+] w asks for the
|
|
1163
|
+
// seat later through the transfer the server arbitrates.
|
|
1164
|
+
if (seat.accessMode !== "writer" || !writerSeatUnavailable(error))
|
|
1165
|
+
throw error;
|
|
1166
|
+
this.#pendingConnectionRequests.delete(idempotencyKey);
|
|
1167
|
+
grant = await request("observer", this.#idempotencyKey());
|
|
1168
|
+
}
|
|
1169
|
+
return validateTerminalGrant({
|
|
1170
|
+
grant,
|
|
1171
|
+
allowedCunaOrigins: this.#options.allowedCunaOrigins,
|
|
1172
|
+
requiredCapabilities: ["acknowledgement", "heartbeat", "resume", "live_resize"],
|
|
1173
|
+
now: this.#clock(),
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
#forgetConnectionRequests(agentSessionId) {
|
|
1177
|
+
for (const [key, request] of this.#pendingConnectionRequests) {
|
|
1178
|
+
if (request.agentSessionId === agentSessionId)
|
|
1179
|
+
this.#pendingConnectionRequests.delete(key);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async #cancelConnectionRequests(agentSessionId) {
|
|
1183
|
+
for (const [key, request] of this.#pendingConnectionRequests) {
|
|
1184
|
+
if (request.agentSessionId !== agentSessionId)
|
|
1185
|
+
continue;
|
|
1186
|
+
const signal = AbortSignal.timeout(this.#options.readyTimeoutMs ?? 10_000);
|
|
1187
|
+
try {
|
|
1188
|
+
await withOutputDeadline(this.#options.controlPlane.cancelTerminalConnection({ ...request, signal }), this.#options.readyTimeoutMs ?? 10_000, () => runtimeFailure("runtime_cleanup_timeout", "Terminal issuance cancellation is unconfirmed. Retry cleanup with the original request.", { retryable: true }));
|
|
1189
|
+
}
|
|
1190
|
+
catch (error) {
|
|
1191
|
+
const reason = error?.details?.reason;
|
|
1192
|
+
// This endpoint fences unredeemed issuance only. A redeemed connection
|
|
1193
|
+
// is closed through the owned socket; cancellation cannot detach it.
|
|
1194
|
+
if (reason !== "terminal_connection_already_redeemed")
|
|
1195
|
+
throw error;
|
|
1196
|
+
}
|
|
1197
|
+
this.#pendingConnectionRequests.delete(key);
|
|
1198
|
+
for (const entry of this.#terminals.values()) {
|
|
1199
|
+
if (entry.observation.agentSessionId === agentSessionId && entry.reconnectIdempotencyKey === key)
|
|
1200
|
+
delete entry.reconnectIdempotencyKey;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
async #awaitReady(entry, iterator, signal) {
|
|
1205
|
+
const deadlineMs = this.#options.readyTimeoutMs ?? 10_000;
|
|
1206
|
+
if (!Number.isSafeInteger(deadlineMs) || deadlineMs < 1 || deadlineMs > 60_000) {
|
|
1207
|
+
throw runtimeFailure("terminal_timeout", "The terminal readiness deadline is invalid.");
|
|
1208
|
+
}
|
|
1209
|
+
const deadline = Date.now() + deadlineMs;
|
|
1210
|
+
while (Date.now() < deadline) {
|
|
1211
|
+
if (signal?.aborted)
|
|
1212
|
+
throw runtimeFailure("terminal_disconnected", "Terminal attachment was cancelled.");
|
|
1213
|
+
const remaining = deadline - Date.now();
|
|
1214
|
+
const result = await nextWithTimeout(iterator, remaining, signal);
|
|
1215
|
+
if (result.done)
|
|
1216
|
+
throw runtimeFailure("terminal_not_ready", "The terminal closed before PTY readiness was proven.");
|
|
1217
|
+
const frames = entry.decoder.push(result.value);
|
|
1218
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
1219
|
+
const frame = frames[index];
|
|
1220
|
+
if (frame === undefined)
|
|
1221
|
+
continue;
|
|
1222
|
+
assertTerminalFrameLegal("negotiating", "server_to_client", frame.type);
|
|
1223
|
+
if (frame.type === "error")
|
|
1224
|
+
throw this.#remoteTerminalError(frame, entry.observation.agentSessionId);
|
|
1225
|
+
if (frame.type !== "ready")
|
|
1226
|
+
continue;
|
|
1227
|
+
const payload = decodeTerminalControl(frame);
|
|
1228
|
+
assertReadyPayloadMatches(payload, entry.observation);
|
|
1229
|
+
return Object.freeze({
|
|
1230
|
+
payload,
|
|
1231
|
+
bufferedFrames: Object.freeze(frames.slice(index + 1)),
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
throw runtimeFailure("terminal_timeout", "The terminal did not prove PTY readiness before the deadline.", { retryable: true });
|
|
1236
|
+
}
|
|
1237
|
+
#requireGrantCapability(entry, name) {
|
|
1238
|
+
const matches = entry.capabilities.filter((capability) => capability.name === name);
|
|
1239
|
+
if (matches.length !== 1 || matches[0]?.availability === "unknown") {
|
|
1240
|
+
throw runtimeFailure("capability_unknown", `Cuna cannot prove terminal capability ${name}.`);
|
|
1241
|
+
}
|
|
1242
|
+
if (matches[0]?.availability !== "supported") {
|
|
1243
|
+
throw runtimeFailure("capability_unsupported", `Terminal capability ${name} is unsupported.`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
async #pump(entry, connection, connectionRevision, iterator) {
|
|
1247
|
+
try {
|
|
1248
|
+
for (;;) {
|
|
1249
|
+
const result = await iterator.next();
|
|
1250
|
+
if (entry.connection !== connection || entry.connectionRevision !== connectionRevision)
|
|
1251
|
+
return;
|
|
1252
|
+
if (result.done)
|
|
1253
|
+
break;
|
|
1254
|
+
for (const frame of entry.decoder.push(result.value)) {
|
|
1255
|
+
if (entry.connection !== connection ||
|
|
1256
|
+
entry.connectionRevision !== connectionRevision ||
|
|
1257
|
+
entry.state === "closed" ||
|
|
1258
|
+
entry.state === "detached" ||
|
|
1259
|
+
entry.state === "failed")
|
|
1260
|
+
return;
|
|
1261
|
+
await this.#handleAttachedFrame(entry, frame);
|
|
1262
|
+
}
|
|
1263
|
+
if (entry.state === "closed" || entry.state === "detached")
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
if (entry.state === "active" || entry.state === "reconnecting") {
|
|
1267
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
1268
|
+
entry.outputAbort.abort(runtimeFailure("terminal_disconnected", "The terminal transport closed."));
|
|
1269
|
+
entry.state = "interrupted";
|
|
1270
|
+
retireInputAcceptance(entry);
|
|
1271
|
+
entry.outputContinuity = "unknown";
|
|
1272
|
+
entry.reason = "transport_closed_without_terminal_exit";
|
|
1273
|
+
this.#publish(entry);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
catch (error) {
|
|
1277
|
+
if (entry.connection !== connection || entry.connectionRevision !== connectionRevision)
|
|
1278
|
+
return;
|
|
1279
|
+
if (entry.state === "detached" || entry.state === "closed")
|
|
1280
|
+
return;
|
|
1281
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
1282
|
+
entry.outputAbort.abort(error);
|
|
1283
|
+
const remoteReason = remoteSafeReason(error);
|
|
1284
|
+
// Expiry ends this attachment, not the remote process. Reconnect must
|
|
1285
|
+
// obtain fresh admission; it cannot reuse expired input authority.
|
|
1286
|
+
const expiredView = remoteReason === "view.lease_expired";
|
|
1287
|
+
entry.state = (error instanceof TerminalProtocolError ||
|
|
1288
|
+
(error instanceof RuntimeBoundaryError && error.code === "terminal_protocol_error") || isHistoryGap(error)) && !expiredView ? "failed" : "interrupted";
|
|
1289
|
+
retireInputAcceptance(entry);
|
|
1290
|
+
entry.outputContinuity = "unknown";
|
|
1291
|
+
entry.reason = safeReason(error);
|
|
1292
|
+
if (remoteReason === undefined)
|
|
1293
|
+
delete entry.remoteReason;
|
|
1294
|
+
else
|
|
1295
|
+
entry.remoteReason = remoteReason;
|
|
1296
|
+
this.#publish(entry);
|
|
1297
|
+
await connection.close({ code: 1002, reason: "cuna_terminal_protocol_failure" }).catch(() => undefined);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
#armTerminalViewDeadline(entry) {
|
|
1301
|
+
if (entry.terminalViewDeadline !== undefined)
|
|
1302
|
+
clearTimeout(entry.terminalViewDeadline);
|
|
1303
|
+
if (entry.terminalView === undefined)
|
|
1304
|
+
return;
|
|
1305
|
+
const revision = entry.connectionRevision;
|
|
1306
|
+
entry.terminalViewDeadline = setTimeout(() => {
|
|
1307
|
+
if (this.#closed || entry.connectionRevision !== revision || entry.state !== "active" || entry.terminalView?.ready === true)
|
|
1308
|
+
return;
|
|
1309
|
+
const failure = runtimeFailure("terminal_protocol_error", "The current terminal view did not become ready before its deadline.");
|
|
1310
|
+
entry.outputAbort.abort(failure);
|
|
1311
|
+
entry.state = "failed";
|
|
1312
|
+
entry.outputContinuity = "incomplete";
|
|
1313
|
+
retireInputAcceptance(entry);
|
|
1314
|
+
this.#publish(entry);
|
|
1315
|
+
void entry.connection.close({ code: 1002, reason: "cuna_terminal_view_timeout" }).catch(() => undefined);
|
|
1316
|
+
}, this.#options.readyTimeoutMs ?? 5_000);
|
|
1317
|
+
entry.terminalViewDeadline.unref();
|
|
1318
|
+
}
|
|
1319
|
+
async #handleAttachedFrame(entry, frame) {
|
|
1320
|
+
assertTerminalFrameLegal("attached", "server_to_client", frame.type, entry.localActionsNegotiated);
|
|
1321
|
+
if (isLocalActionFrameType(frame.type)) {
|
|
1322
|
+
const payload = decodeTerminalControl(frame);
|
|
1323
|
+
if (frame.type === "local_action_request") {
|
|
1324
|
+
const request = payload.request;
|
|
1325
|
+
const identity = request.identity;
|
|
1326
|
+
if (identity.userId !== entry.observation.userId ||
|
|
1327
|
+
identity.machineId !== entry.observation.machineId ||
|
|
1328
|
+
identity.workspaceBindingId !== entry.observation.workspaceBindingId ||
|
|
1329
|
+
identity.workspaceBindingGeneration !== entry.observation.workspaceBindingGeneration ||
|
|
1330
|
+
identity.agentSessionId !== entry.observation.agentSessionId ||
|
|
1331
|
+
identity.processEpoch !== entry.observation.processEpoch ||
|
|
1332
|
+
identity.fencingGeneration !== entry.fencingGeneration ||
|
|
1333
|
+
!entry.localActionAcceptance?.acceptedKinds.includes(request.kind)) {
|
|
1334
|
+
throw runtimeFailure("grant_scope_mismatch", "The local action request targets another attachment authority.");
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (this.#options.onLocalActionFrame === undefined) {
|
|
1338
|
+
throw runtimeFailure("terminal_protocol_error", "A local action frame arrived without a local broker consumer.");
|
|
1339
|
+
}
|
|
1340
|
+
await this.#options.onLocalActionFrame({ tabId: entry.tabId, frame, payload });
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (frame.type === "control_state") {
|
|
1344
|
+
const geometry = decodeTerminalControl(frame);
|
|
1345
|
+
// HTTP may reveal a newer committed epoch before an older queued wire
|
|
1346
|
+
// notice arrives. Ignore its geometry without losing the attachment.
|
|
1347
|
+
if (geometry.writerEpoch < entry.writerEpoch)
|
|
1348
|
+
return;
|
|
1349
|
+
if (geometry.writerEpoch !== entry.writerEpoch) {
|
|
1350
|
+
throw runtimeFailure("terminal_protocol_error", "Terminal geometry targets a different writer epoch.");
|
|
1351
|
+
}
|
|
1352
|
+
entry.geometry = geometry;
|
|
1353
|
+
if (this.#options.onTerminalGeometry !== undefined) {
|
|
1354
|
+
const timeoutMs = this.#options.outputDeliveryTimeoutMs ?? 5_000;
|
|
1355
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000) {
|
|
1356
|
+
throw runtimeFailure("terminal_protocol_error", "The terminal geometry delivery deadline is invalid.");
|
|
1357
|
+
}
|
|
1358
|
+
try {
|
|
1359
|
+
await withOutputDeadline(Promise.resolve(this.#options.onTerminalGeometry({
|
|
1360
|
+
snapshot: snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock()), signal: entry.outputAbort.signal,
|
|
1361
|
+
})), timeoutMs, () => runtimeFailure("terminal_protocol_error", "Terminal geometry application exceeded its bounded deadline."));
|
|
1362
|
+
}
|
|
1363
|
+
catch (error) {
|
|
1364
|
+
// Initial same-chunk delivery runs before the pump exists. Revoke
|
|
1365
|
+
// its consumer here as well, so a timed-out resize cannot later
|
|
1366
|
+
// mutate a viewport after failed attachment cleanup has returned.
|
|
1367
|
+
entry.geometry = null;
|
|
1368
|
+
entry.outputAbort.abort(error);
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
this.#publish(entry);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (frame.type === "view_started") {
|
|
1376
|
+
const payload = decodeTerminalControl(frame);
|
|
1377
|
+
if (entry.terminalView === undefined || entry.terminalView.viewId !== null)
|
|
1378
|
+
throw runtimeFailure("terminal_protocol_error", "Unexpected or duplicate terminal view start.");
|
|
1379
|
+
entry.terminalView.viewId = String(payload.viewId);
|
|
1380
|
+
if (this.#options.onTerminalViewStarted === undefined)
|
|
1381
|
+
throw runtimeFailure("terminal_protocol_error", "The terminal view has no reset consumer.");
|
|
1382
|
+
const authority = entry.outputAbort;
|
|
1383
|
+
const revision = entry.connectionRevision;
|
|
1384
|
+
try {
|
|
1385
|
+
const timeoutMs = this.#options.outputDeliveryTimeoutMs ?? 5_000;
|
|
1386
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000)
|
|
1387
|
+
throw runtimeFailure("terminal_protocol_error", "The terminal view reset deadline is invalid.");
|
|
1388
|
+
await withOutputDeadline(Promise.resolve(this.#options.onTerminalViewStarted({
|
|
1389
|
+
snapshot: snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock()),
|
|
1390
|
+
columns: Number(payload.columns), rows: Number(payload.rows), signal: authority.signal,
|
|
1391
|
+
})), timeoutMs);
|
|
1392
|
+
if (authority.signal.aborted || entry.connectionRevision !== revision || entry.state !== "active")
|
|
1393
|
+
throw runtimeFailure("terminal_disconnected", "Terminal view reset completed after its attachment retired.");
|
|
1394
|
+
}
|
|
1395
|
+
catch (error) {
|
|
1396
|
+
authority.abort(error);
|
|
1397
|
+
throw error;
|
|
1398
|
+
}
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (frame.type === "view_ready") {
|
|
1402
|
+
const payload = decodeTerminalControl(frame);
|
|
1403
|
+
if (entry.terminalView === undefined || entry.terminalView.viewId === null || entry.terminalView.ready ||
|
|
1404
|
+
payload.viewId !== entry.terminalView.viewId || entry.outputSequence < 1n ||
|
|
1405
|
+
payload.afterOutputSequence !== entry.outputSequence.toString())
|
|
1406
|
+
throw runtimeFailure("terminal_protocol_error", "Terminal view readiness does not match consumed output.");
|
|
1407
|
+
entry.terminalView.ready = true;
|
|
1408
|
+
if (entry.terminalViewDeadline !== undefined)
|
|
1409
|
+
clearTimeout(entry.terminalViewDeadline);
|
|
1410
|
+
this.#publish(entry);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (frame.type === "output") {
|
|
1414
|
+
if (entry.terminalView !== undefined && (entry.terminalView.viewId === null || frame.sequence !== entry.outputSequence + 1n))
|
|
1415
|
+
throw runtimeFailure("terminal_protocol_error", "Canonical terminal output is not contiguous within a started view.");
|
|
1416
|
+
if (frame.sequence <= entry.outputSequence) {
|
|
1417
|
+
throw runtimeFailure("terminal_protocol_error", "Terminal output sequence regressed or duplicated.");
|
|
1418
|
+
}
|
|
1419
|
+
if (this.#options.onTerminalOutput !== undefined) {
|
|
1420
|
+
const timeoutMs = this.#options.outputDeliveryTimeoutMs ?? 5_000;
|
|
1421
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000) {
|
|
1422
|
+
throw runtimeFailure("terminal_protocol_error", "The terminal output delivery deadline is invalid.");
|
|
1423
|
+
}
|
|
1424
|
+
await withOutputDeadline(Promise.resolve(this.#options.onTerminalOutput({
|
|
1425
|
+
provenance: (entry.terminalView === undefined ? entry.replayBoundaryObserved === true : entry.terminalView.ready) ? "live" : "replay_or_unknown",
|
|
1426
|
+
tabId: entry.tabId,
|
|
1427
|
+
agentSessionId: entry.observation.agentSessionId,
|
|
1428
|
+
binding: Object.freeze({
|
|
1429
|
+
userId: entry.observation.userId,
|
|
1430
|
+
machineId: entry.observation.machineId,
|
|
1431
|
+
agentSessionId: entry.observation.agentSessionId,
|
|
1432
|
+
processEpoch: entry.observation.processEpoch,
|
|
1433
|
+
fencingGeneration: entry.fencingGeneration,
|
|
1434
|
+
}),
|
|
1435
|
+
sequence: frame.sequence,
|
|
1436
|
+
bytes: frame.payload.slice(),
|
|
1437
|
+
signal: entry.outputAbort.signal,
|
|
1438
|
+
})), timeoutMs);
|
|
1439
|
+
}
|
|
1440
|
+
entry.outputSequence = frame.sequence;
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (frame.type === "ready") {
|
|
1444
|
+
const payload = decodeTerminalControl(frame);
|
|
1445
|
+
if (payload.protocol !== TERMINAL_PROTOCOL ||
|
|
1446
|
+
payload.agentSessionId !== entry.observation.agentSessionId ||
|
|
1447
|
+
payload.processEpoch !== entry.observation.processEpoch ||
|
|
1448
|
+
payload.fencingGeneration !== entry.fencingGeneration) {
|
|
1449
|
+
throw runtimeFailure("grant_scope_mismatch", "Terminal readiness evidence targets another AgentSession generation.");
|
|
1450
|
+
}
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (frame.type === "writer_epoch") {
|
|
1454
|
+
// The writer seat moved. The server names the new epoch, its holder and
|
|
1455
|
+
// this attachment's own mode; the local view follows and says so.
|
|
1456
|
+
const payload = decodeTerminalControl(frame);
|
|
1457
|
+
if (payload.accessMode === "writer" && payload.writerClientInstanceId !== this.#options.clientInstanceId) {
|
|
1458
|
+
throw runtimeFailure("terminal_protocol_error", "The writer notice names another client.");
|
|
1459
|
+
}
|
|
1460
|
+
// Compare wire facts with wire facts. An HTTP response can advance the
|
|
1461
|
+
// request fence, but cannot grant input or contradict a later wire role.
|
|
1462
|
+
if (entry.wireWriterNotice?.writerEpoch === payload.writerEpoch && (entry.wireWriterNotice.accessMode !== payload.accessMode || entry.wireWriterNotice.writerClientInstanceId !== payload.writerClientInstanceId)) {
|
|
1463
|
+
throw runtimeFailure("terminal_protocol_error", "The writer notice contradicts the same wire epoch.");
|
|
1464
|
+
}
|
|
1465
|
+
if (payload.writerEpoch < entry.writerEpoch) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
entry.wireWriterNotice = payload;
|
|
1469
|
+
if (payload.writerEpoch !== entry.writerEpoch) {
|
|
1470
|
+
retireInputAcceptance(entry);
|
|
1471
|
+
entry.geometry = null;
|
|
1472
|
+
}
|
|
1473
|
+
const wasWriter = entry.accessMode === "writer";
|
|
1474
|
+
entry.accessMode = payload.accessMode;
|
|
1475
|
+
entry.writerEpoch = payload.writerEpoch;
|
|
1476
|
+
entry.writerClientInstanceId = payload.writerClientInstanceId;
|
|
1477
|
+
if (entry.writerTransfer !== undefined && payload.writerEpoch > entry.writerTransfer.expectedWriterEpoch) {
|
|
1478
|
+
delete entry.writerTransfer;
|
|
1479
|
+
}
|
|
1480
|
+
if (payload.accessMode === "writer")
|
|
1481
|
+
entry.heldSeat = true;
|
|
1482
|
+
if (wasWriter && payload.accessMode !== "writer")
|
|
1483
|
+
entry.reason = "writer_transferred";
|
|
1484
|
+
else if (!wasWriter && payload.accessMode === "writer")
|
|
1485
|
+
delete entry.reason;
|
|
1486
|
+
this.#publish(entry);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (frame.type === "acknowledgement") {
|
|
1490
|
+
const payload = decodeTerminalControl(frame);
|
|
1491
|
+
const acknowledged = BigInt(String(payload.clientSequence));
|
|
1492
|
+
if (acknowledged <= 0n || payload.meaning !== "durably_accepted_not_executed") {
|
|
1493
|
+
throw runtimeFailure("terminal_protocol_error", "Terminal input acknowledgement is invalid.");
|
|
1494
|
+
}
|
|
1495
|
+
// Sequences remain monotone across this runtime's reconnects. Late
|
|
1496
|
+
// receipts for retired scopes never certify or disrupt the current one.
|
|
1497
|
+
if (acknowledged <= entry.retiredInputSequence)
|
|
1498
|
+
return;
|
|
1499
|
+
if (acknowledged <= entry.acknowledgedInputSequence ||
|
|
1500
|
+
!entry.pendingInputSequences.has(acknowledged) ||
|
|
1501
|
+
payload.meaning !== "durably_accepted_not_executed") {
|
|
1502
|
+
throw runtimeFailure("terminal_protocol_error", "Terminal input acknowledgement is invalid.");
|
|
1503
|
+
}
|
|
1504
|
+
for (const sequence of entry.pendingInputSequences) {
|
|
1505
|
+
if (sequence <= acknowledged)
|
|
1506
|
+
entry.pendingInputSequences.delete(sequence);
|
|
1507
|
+
}
|
|
1508
|
+
entry.acknowledgedInputSequence = acknowledged;
|
|
1509
|
+
entry.inputContinuity = entry.pendingInputSequences.size === 0 && !entry.historicalInputUncertainty ? "complete" : "uncertain";
|
|
1510
|
+
this.#publish(entry);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
if (frame.type === "exit") {
|
|
1514
|
+
decodeTerminalControl(frame);
|
|
1515
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
1516
|
+
entry.outputAbort.abort(runtimeFailure("terminal_disconnected", "The remote terminal process exited."));
|
|
1517
|
+
entry.state = "closed";
|
|
1518
|
+
entry.reason = "remote_process_exit";
|
|
1519
|
+
try {
|
|
1520
|
+
this.#views.detach(entry.viewId);
|
|
1521
|
+
}
|
|
1522
|
+
catch { /* the view may already be detached */ }
|
|
1523
|
+
this.#publish(entry);
|
|
1524
|
+
await entry.connection.close({ code: 1000, reason: "cuna_remote_process_exit" });
|
|
1525
|
+
if (this.#activeTabId === entry.tabId)
|
|
1526
|
+
this.#activeTabId = this.#nextActiveTab(entry.tabId);
|
|
1527
|
+
this.#terminals.delete(entry.tabId);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
if (frame.type === "error")
|
|
1531
|
+
throw this.#remoteTerminalError(frame, entry.observation.agentSessionId);
|
|
1532
|
+
if (frame.type === "heartbeat") {
|
|
1533
|
+
decodeTerminalControl(frame);
|
|
1534
|
+
if (frame.sequence <= entry.heartbeatSequence) {
|
|
1535
|
+
throw runtimeFailure("terminal_protocol_error", "Terminal heartbeat sequence regressed or duplicated.");
|
|
1536
|
+
}
|
|
1537
|
+
if (this.#clock() - entry.lastHeartbeatAt > this.#heartbeatTimeoutMs()) {
|
|
1538
|
+
this.#expireHeartbeat(entry, entry.connection, entry.connectionRevision);
|
|
1539
|
+
throw runtimeFailure("terminal_disconnected", "A late terminal heartbeat cannot revive an expired attachment.", { retryable: true });
|
|
1540
|
+
}
|
|
1541
|
+
entry.heartbeatSequence = frame.sequence;
|
|
1542
|
+
// The producer emits this exact sequence only after RESUME replay. A
|
|
1543
|
+
// heartbeat request cannot reuse it because client sequences increase.
|
|
1544
|
+
if (frame.sequence === entry.replayBoundarySequence)
|
|
1545
|
+
entry.replayBoundaryObserved = true;
|
|
1546
|
+
entry.lastHeartbeatAt = this.#clock();
|
|
1547
|
+
this.#scheduleHeartbeatWatchdog(entry, entry.connection, entry.connectionRevision);
|
|
1548
|
+
this.#publish(entry);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
#remoteTerminalError(frame, agentSessionId) {
|
|
1552
|
+
const payload = decodeTerminalControl(frame);
|
|
1553
|
+
if (payload.code === "continuity_incomplete" && payload.safeReason === "retained_output_gap") {
|
|
1554
|
+
return terminalHistoryGap(agentSessionId);
|
|
1555
|
+
}
|
|
1556
|
+
return runtimeFailure("terminal_protocol_error", payload.code === "terminal_input_recovery_required"
|
|
1557
|
+
? "Terminal input requires recovery. Reconnecting cannot confirm earlier input delivery."
|
|
1558
|
+
: payload.code === "opencode_server_exited"
|
|
1559
|
+
? "OpenCode's server stopped. Inspect this session before starting another session."
|
|
1560
|
+
: "The Cuna terminal gateway rejected the connection.", {
|
|
1561
|
+
retryable: payload.code !== "opencode_server_exited" && payload.retryable === true,
|
|
1562
|
+
safeDetails: { reason: typeof payload.code === "string" ? payload.code : "terminal_error" },
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
#publish(entry) {
|
|
1566
|
+
const now = this.#clock();
|
|
1567
|
+
if (!this.#closed && entry.state === "active" && entry.capabilityRefresh === undefined &&
|
|
1568
|
+
now >= (entry.nextCapabilityRefreshAt ?? 0) && now >= Date.parse(entry.capabilitySnapshot.expiresAt) - 5_000) {
|
|
1569
|
+
const revision = entry.connectionRevision;
|
|
1570
|
+
const capabilityReadRevision = ++entry.capabilityReadRevision;
|
|
1571
|
+
entry.nextCapabilityRefreshAt = now + 5_000;
|
|
1572
|
+
entry.capabilityRefresh = this.#options.controlPlane.discoverCapabilities("agent_session", entry.observation.agentSessionId, AbortSignal.timeout(5_000)).then(evidence => {
|
|
1573
|
+
if (!this.#closed && entry.state === "active" && entry.connectionRevision === revision && entry.capabilityReadRevision === capabilityReadRevision) {
|
|
1574
|
+
entry.capabilitySnapshot = evidence;
|
|
1575
|
+
this.#publish(entry);
|
|
1576
|
+
}
|
|
1577
|
+
}).catch(() => { }).finally(() => { delete entry.capabilityRefresh; });
|
|
1578
|
+
}
|
|
1579
|
+
this.#options.onTerminalState?.(snapshot(entry, this.#heartbeatTimeoutMs(), this.#clock()));
|
|
1580
|
+
}
|
|
1581
|
+
#localActionAcceptance(offer, agentSessionId) {
|
|
1582
|
+
if (this.#options.onLocalActionFrame === undefined)
|
|
1583
|
+
return undefined;
|
|
1584
|
+
const configured = typeof this.#options.localActionKinds === "function"
|
|
1585
|
+
? this.#options.localActionKinds(agentSessionId)
|
|
1586
|
+
: this.#options.localActionKinds ?? [];
|
|
1587
|
+
const valid = configured.filter((kind) => TERMINAL_LOCAL_ACTION_KINDS.includes(kind));
|
|
1588
|
+
return negotiateTerminalLocalActions(offer, new Set(valid));
|
|
1589
|
+
}
|
|
1590
|
+
#requireTerminal(tabId) {
|
|
1591
|
+
const entry = this.#terminals.get(tabId);
|
|
1592
|
+
if (entry === undefined)
|
|
1593
|
+
throw runtimeFailure("session_unknown", "The local terminal tab does not exist.");
|
|
1594
|
+
return entry;
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Only the writing seat may reach the PTY: input, resize and signals. The
|
|
1598
|
+
* gateway closes an observer's attachment on the first such frame
|
|
1599
|
+
* (`observer_control_rejected`), so the refusal must happen here, by name,
|
|
1600
|
+
* before anything is sent. An observer tab keeps observing.
|
|
1601
|
+
*/
|
|
1602
|
+
#requireWriterSeat(entry) {
|
|
1603
|
+
if (entry.accessMode !== "writer" || entry.writerClientInstanceId !== this.#options.clientInstanceId) {
|
|
1604
|
+
throw runtimeFailure("terminal_observer", "This attachment observes the terminal; input is disabled.");
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
#requireActiveTerminal(tabId) {
|
|
1608
|
+
if (tabId === undefined)
|
|
1609
|
+
throw runtimeFailure("session_unknown", "No terminal tab is active.");
|
|
1610
|
+
const entry = this.#requireTerminal(tabId);
|
|
1611
|
+
if (entry.state !== "active")
|
|
1612
|
+
throw runtimeFailure("terminal_disconnected", "The terminal tab is not connected.");
|
|
1613
|
+
this.#assertHeartbeatFresh(entry);
|
|
1614
|
+
return entry;
|
|
1615
|
+
}
|
|
1616
|
+
#heartbeatTimeoutMs() {
|
|
1617
|
+
const timeoutMs = this.#options.heartbeatTimeoutMs ?? 45_000;
|
|
1618
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 120_000) {
|
|
1619
|
+
throw runtimeFailure("terminal_protocol_error", "The terminal heartbeat deadline is invalid.");
|
|
1620
|
+
}
|
|
1621
|
+
return timeoutMs;
|
|
1622
|
+
}
|
|
1623
|
+
#assertHeartbeatFresh(entry) {
|
|
1624
|
+
if (this.#clock() - entry.lastHeartbeatAt <= this.#heartbeatTimeoutMs())
|
|
1625
|
+
return;
|
|
1626
|
+
this.#expireHeartbeat(entry, entry.connection, entry.connectionRevision);
|
|
1627
|
+
throw runtimeFailure("terminal_disconnected", "The terminal heartbeat expired; input is fenced until a fresh attachment is proven.", { retryable: true });
|
|
1628
|
+
}
|
|
1629
|
+
#scheduleHeartbeatWatchdog(entry, connection, connectionRevision) {
|
|
1630
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
1631
|
+
const interval = Math.max(250, Math.min(10_000, Math.floor(this.#heartbeatTimeoutMs() / 3)));
|
|
1632
|
+
entry.heartbeatTimer = setInterval(() => {
|
|
1633
|
+
if (entry.connection !== connection ||
|
|
1634
|
+
entry.connectionRevision !== connectionRevision ||
|
|
1635
|
+
entry.state !== "active") {
|
|
1636
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (this.#clock() - entry.lastHeartbeatAt > this.#heartbeatTimeoutMs()) {
|
|
1640
|
+
this.#expireHeartbeat(entry, connection, connectionRevision);
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
if (entry.heartbeatSendPending)
|
|
1644
|
+
return;
|
|
1645
|
+
entry.heartbeatSendPending = true;
|
|
1646
|
+
void this.#enqueueTerminalSend(entry, async (authority) => {
|
|
1647
|
+
entry.wireSequence += 1n;
|
|
1648
|
+
await authority.connection.send(encodeTerminalControl("heartbeat", entry.wireSequence, {}));
|
|
1649
|
+
}).catch((error) => {
|
|
1650
|
+
if (entry.connection !== connection ||
|
|
1651
|
+
entry.connectionRevision !== connectionRevision ||
|
|
1652
|
+
entry.state !== "active")
|
|
1653
|
+
return;
|
|
1654
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
1655
|
+
entry.outputAbort.abort(error);
|
|
1656
|
+
entry.state = "interrupted";
|
|
1657
|
+
retireInputAcceptance(entry);
|
|
1658
|
+
entry.outputContinuity = "unknown";
|
|
1659
|
+
entry.reason = "heartbeat_send_failed";
|
|
1660
|
+
this.#publish(entry);
|
|
1661
|
+
void connection.close({ code: 1001, reason: "cuna_heartbeat_send_failed" }).catch(() => undefined);
|
|
1662
|
+
}).finally(() => {
|
|
1663
|
+
entry.heartbeatSendPending = false;
|
|
1664
|
+
});
|
|
1665
|
+
}, interval);
|
|
1666
|
+
entry.heartbeatTimer.unref();
|
|
1667
|
+
}
|
|
1668
|
+
#clearHeartbeatWatchdog(entry) {
|
|
1669
|
+
if (entry.heartbeatTimer !== undefined)
|
|
1670
|
+
clearTimeout(entry.heartbeatTimer);
|
|
1671
|
+
delete entry.heartbeatTimer;
|
|
1672
|
+
}
|
|
1673
|
+
#expireHeartbeat(entry, connection, connectionRevision) {
|
|
1674
|
+
if (entry.connection !== connection ||
|
|
1675
|
+
entry.connectionRevision !== connectionRevision ||
|
|
1676
|
+
entry.state !== "active")
|
|
1677
|
+
return;
|
|
1678
|
+
this.#clearHeartbeatWatchdog(entry);
|
|
1679
|
+
entry.outputAbort.abort(runtimeFailure("terminal_disconnected", "The terminal heartbeat expired."));
|
|
1680
|
+
entry.state = "interrupted";
|
|
1681
|
+
retireInputAcceptance(entry);
|
|
1682
|
+
entry.outputContinuity = "unknown";
|
|
1683
|
+
entry.reason = "heartbeat_expired";
|
|
1684
|
+
this.#publish(entry);
|
|
1685
|
+
void connection.close({ code: 1001, reason: "cuna_heartbeat_expired" }).catch(() => undefined);
|
|
1686
|
+
}
|
|
1687
|
+
#nextActiveTab(excluding) {
|
|
1688
|
+
return [...this.#terminals.values()].find((entry) => entry.tabId !== excluding && entry.state === "active")?.tabId;
|
|
1689
|
+
}
|
|
1690
|
+
#assertOpen() {
|
|
1691
|
+
if (this.#closed)
|
|
1692
|
+
throw runtimeFailure("runtime_closed", "The local runtime has already stopped.");
|
|
1693
|
+
}
|
|
1694
|
+
#assertReady() {
|
|
1695
|
+
this.#assertOpen();
|
|
1696
|
+
if (this.#mode === "foreground") {
|
|
1697
|
+
if (this.#foreground.state !== "ready") {
|
|
1698
|
+
throw runtimeFailure("remote_state_unproven", "The foreground runtime is not ready in this process.");
|
|
1699
|
+
}
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
if (this.#daemon.snapshot().state === "ready" &&
|
|
1703
|
+
this.#startupEvidenceExpiresAt <= this.#clock()) {
|
|
1704
|
+
this.#daemon.transition("degraded", "startup_evidence_expired", this.#clock());
|
|
1705
|
+
this.#daemon.transition("recovery_required", "runtime_evidence_refresh_required", this.#clock());
|
|
1706
|
+
}
|
|
1707
|
+
if (this.#daemon.snapshot().state !== "ready") {
|
|
1708
|
+
throw runtimeFailure("remote_state_unproven", "The local runtime is not in a verified ready state.");
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
async function withOutputDeadline(operation, timeoutMs, timeoutFailure = () => runtimeFailure("terminal_protocol_error", "The terminal output consumer exceeded its bounded deadline.")) {
|
|
1713
|
+
let timeout;
|
|
1714
|
+
try {
|
|
1715
|
+
return await Promise.race([
|
|
1716
|
+
operation,
|
|
1717
|
+
new Promise((_resolve, reject) => {
|
|
1718
|
+
timeout = setTimeout(() => reject(timeoutFailure()), timeoutMs);
|
|
1719
|
+
}),
|
|
1720
|
+
]);
|
|
1721
|
+
}
|
|
1722
|
+
finally {
|
|
1723
|
+
if (timeout !== undefined)
|
|
1724
|
+
clearTimeout(timeout);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* The server's own name for "the writing seat is not yours to take right now":
|
|
1729
|
+
* held by another client (`terminal_writer_held`) or moved past the epoch this
|
|
1730
|
+
* client expected (`terminal_writer_stale`). The reason travels in the safe
|
|
1731
|
+
* error details the API transport keeps for every refusal.
|
|
1732
|
+
*/
|
|
1733
|
+
function writerSeatUnavailable(error) {
|
|
1734
|
+
if (typeof error !== "object" || error === null)
|
|
1735
|
+
return false;
|
|
1736
|
+
const reason = error.details?.reason;
|
|
1737
|
+
return reason === "terminal_writer_held" || reason === "terminal_writer_stale";
|
|
1738
|
+
}
|
|
1739
|
+
function snapshot(entry, heartbeatTimeoutMs = 45_000, now = Date.now()) {
|
|
1740
|
+
return Object.freeze({
|
|
1741
|
+
tabId: entry.tabId,
|
|
1742
|
+
viewId: entry.viewId,
|
|
1743
|
+
...(entry.terminalView === undefined ? {} : { terminalView: Object.freeze({ ...entry.terminalView }) }),
|
|
1744
|
+
userId: entry.observation.userId,
|
|
1745
|
+
machineId: entry.observation.machineId,
|
|
1746
|
+
workspaceBindingId: entry.observation.workspaceBindingId,
|
|
1747
|
+
workspaceBindingGeneration: entry.observation.workspaceBindingGeneration,
|
|
1748
|
+
agentSessionId: entry.observation.agentSessionId,
|
|
1749
|
+
processEpoch: entry.observation.processEpoch,
|
|
1750
|
+
state: entry.state,
|
|
1751
|
+
fencingGeneration: entry.fencingGeneration,
|
|
1752
|
+
inputSequence: entry.inputSequence,
|
|
1753
|
+
acknowledgedInputSequence: entry.acknowledgedInputSequence,
|
|
1754
|
+
inputContinuity: entry.inputContinuity,
|
|
1755
|
+
historicalInputUncertainty: entry.historicalInputUncertainty,
|
|
1756
|
+
outputSequence: entry.outputSequence,
|
|
1757
|
+
outputContinuity: entry.outputContinuity,
|
|
1758
|
+
resizeCapability: entry.resizeCapability,
|
|
1759
|
+
accessMode: entry.accessMode,
|
|
1760
|
+
writerEpoch: entry.writerEpoch,
|
|
1761
|
+
writerClientInstanceId: entry.writerClientInstanceId,
|
|
1762
|
+
geometry: entry.geometry,
|
|
1763
|
+
writerTransferCapability: writerTransferCapability(entry.capabilitySnapshot, entry.observation.agentSessionId, now),
|
|
1764
|
+
heartbeatObservedAt: entry.lastHeartbeatAt,
|
|
1765
|
+
heartbeatExpiresAt: entry.lastHeartbeatAt + heartbeatTimeoutMs,
|
|
1766
|
+
...(entry.reason === undefined ? {} : { reason: entry.reason }),
|
|
1767
|
+
...(entry.remoteReason === undefined ? {} : { remoteReason: entry.remoteReason }),
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* The remote side's own code for a failure, only when it is a bounded
|
|
1772
|
+
* identifier: `#remoteTerminalError` stores the ERROR frame's `code` under
|
|
1773
|
+
* `safeDetails.reason`, and nothing else is ever rendered from a remote frame.
|
|
1774
|
+
*/
|
|
1775
|
+
function remoteSafeReason(error) {
|
|
1776
|
+
if (!(error instanceof RuntimeBoundaryError))
|
|
1777
|
+
return undefined;
|
|
1778
|
+
const reason = error.safeDetails?.reason;
|
|
1779
|
+
return typeof reason === "string" && reason !== error.code && /^[a-z][a-z0-9_.]{0,63}$/u.test(reason) ? reason : undefined;
|
|
1780
|
+
}
|
|
1781
|
+
function sameEntryBinding(entry, binding) {
|
|
1782
|
+
return entry.observation.userId === binding.userId &&
|
|
1783
|
+
entry.observation.machineId === binding.machineId &&
|
|
1784
|
+
entry.observation.agentSessionId === binding.agentSessionId &&
|
|
1785
|
+
entry.observation.processEpoch === binding.processEpoch &&
|
|
1786
|
+
entry.fencingGeneration === binding.fencingGeneration;
|
|
1787
|
+
}
|
|
1788
|
+
async function nextWithTimeout(iterator, timeoutMs, signal) {
|
|
1789
|
+
let timeout;
|
|
1790
|
+
let removeAbort = () => undefined;
|
|
1791
|
+
try {
|
|
1792
|
+
return await Promise.race([
|
|
1793
|
+
iterator.next(),
|
|
1794
|
+
new Promise((_resolve, reject) => {
|
|
1795
|
+
timeout = setTimeout(() => reject(runtimeFailure("terminal_timeout", "The terminal readiness handshake timed out.", { retryable: true })), Math.max(1, timeoutMs));
|
|
1796
|
+
}),
|
|
1797
|
+
new Promise((_resolve, reject) => {
|
|
1798
|
+
if (signal === undefined)
|
|
1799
|
+
return;
|
|
1800
|
+
const abort = () => reject(runtimeFailure("terminal_disconnected", "Terminal attachment was cancelled.", { cause: signal.reason }));
|
|
1801
|
+
if (signal.aborted)
|
|
1802
|
+
abort();
|
|
1803
|
+
else {
|
|
1804
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1805
|
+
removeAbort = () => signal.removeEventListener("abort", abort);
|
|
1806
|
+
}
|
|
1807
|
+
}),
|
|
1808
|
+
]);
|
|
1809
|
+
}
|
|
1810
|
+
finally {
|
|
1811
|
+
if (timeout !== undefined)
|
|
1812
|
+
clearTimeout(timeout);
|
|
1813
|
+
removeAbort();
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function retireInputAcceptance(entry) {
|
|
1817
|
+
entry.historicalInputUncertainty ||= entry.pendingInputSequences.size > 0;
|
|
1818
|
+
if (entry.inputSequence > entry.retiredInputSequence)
|
|
1819
|
+
entry.retiredInputSequence = entry.inputSequence;
|
|
1820
|
+
entry.pendingInputSequences.clear();
|
|
1821
|
+
if (entry.historicalInputUncertainty)
|
|
1822
|
+
entry.inputContinuity = "uncertain";
|
|
1823
|
+
}
|
|
1824
|
+
function isPermanentInputRecoveryFailure(error) {
|
|
1825
|
+
return error instanceof RuntimeBoundaryError && error.code === "terminal_protocol_error" &&
|
|
1826
|
+
!error.retryable && error.safeDetails?.reason === "terminal_input_recovery_required";
|
|
1827
|
+
}
|
|
1828
|
+
function isHistoryGap(error) {
|
|
1829
|
+
return error instanceof RuntimeBoundaryError && error.code === "terminal_history_gap";
|
|
1830
|
+
}
|
|
1831
|
+
function safeReason(error) {
|
|
1832
|
+
if (isPermanentInputRecoveryFailure(error))
|
|
1833
|
+
return "terminal_input_recovery_required";
|
|
1834
|
+
if (error instanceof TerminalProtocolError)
|
|
1835
|
+
return "terminal_protocol_error";
|
|
1836
|
+
if (error instanceof RuntimeBoundaryError)
|
|
1837
|
+
return error.code;
|
|
1838
|
+
return "transport_failure";
|
|
1839
|
+
}
|
|
1840
|
+
function throwIfAborted(signal, message) {
|
|
1841
|
+
if (signal.aborted) {
|
|
1842
|
+
throw runtimeFailure("terminal_disconnected", message, { retryable: false, cause: signal.reason });
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
function viewId(tabId, generation) {
|
|
1846
|
+
return `${tabId}:attachment:${generation}`;
|
|
1847
|
+
}
|
|
1848
|
+
function assertIdentifier(value, label) {
|
|
1849
|
+
if (!/^[A-Za-z0-9._:-]{1,256}$/u.test(value)) {
|
|
1850
|
+
throw runtimeFailure("session_conflict", `The ${label} is invalid.`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
function assertDimensions(columns, rows) {
|
|
1854
|
+
if (!Number.isSafeInteger(columns) || !Number.isSafeInteger(rows) || columns < 1 || rows < 1 || columns > 1000 || rows > 1000) {
|
|
1855
|
+
throw runtimeFailure("terminal_protocol_error", "The terminal dimensions are outside protocol bounds.");
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
//# sourceMappingURL=boundary.js.map
|