@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,2814 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { requireCapability, } from "../api/client.js";
|
|
3
|
+
import { ARTIFACT_CHANNEL, packageBuildDigest, PROTOCOL_RANGE } from "../build-identity.js";
|
|
4
|
+
import { DEFAULT_BASE_URL, environmentCredentialState, publicConfig } from "../config/config.js";
|
|
5
|
+
import { EXIT_CODES, CunaError, unsupportedError, usageError } from "../core/errors.js";
|
|
6
|
+
import { REMOTE_CONVERGENCE_BUDGET_MS, REMOTE_CONVERGENCE_POLL_INTERVAL_MS, observationBudgetElapsed, } from "../core/observation-budget.js";
|
|
7
|
+
import { OFF_CONTRACT_RESPONSE_HINT, SUPPORT_URL, automationCredentialHint } from "../core/product-web.js";
|
|
8
|
+
import { assertCanonicalUuid, assertIdempotencyKey, assertMachineId, assertPublicId, assertSafeDisplayText, ContractViolation, integerArgument, } from "../core/validation.js";
|
|
9
|
+
import { preflightAgentJourneyInvocation } from "../journey/intent.js";
|
|
10
|
+
import { listAllMachines } from "../machines/pagination.js";
|
|
11
|
+
import { classifyLiveSupervisorUpdateFailure, liveSupervisorInstallationEvidenceLabel, liveSupervisorInstallerOutcomeLabel, liveSupervisorInstallerReachLines, liveSupervisorSessionOutcomeLabel, liveSupervisorUpdateNotes, liveSupervisorUpdatePhaseLabel, liveSupervisorUpdateRecordSurvives, readLiveSupervisorUpdateOperation, summarizeLiveSupervisorUpdate, } from "../machines/live-supervisor-update.js";
|
|
12
|
+
import { isOpenCodeRuntimeUnverifiedCapabilityRejection, isOpenCodeSupervisorRepairCapabilityRejection, isOpenCodeSupervisorUpgradeCapabilityRejection, openCodeRuntimeUnverified, openCodeSupervisorUpgradeRequired, } from "../machines/opencode-supervisor.js";
|
|
13
|
+
import { machineProviderAvailability, machineSupportsProvider, providerDisplayName, providerVerdict, } from "../machines/provider-availability.js";
|
|
14
|
+
import { classifySessionActionability, displaySessionActionability } from "../machines/session-actionability.js";
|
|
15
|
+
import { agentSessionProcessObservation, AGENT_SESSION_OBSERVATION_NOTE, isAgentSessionIntendedActive, } from "../machines/session-visibility.js";
|
|
16
|
+
import { loadWorkspaceBindingIntent } from "../workspace/binding-store.js";
|
|
17
|
+
import { INITIAL_RUNTIME_GATES } from "../runtime/contracts.js";
|
|
18
|
+
import { evaluateRuntimeSupport } from "../platform/support.js";
|
|
19
|
+
import { CLI_VERSION } from "../version.js";
|
|
20
|
+
import { assertRegisteredCliRoute, booleanOption, rejectUnknownOptions, stringOption, } from "../cli/parser.js";
|
|
21
|
+
function productionConvergencePoller() {
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
now: () => Date.now(),
|
|
24
|
+
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Read back until an accepted mutation is visible, or until OUR budget elapses.
|
|
29
|
+
*
|
|
30
|
+
* D2, and the whole difference between the two answers this fixes. A single
|
|
31
|
+
* immediate read cannot distinguish "the change did not happen" from "the change
|
|
32
|
+
* has not arrived yet", and the CLI was reporting the first for the second with
|
|
33
|
+
* `retryable: false` — telling the user not to retry AND implying the mutation
|
|
34
|
+
* had not landed, both false, for a deletion that had already succeeded.
|
|
35
|
+
*
|
|
36
|
+
* On elapse this raises the budget refusal from `core/observation-budget.ts`,
|
|
37
|
+
* which is retryable and names the read-only command that settles it. It is NOT
|
|
38
|
+
* `postcondition_unverified`: that code is reserved for an observation that
|
|
39
|
+
* CONTRADICTS the write and that no amount of waiting repairs.
|
|
40
|
+
*/
|
|
41
|
+
async function convergeOnRemoteState(context, input) {
|
|
42
|
+
const poller = context.convergencePoller ?? productionConvergencePoller();
|
|
43
|
+
const deadline = poller.now() + REMOTE_CONVERGENCE_BUDGET_MS;
|
|
44
|
+
let probe = await input.probe();
|
|
45
|
+
while (!probe.settled) {
|
|
46
|
+
const remaining = deadline - poller.now();
|
|
47
|
+
if (remaining <= 0) {
|
|
48
|
+
throw observationBudgetElapsed({
|
|
49
|
+
kind: "convergence",
|
|
50
|
+
operation: input.operation,
|
|
51
|
+
settleWith: input.settleWith,
|
|
52
|
+
budgetMs: REMOTE_CONVERGENCE_BUDGET_MS,
|
|
53
|
+
details: probe.details,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
await poller.sleep(Math.min(REMOTE_CONVERGENCE_POLL_INTERVAL_MS, remaining));
|
|
57
|
+
probe = await input.probe();
|
|
58
|
+
}
|
|
59
|
+
return probe.observation;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The process states in which the child is no longer running. The producer
|
|
63
|
+
* chooses among all three: a signalled exit, an exit of the process's own
|
|
64
|
+
* accord, and an ending it could not classify.
|
|
65
|
+
*/
|
|
66
|
+
const AGENT_SESSION_ENDED_PROCESS_STATES = new Set(["terminated", "exited", "failed"]);
|
|
67
|
+
/**
|
|
68
|
+
* Whether a termination has actually landed.
|
|
69
|
+
*
|
|
70
|
+
* Matching one value of a vocabulary the producer chooses from cannot settle:
|
|
71
|
+
* any ended process state satisfies this. The conjunction over the other two
|
|
72
|
+
* fields stays, so a process that merely died on its own is not read as a
|
|
73
|
+
* completed termination.
|
|
74
|
+
*/
|
|
75
|
+
function agentSessionTerminationConfirmed(session) {
|
|
76
|
+
return session.desiredState === "terminated" &&
|
|
77
|
+
session.requestState === "terminal" &&
|
|
78
|
+
AGENT_SESSION_ENDED_PROCESS_STATES.has(session.processState);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* An AgentSession carries three states and this CLI's own invariants are
|
|
82
|
+
* conjunctions of all three — see `agentSessionTerminationConfirmed` above, and
|
|
83
|
+
* the termination timeout, which reports all three because one is not enough to
|
|
84
|
+
* explain anything.
|
|
85
|
+
*
|
|
86
|
+
* The human rendering used to print `processState` alone, so a session that was
|
|
87
|
+
* `terminated / termination_pending / running` displayed as plain `running`:
|
|
88
|
+
* exactly the disagreement a person needs to see, replaced by the one word that
|
|
89
|
+
* hides it. Settled sessions still show a single word, because a triple on every
|
|
90
|
+
* healthy row is noise; anything unsettled shows all three, in the fixed order
|
|
91
|
+
* desired/request/process.
|
|
92
|
+
*
|
|
93
|
+
* The provenance of `processState` is the second thing this line hid. Every
|
|
94
|
+
* authority above is a CLAIM, and the producer publishes who established the
|
|
95
|
+
* process one; a row whose runtime lease kept moving while nothing observed the
|
|
96
|
+
* child printed the byte-identical line to a row a supervisor had just looked
|
|
97
|
+
* at. So whenever the state asserts a live process, the note from
|
|
98
|
+
* `session-visibility.ts` is printed beside it — `observed` adds nothing, which
|
|
99
|
+
* is why it has no note. It qualifies the state and never replaces it: a
|
|
100
|
+
* confirmed termination returns above this and can never acquire one.
|
|
101
|
+
*/
|
|
102
|
+
function agentSessionStateLabel(session) {
|
|
103
|
+
if (agentSessionTerminationConfirmed(session))
|
|
104
|
+
return "terminated";
|
|
105
|
+
const note = session.processState === "running"
|
|
106
|
+
? AGENT_SESSION_OBSERVATION_NOTE[agentSessionProcessObservation(session)]
|
|
107
|
+
: undefined;
|
|
108
|
+
const qualifier = note === undefined ? "" : ` (${note})`;
|
|
109
|
+
if (session.desiredState === "running" &&
|
|
110
|
+
session.requestState === "launched" &&
|
|
111
|
+
session.processState === "running") {
|
|
112
|
+
return `running${qualifier}`;
|
|
113
|
+
}
|
|
114
|
+
return `${session.desiredState}/${session.requestState}/${session.processState}${qualifier}`;
|
|
115
|
+
}
|
|
116
|
+
function requireCredential(context) {
|
|
117
|
+
if (context.credentialMode !== undefined)
|
|
118
|
+
return;
|
|
119
|
+
throw new CunaError({
|
|
120
|
+
code: "cuna.auth.required",
|
|
121
|
+
message: "This command requires a Cuna credential.",
|
|
122
|
+
exitCode: EXIT_CODES.auth,
|
|
123
|
+
hint: `Run \`cuna login\` for interactive use, or use an automation credential. ${automationCredentialHint()}`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function requireOperand(operands, index, label) {
|
|
127
|
+
const value = operands[index];
|
|
128
|
+
if (value === undefined)
|
|
129
|
+
throw usageError(`Missing ${label}.`);
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A required option's value, or a refusal that says the option is missing.
|
|
134
|
+
* Absent and malformed are different mistakes: reading an absent option as `""`
|
|
135
|
+
* and handing it to a shape validator reports the wrong one.
|
|
136
|
+
*/
|
|
137
|
+
/**
|
|
138
|
+
* The workspace binding this folder is already bound to, for the account this
|
|
139
|
+
* invocation authenticates as. Read-only: creating a binding means choosing a
|
|
140
|
+
* local root, an exclusion policy and a project identity, which are the
|
|
141
|
+
* journey's decisions, so an unbound folder is refused rather than bound here.
|
|
142
|
+
*/
|
|
143
|
+
async function resolveLocalWorkspaceBinding(context) {
|
|
144
|
+
const identity = await context.client.getIdentity();
|
|
145
|
+
const workspaceId = identity.workspaceId;
|
|
146
|
+
const unbound = (reason, hint) => new CunaError({
|
|
147
|
+
code: "cuna.workspace.binding_required",
|
|
148
|
+
message: "This folder is not bound to a Cuna Machine.",
|
|
149
|
+
exitCode: EXIT_CODES.usage,
|
|
150
|
+
hint,
|
|
151
|
+
details: { reason, folder: context.workspaceRoot ?? "." },
|
|
152
|
+
});
|
|
153
|
+
if (workspaceId === undefined) {
|
|
154
|
+
throw unbound("workspace_unassigned", "No Cuna workspace is assigned to this account yet. Run `cuna workspace show` to see where you stand.");
|
|
155
|
+
}
|
|
156
|
+
const loaded = await loadWorkspaceBindingIntent({
|
|
157
|
+
startPath: context.workspaceRoot ?? ".",
|
|
158
|
+
profileId: context.config.profile,
|
|
159
|
+
userId: identity.id,
|
|
160
|
+
workspaceId,
|
|
161
|
+
});
|
|
162
|
+
if (loaded === undefined) {
|
|
163
|
+
throw unbound("folder_not_bound", "Run `cuna opencode` (or `cuna claude`, `cuna codex`) in this folder once: that binds the folder to a Machine and records the workspace binding this command needs. Or pass --workspace-binding-id and --workspace-generation yourself.");
|
|
164
|
+
}
|
|
165
|
+
const record = loaded.record;
|
|
166
|
+
// A folder that has moved is bound to a path that is no longer this one. The
|
|
167
|
+
// journey knows how to relocate it; a batch mutation must not pretend the
|
|
168
|
+
// binding still describes where it is running.
|
|
169
|
+
if (loaded.relocationRequired) {
|
|
170
|
+
throw unbound("relocation_required", "This folder has moved since it was bound. Run `cuna opencode` in it once so Cuna can relocate the binding.");
|
|
171
|
+
}
|
|
172
|
+
if (record.generation < 1) {
|
|
173
|
+
throw unbound("generation_uncommitted", "This folder is bound but has no committed workspace generation yet. Run `cuna opencode` in this folder once to commit one.");
|
|
174
|
+
}
|
|
175
|
+
return Object.freeze({ bindingId: record.bindingId, generation: record.generation });
|
|
176
|
+
}
|
|
177
|
+
function requireOption(parsed, name, hint) {
|
|
178
|
+
const value = stringOption(parsed, name);
|
|
179
|
+
if (value === undefined) {
|
|
180
|
+
throw usageError(`Option --${name} is required.`, hint);
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
function integerOption(parsed, name, minimum, maximum) {
|
|
185
|
+
const raw = stringOption(parsed, name);
|
|
186
|
+
if (raw === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
return integerArgument(raw, name, minimum, maximum);
|
|
189
|
+
}
|
|
190
|
+
function agentOption(parsed, required) {
|
|
191
|
+
const raw = stringOption(parsed, "agent");
|
|
192
|
+
if (raw === undefined) {
|
|
193
|
+
if (required)
|
|
194
|
+
throw usageError("Option --agent is required.");
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
if (raw !== "claude-code" && raw !== "codex" && raw !== "opencode") {
|
|
198
|
+
throw usageError("Option --agent must be claude-code, codex, or opencode.");
|
|
199
|
+
}
|
|
200
|
+
return raw;
|
|
201
|
+
}
|
|
202
|
+
function normalizedAgentSessionAuthMode(rawAuthMode, credentialBinding) {
|
|
203
|
+
let authMode;
|
|
204
|
+
if (rawAuthMode === undefined) {
|
|
205
|
+
authMode = undefined;
|
|
206
|
+
}
|
|
207
|
+
else if (rawAuthMode === "interactive_login" || rawAuthMode === "credential_binding") {
|
|
208
|
+
authMode = rawAuthMode;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
throw usageError("Option --auth-mode must be interactive_login or credential_binding.");
|
|
212
|
+
}
|
|
213
|
+
if (authMode === "credential_binding" && credentialBinding === undefined) {
|
|
214
|
+
throw usageError("Option --credential-binding is required for credential_binding auth mode.");
|
|
215
|
+
}
|
|
216
|
+
if (authMode !== "credential_binding" && credentialBinding !== undefined) {
|
|
217
|
+
throw usageError("Option --credential-binding requires --auth-mode credential_binding.");
|
|
218
|
+
}
|
|
219
|
+
return authMode;
|
|
220
|
+
}
|
|
221
|
+
function requireConfirmation(parsed, command) {
|
|
222
|
+
if (booleanOption(parsed, "yes"))
|
|
223
|
+
return;
|
|
224
|
+
throw new CunaError({
|
|
225
|
+
code: "cuna.confirmation.required",
|
|
226
|
+
message: `The ${command} mutation requires explicit confirmation in this initial build.`,
|
|
227
|
+
exitCode: EXIT_CODES.policy,
|
|
228
|
+
hint: `Review the target and repeat with --yes.`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The read-back CONTRADICTED the write, and waiting will not repair it.
|
|
233
|
+
*
|
|
234
|
+
* Narrowed 2026-08-19. This used to be raised for two unrelated observations:
|
|
235
|
+
* a genuine contradiction (a rename that observed a different name, a create
|
|
236
|
+
* that observed a different id — an identity the producer can never converge
|
|
237
|
+
* to), and a state that had simply not arrived yet. Only the first belongs
|
|
238
|
+
* here. The second goes through `convergeOnRemoteState` and, if the CLI's own
|
|
239
|
+
* budget runs out first, reports that as the CLI's budget rather than as the
|
|
240
|
+
* server's failure.
|
|
241
|
+
*/
|
|
242
|
+
function postconditionUnverified(operation, details) {
|
|
243
|
+
throw new CunaError({
|
|
244
|
+
code: "cuna.remote.postcondition_unverified",
|
|
245
|
+
message: `Cuna accepted ${operation}, but the CLI could not verify the resulting remote state.`,
|
|
246
|
+
exitCode: EXIT_CODES.conflict,
|
|
247
|
+
hint: "Inspect the target with a read-only command before retrying the mutation.",
|
|
248
|
+
details,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function apiKeyCreateInput(parsed, now = Date.now()) {
|
|
252
|
+
const rawName = stringOption(parsed, "name");
|
|
253
|
+
if (rawName === undefined || rawName.length < 1 || rawName.length > 80 || rawName.trim() !== rawName) {
|
|
254
|
+
throw usageError("Option --name is required and must contain 1 through 80 non-padding characters.");
|
|
255
|
+
}
|
|
256
|
+
const name = assertSafeDisplayText(rawName, "API key name");
|
|
257
|
+
const expiresAt = stringOption(parsed, "expires-at");
|
|
258
|
+
if (expiresAt !== undefined) {
|
|
259
|
+
const utc = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u;
|
|
260
|
+
const expiryMs = Date.parse(expiresAt);
|
|
261
|
+
const oneHour = 60 * 60 * 1_000;
|
|
262
|
+
const oneYear = 365 * 24 * oneHour;
|
|
263
|
+
if (!utc.test(expiresAt) || !Number.isFinite(expiryMs) || expiryMs < now + oneHour || expiryMs > now + oneYear) {
|
|
264
|
+
throw usageError("Option --expires-at must be a UTC instant between 1 hour and 365 days from now.");
|
|
265
|
+
}
|
|
266
|
+
return Object.freeze({ name, expiresAt: new Date(expiryMs).toISOString() });
|
|
267
|
+
}
|
|
268
|
+
return Object.freeze({ name });
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Whether the key on this row still opens the door, from both timestamps.
|
|
272
|
+
*
|
|
273
|
+
* `expiresAt` and `revokedAt` are independent nullable instants
|
|
274
|
+
* (`ApiKeyMetadata`), and `--expires-at` mints keys up to 365 days out. The
|
|
275
|
+
* human line derived one word from `revokedAt` alone, so a key past its expiry
|
|
276
|
+
* but never revoked printed `active` while the JSON on the same invocation said
|
|
277
|
+
* otherwise. Expiry is now a state the word can carry, and the instant it turns
|
|
278
|
+
* on is printed rather than left to a second command.
|
|
279
|
+
*/
|
|
280
|
+
function apiKeyStatusLabel(key, now) {
|
|
281
|
+
if (key.revokedAt !== null)
|
|
282
|
+
return `revoked ${key.revokedAt}`;
|
|
283
|
+
if (key.expiresAt === null)
|
|
284
|
+
return "active (no expiry)";
|
|
285
|
+
const expiryMs = Date.parse(key.expiresAt);
|
|
286
|
+
if (!Number.isFinite(expiryMs))
|
|
287
|
+
return `active until ${key.expiresAt}`;
|
|
288
|
+
return expiryMs <= now ? `expired ${key.expiresAt}` : `active until ${key.expiresAt}`;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The reconciliation key for one create operation.
|
|
292
|
+
*
|
|
293
|
+
* Demanding this from the user made every `machines create` fail until they
|
|
294
|
+
* discovered a flag whose purpose only matters when a create outcome is
|
|
295
|
+
* uncertain — and the layer below already defaulted it, so the requirement
|
|
296
|
+
* bought nothing. It is now generated per invocation and the flag remains as an
|
|
297
|
+
* override, which is the case that actually needs a caller-known value: reusing
|
|
298
|
+
* the same key to reconcile a create whose result was never observed.
|
|
299
|
+
*/
|
|
300
|
+
function idempotencyKey(parsed) {
|
|
301
|
+
const value = stringOption(parsed, "idempotency-key");
|
|
302
|
+
return value === undefined ? randomUUID() : assertIdempotencyKey(value);
|
|
303
|
+
}
|
|
304
|
+
function machineRecord(machine) {
|
|
305
|
+
const provider = machineProviderAvailability(machine);
|
|
306
|
+
return Object.freeze({
|
|
307
|
+
id: machine.id,
|
|
308
|
+
name: machine.name,
|
|
309
|
+
state: machine.state,
|
|
310
|
+
...(machine.agent === undefined ? {} : { agent: machine.agent }),
|
|
311
|
+
...(machine.vcpus === undefined ? {} : { vcpus: machine.vcpus }),
|
|
312
|
+
...(machine.memoryMiB === undefined ? {} : { memory_mib: machine.memoryMiB }),
|
|
313
|
+
...(machine.createdAt === undefined ? {} : { created_at: machine.createdAt }),
|
|
314
|
+
...(machine.updatedAt === undefined ? {} : { updated_at: machine.updatedAt }),
|
|
315
|
+
provider_availability: Object.freeze({
|
|
316
|
+
...(provider.declaredId === undefined ? {} : { declared_id: provider.declaredId }),
|
|
317
|
+
display_name: provider.displayName,
|
|
318
|
+
usability: provider.usability,
|
|
319
|
+
actionable: provider.actionable,
|
|
320
|
+
...(provider.reasonCode === undefined ? {} : { reason_code: provider.reasonCode }),
|
|
321
|
+
...(provider.observationVersion === undefined ? {} : { observation_version: provider.observationVersion }),
|
|
322
|
+
}),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function agentSessionRecord(session, machine, now) {
|
|
326
|
+
const actionability = machine === undefined || now === undefined
|
|
327
|
+
? undefined
|
|
328
|
+
: classifySessionActionability({ session, machine, now });
|
|
329
|
+
return Object.freeze({
|
|
330
|
+
id: session.id,
|
|
331
|
+
machine_id: session.machineId,
|
|
332
|
+
...(session.workspaceBindingId === undefined
|
|
333
|
+
? {}
|
|
334
|
+
: {
|
|
335
|
+
workspace_binding_id: session.workspaceBindingId,
|
|
336
|
+
workspace_generation: session.workspaceGeneration,
|
|
337
|
+
}),
|
|
338
|
+
name: session.name,
|
|
339
|
+
agent: session.agent,
|
|
340
|
+
cwd: session.cwd,
|
|
341
|
+
auth_mode: session.authMode,
|
|
342
|
+
desired_state: session.desiredState,
|
|
343
|
+
request_state: session.requestState,
|
|
344
|
+
process_state: session.processState,
|
|
345
|
+
// The provenance of the line above, and never omitted. A caller reading
|
|
346
|
+
// `--json` must not have to infer it from a lease: absence on the wire is
|
|
347
|
+
// folded to `unknown` once, in `session-visibility.ts`, and emitted here so
|
|
348
|
+
// that "no supervisor established this" is a value rather than a silence.
|
|
349
|
+
process_observation: agentSessionProcessObservation(session),
|
|
350
|
+
...(session.terminalReason === undefined ? {} : { terminal_reason: session.terminalReason }),
|
|
351
|
+
...(session.processEpoch === undefined ? {} : { process_epoch: session.processEpoch }),
|
|
352
|
+
...(session.runtimeObservedAt === undefined ? {} : { runtime_observed_at: session.runtimeObservedAt }),
|
|
353
|
+
...(session.runtimeExpiresAt === undefined ? {} : { runtime_expires_at: session.runtimeExpiresAt }),
|
|
354
|
+
...(session.terminationRequestedAt === undefined
|
|
355
|
+
? {}
|
|
356
|
+
: { termination_requested_at: session.terminationRequestedAt }),
|
|
357
|
+
row_version: session.rowVersion,
|
|
358
|
+
created_at: session.createdAt,
|
|
359
|
+
updated_at: session.updatedAt,
|
|
360
|
+
...(actionability === undefined ? {} : {
|
|
361
|
+
base_state: actionability.baseState,
|
|
362
|
+
refresh_status: actionability.refreshStatus,
|
|
363
|
+
can_attach: actionability.canAttach,
|
|
364
|
+
recovery_action: actionability.recoveryAction,
|
|
365
|
+
reason_code: actionability.reasonCode,
|
|
366
|
+
observation_revision: actionability.observationRevision,
|
|
367
|
+
}),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function agentDisplayName(agent) {
|
|
371
|
+
return providerDisplayName(agent);
|
|
372
|
+
}
|
|
373
|
+
function machineSessionCounts(machine, sessions, now) {
|
|
374
|
+
const count = (agent) => {
|
|
375
|
+
const matching = sessions.filter((session) => session.agent === agent);
|
|
376
|
+
return Object.freeze({
|
|
377
|
+
running: matching.filter((session) => session.processState === "running" &&
|
|
378
|
+
classifySessionActionability({ session, machine, now }).canAttach).length,
|
|
379
|
+
total: matching.length,
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
return Object.freeze({
|
|
383
|
+
claude: count("claude-code"),
|
|
384
|
+
codex: count("codex"),
|
|
385
|
+
opencode: count("opencode"),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
async function listAllMachineAgentSessions(client, machineId) {
|
|
389
|
+
const items = [];
|
|
390
|
+
const cursors = new Set();
|
|
391
|
+
let cursor;
|
|
392
|
+
do {
|
|
393
|
+
const page = await client.listAgentSessions(machineId, {
|
|
394
|
+
limit: 100,
|
|
395
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
396
|
+
});
|
|
397
|
+
items.push(...page.items);
|
|
398
|
+
cursor = page.nextCursor;
|
|
399
|
+
if (cursor !== undefined && cursors.has(cursor)) {
|
|
400
|
+
throw new Error("AgentSession pagination repeated a cursor.");
|
|
401
|
+
}
|
|
402
|
+
if (cursor !== undefined)
|
|
403
|
+
cursors.add(cursor);
|
|
404
|
+
} while (cursor !== undefined);
|
|
405
|
+
return Object.freeze(items);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* One flat record as one `key\tvalue` line per field.
|
|
409
|
+
*
|
|
410
|
+
* `config get` had no human rendering at all: a person at a terminal got the
|
|
411
|
+
* JSON record pretty-printed, braces and quotes included, for nine scalar
|
|
412
|
+
* fields. Nothing was hidden, which is why this is a rendering and not a new
|
|
413
|
+
* projection — every key and every value survives, in declaration order.
|
|
414
|
+
*/
|
|
415
|
+
function renderScalarRecord(record) {
|
|
416
|
+
return Object.entries(record)
|
|
417
|
+
.map(([key, value]) => `${key}\t${value === null || value === undefined ? "null" : String(value)}`)
|
|
418
|
+
.join("\n");
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* A page footer that exists only when the page is not the whole answer.
|
|
422
|
+
*
|
|
423
|
+
* Without it "no such session" and "that session is on page 2" print the same
|
|
424
|
+
* bytes. The continuation is named as the exact next command option, because a
|
|
425
|
+
* truncation notice that does not say how to continue is only half the fact.
|
|
426
|
+
*/
|
|
427
|
+
function truncationFooter(nextCursor) {
|
|
428
|
+
return nextCursor === undefined ? [] : [`-- more results; continue with --cursor ${nextCursor}`];
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* A bounded, safe token naming WHY a machine's AgentSessions could not be read.
|
|
432
|
+
*
|
|
433
|
+
* The overview used to report the constant `sessions_unavailable` for every
|
|
434
|
+
* cause. On 2026-09-07 an Edge release added a field this CLI's decoder did not
|
|
435
|
+
* know, so every read failed `no_unknown_fields` — and the overview said only
|
|
436
|
+
* "unavailable", which named nothing and pointed nowhere. Diagnosing it needed a
|
|
437
|
+
* different command. A refusal that cannot be told apart from any other refusal
|
|
438
|
+
* is not a report.
|
|
439
|
+
*
|
|
440
|
+
* Only vocabulary the CLI itself mints is rendered: an error code, or a
|
|
441
|
+
* contract predicate and the key path that failed. No message text, no server
|
|
442
|
+
* body, no field values — the reason must never become a channel for data the
|
|
443
|
+
* server chose. The result is capped so a hostile or absurd token cannot flood
|
|
444
|
+
* a terminal row.
|
|
445
|
+
*/
|
|
446
|
+
function safeSessionsErrorReason(error) {
|
|
447
|
+
const bound = (value) => value.slice(0, 64);
|
|
448
|
+
if (error instanceof ContractViolation) {
|
|
449
|
+
return bound(error.field === undefined
|
|
450
|
+
? `contract:${error.predicate}`
|
|
451
|
+
: `contract:${error.predicate}:${error.field}`);
|
|
452
|
+
}
|
|
453
|
+
if (error instanceof CunaError)
|
|
454
|
+
return bound(error.code);
|
|
455
|
+
return "unknown";
|
|
456
|
+
}
|
|
457
|
+
function renderMachineOverview(items, now) {
|
|
458
|
+
if (items.length === 0)
|
|
459
|
+
return "No machines found.";
|
|
460
|
+
return items.flatMap(({ machine, sessions, sessionsError }) => {
|
|
461
|
+
const counts = machineSessionCounts(machine, sessions, now);
|
|
462
|
+
const provider = machineProviderAvailability(machine);
|
|
463
|
+
const claude = `Claude ${counts.claude.running}/${counts.claude.total} running`;
|
|
464
|
+
const codex = `Codex ${counts.codex.running}/${counts.codex.total} running`;
|
|
465
|
+
const opencode = `OpenCode ${counts.opencode.running}/${counts.opencode.total} running`;
|
|
466
|
+
const providerCounts = `${opencode} · ${claude} · ${codex}`;
|
|
467
|
+
const header = `▾ ${machine.name} ${machine.state} ${provider.displayName} ${providerVerdict(provider)} ${providerCounts}`;
|
|
468
|
+
if (sessionsError !== undefined)
|
|
469
|
+
return [header, ` └─ AgentSessions unavailable (${sessionsError})`];
|
|
470
|
+
if (sessions.length === 0)
|
|
471
|
+
return [header, " └─ No AgentSessions"];
|
|
472
|
+
return [
|
|
473
|
+
header,
|
|
474
|
+
...sessions.map((session, index) => {
|
|
475
|
+
const branch = index === sessions.length - 1 ? "└─" : "├─";
|
|
476
|
+
const actionability = classifySessionActionability({ session, machine, now });
|
|
477
|
+
return ` ${branch} ${agentDisplayName(session.agent)} ${session.name} ${displaySessionActionability(actionability)} ${session.id}`;
|
|
478
|
+
}),
|
|
479
|
+
];
|
|
480
|
+
}).join("\n");
|
|
481
|
+
}
|
|
482
|
+
function capabilityRecord(snapshot) {
|
|
483
|
+
return Object.freeze({
|
|
484
|
+
schema_version: snapshot.schemaVersion,
|
|
485
|
+
subject_scope: snapshot.subjectScope,
|
|
486
|
+
...(snapshot.subjectId === undefined ? {} : { subject_id: snapshot.subjectId }),
|
|
487
|
+
observed_at: snapshot.observedAt,
|
|
488
|
+
expires_at: snapshot.expiresAt,
|
|
489
|
+
etag: snapshot.etag,
|
|
490
|
+
capabilities: snapshot.capabilities.map((capability) => ({
|
|
491
|
+
id: capability.id,
|
|
492
|
+
availability: capability.availability,
|
|
493
|
+
interaction: capability.interaction,
|
|
494
|
+
mutation_class: capability.mutationClass,
|
|
495
|
+
surfaces: capability.surfaces,
|
|
496
|
+
required_permissions: capability.requiredPermissions,
|
|
497
|
+
...(capability.reasonCode === undefined ? {} : { reason_code: capability.reasonCode }),
|
|
498
|
+
})),
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
export function preflightInvocation(parsed, now = Date.now()) {
|
|
502
|
+
assertRegisteredCliRoute(parsed);
|
|
503
|
+
switch (parsed.command) {
|
|
504
|
+
case "config":
|
|
505
|
+
rejectUnknownOptions(parsed, []);
|
|
506
|
+
if (parsed.operands.length !== 1 || parsed.operands[0] !== "get") {
|
|
507
|
+
throw unsupportedError("configuration mutation", "config_writes_not_implemented");
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
case "observe":
|
|
511
|
+
rejectUnknownOptions(parsed, ["project"]);
|
|
512
|
+
if (parsed.operands.length !== 0)
|
|
513
|
+
throw usageError("observe accepts no operands.");
|
|
514
|
+
assertCanonicalUuid(stringOption(parsed, "project") ?? "", "Project ID");
|
|
515
|
+
return;
|
|
516
|
+
case "share": {
|
|
517
|
+
rejectUnknownOptions(parsed, ["project", "grant"]);
|
|
518
|
+
if (parsed.operands.length !== 0)
|
|
519
|
+
throw usageError("share accepts no operands.");
|
|
520
|
+
assertCanonicalUuid(stringOption(parsed, "project") ?? "", "Project ID");
|
|
521
|
+
const grant = stringOption(parsed, "grant");
|
|
522
|
+
if (grant !== undefined)
|
|
523
|
+
assertCanonicalUuid(grant, "Grant ID");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
case "capabilities": {
|
|
527
|
+
rejectUnknownOptions(parsed, ["scope", "resource-id"]);
|
|
528
|
+
if (parsed.operands.length !== 0)
|
|
529
|
+
throw usageError("capabilities accepts no operands.");
|
|
530
|
+
const scope = stringOption(parsed, "scope") ?? "account";
|
|
531
|
+
if (scope !== "account" && scope !== "machine" && scope !== "agent_session") {
|
|
532
|
+
throw usageError("Option --scope must be account, machine, or agent_session.");
|
|
533
|
+
}
|
|
534
|
+
const resourceId = stringOption(parsed, "resource-id");
|
|
535
|
+
if (scope === "account" && resourceId !== undefined) {
|
|
536
|
+
throw usageError("Option --resource-id is not valid for account scope.");
|
|
537
|
+
}
|
|
538
|
+
if (scope !== "account") {
|
|
539
|
+
// A resource-scoped query names a Machine or an AgentSession, and both
|
|
540
|
+
// are canonical UUIDs. Refusing the shape here keeps a typo from
|
|
541
|
+
// travelling to the server and coming back as a contract complaint.
|
|
542
|
+
assertCanonicalUuid(requireOption(parsed, "resource-id", `A ${scope} capability query is scoped to one resource.`), scope === "machine" ? "machine ID" : "AgentSession ID");
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
case "machines":
|
|
547
|
+
preflightMachines(parsed);
|
|
548
|
+
return;
|
|
549
|
+
case "records":
|
|
550
|
+
rejectUnknownOptions(parsed, []);
|
|
551
|
+
if (parsed.operands.length !== 1 || parsed.operands[0] !== "list") {
|
|
552
|
+
throw usageError("records requires the list action.");
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
case "authorizations":
|
|
556
|
+
rejectUnknownOptions(parsed, ["machine"]);
|
|
557
|
+
if (parsed.operands.length !== 1 || parsed.operands[0] !== "list") {
|
|
558
|
+
throw usageError("authorizations requires the list action.");
|
|
559
|
+
}
|
|
560
|
+
assertCanonicalUuid(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."), "machine ID");
|
|
561
|
+
return;
|
|
562
|
+
case "account":
|
|
563
|
+
case "workspace": {
|
|
564
|
+
rejectUnknownOptions(parsed, []);
|
|
565
|
+
const action = requireOperand(parsed.operands, 0, `${parsed.command} action`);
|
|
566
|
+
if (action !== "show" || parsed.operands.length !== 1) {
|
|
567
|
+
throw usageError(`${parsed.command} requires the show action.`);
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
case "usage":
|
|
572
|
+
rejectUnknownOptions(parsed, []);
|
|
573
|
+
if (parsed.operands.length !== 1 || parsed.operands[0] !== "show") {
|
|
574
|
+
throw usageError("usage requires the show action.");
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
case "api-keys": {
|
|
578
|
+
const action = requireOperand(parsed.operands, 0, "api-keys action");
|
|
579
|
+
if (action === "list") {
|
|
580
|
+
rejectUnknownOptions(parsed, []);
|
|
581
|
+
if (parsed.operands.length !== 1)
|
|
582
|
+
throw usageError("api-keys list accepts no operands.");
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (action === "revoke") {
|
|
586
|
+
rejectUnknownOptions(parsed, ["yes"]);
|
|
587
|
+
if (parsed.operands.length !== 2)
|
|
588
|
+
throw usageError("api-keys revoke requires exactly one API key ID.");
|
|
589
|
+
requireConfirmation(parsed, "api-keys.revoke");
|
|
590
|
+
assertCanonicalUuid(requireOperand(parsed.operands, 1, "API key ID"), "API key ID");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (action === "create") {
|
|
594
|
+
rejectUnknownOptions(parsed, ["name", "expires-at", "yes"]);
|
|
595
|
+
if (parsed.operands.length !== 1)
|
|
596
|
+
throw usageError("api-keys create accepts no operands.");
|
|
597
|
+
requireConfirmation(parsed, "api-keys.create");
|
|
598
|
+
// Preflight and execution must read one clock; validating here with
|
|
599
|
+
// the wall clock while execution used the injected one let a fixed
|
|
600
|
+
// test date turn red the day the calendar caught up with it.
|
|
601
|
+
apiKeyCreateInput(parsed, now);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
throw usageError(`Unknown api-keys action ${action}.`);
|
|
605
|
+
}
|
|
606
|
+
case "agent-sessions":
|
|
607
|
+
preflightAgentSessions(parsed);
|
|
608
|
+
return;
|
|
609
|
+
case "executions":
|
|
610
|
+
preflightExecutions(parsed);
|
|
611
|
+
return;
|
|
612
|
+
case "agent": {
|
|
613
|
+
rejectUnknownOptions(parsed, ["agent-session", "yes"]);
|
|
614
|
+
if (parsed.operands.length !== 1 || parsed.operands[0] !== "logout") {
|
|
615
|
+
throw usageError("agent requires the logout action.");
|
|
616
|
+
}
|
|
617
|
+
requireConfirmation(parsed, "agent.logout");
|
|
618
|
+
assertCanonicalUuid(requireOption(parsed, "agent-session", "Run `cuna agent-sessions list --machine <id>` to find an AgentSession ID."), "AgentSession ID");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
case "login":
|
|
622
|
+
case "logout":
|
|
623
|
+
case "whoami":
|
|
624
|
+
rejectUnknownOptions(parsed, []);
|
|
625
|
+
if (parsed.operands.length !== 0)
|
|
626
|
+
throw usageError(`${parsed.command} accepts no operands.`);
|
|
627
|
+
return;
|
|
628
|
+
case "access":
|
|
629
|
+
rejectUnknownOptions(parsed, []);
|
|
630
|
+
if (parsed.operands.length !== 1 || parsed.operands[0] !== "status") {
|
|
631
|
+
throw usageError("access requires the status action.");
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
case "signup":
|
|
635
|
+
rejectUnknownOptions(parsed, []);
|
|
636
|
+
if (parsed.operands.length !== 0)
|
|
637
|
+
throw usageError("signup accepts no operands.");
|
|
638
|
+
return;
|
|
639
|
+
case "claude":
|
|
640
|
+
case "codex":
|
|
641
|
+
case "opencode": {
|
|
642
|
+
preflightAgentJourneyInvocation(parsed);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
case "shell":
|
|
646
|
+
case "sync":
|
|
647
|
+
case "companion":
|
|
648
|
+
rejectUnknownOptions(parsed, []);
|
|
649
|
+
if (parsed.operands.length !== 0)
|
|
650
|
+
throw usageError(`${parsed.command} accepts no operands in this build.`);
|
|
651
|
+
return;
|
|
652
|
+
case "connect":
|
|
653
|
+
rejectUnknownOptions(parsed, []);
|
|
654
|
+
if (parsed.operands.length < 1 || parsed.operands.length > 4) {
|
|
655
|
+
throw usageError("connect requires one through four explicit AgentSession IDs.");
|
|
656
|
+
}
|
|
657
|
+
if (new Set(parsed.operands).size !== parsed.operands.length) {
|
|
658
|
+
throw usageError("connect requires distinct AgentSession IDs.");
|
|
659
|
+
}
|
|
660
|
+
for (const agentSessionId of parsed.operands)
|
|
661
|
+
assertCanonicalUuid(agentSessionId, "AgentSession ID");
|
|
662
|
+
return;
|
|
663
|
+
case "doctor":
|
|
664
|
+
rejectUnknownOptions(parsed, ["check-browser-login"]);
|
|
665
|
+
if (parsed.operands.length !== 0)
|
|
666
|
+
throw usageError("doctor accepts no operands.");
|
|
667
|
+
return;
|
|
668
|
+
case "self-test":
|
|
669
|
+
rejectUnknownOptions(parsed, ["offline"]);
|
|
670
|
+
if (parsed.operands.length !== 0)
|
|
671
|
+
throw usageError("self-test accepts no operands.");
|
|
672
|
+
if (!booleanOption(parsed, "offline")) {
|
|
673
|
+
throw usageError("self-test requires --offline in this release.", "Run `cuna self-test --offline --json`.");
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
case "version":
|
|
677
|
+
rejectUnknownOptions(parsed, ["help", "version"]);
|
|
678
|
+
if (parsed.operands.length !== 0)
|
|
679
|
+
throw usageError("version accepts no operands.");
|
|
680
|
+
return;
|
|
681
|
+
case "help":
|
|
682
|
+
rejectUnknownOptions(parsed, ["help"]);
|
|
683
|
+
if (parsed.operands.length !== 0)
|
|
684
|
+
throw usageError("help accepts no operands.");
|
|
685
|
+
return;
|
|
686
|
+
default:
|
|
687
|
+
throw usageError(`Unknown command ${parsed.command ?? "<none>"}.`, "Run `cuna --help`.");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function preflightLiveUpdateSupervisor(parsed) {
|
|
691
|
+
rejectUnknownOptions(parsed, ["yes", "resume", "forget-unknown", "operation"]);
|
|
692
|
+
if (parsed.operands.length !== 2) {
|
|
693
|
+
throw usageError("machines live-update-supervisor requires exactly one machine ID.");
|
|
694
|
+
}
|
|
695
|
+
const chosen = ["yes", "resume", "forget-unknown"]
|
|
696
|
+
.filter((option) => booleanOption(parsed, option));
|
|
697
|
+
if (chosen.length > 1) {
|
|
698
|
+
throw usageError("machines live-update-supervisor accepts exactly one of --yes, --resume or --forget-unknown.", "Starting a new update, repeating the one this computer already recorded, and clearing that record are three different decisions. Read `cuna machines live-update-status MACHINE_ID` first; it sends nothing.");
|
|
699
|
+
}
|
|
700
|
+
const intent = chosen[0] === "forget-unknown"
|
|
701
|
+
? "forget-unknown"
|
|
702
|
+
: chosen[0] === "resume" ? "resume" : "start";
|
|
703
|
+
// `--resume` is its own explicit decision and carries the identity it will
|
|
704
|
+
// re-send in its own name, so it is not additionally gated on `--yes`.
|
|
705
|
+
if (intent === "start")
|
|
706
|
+
requireConfirmation(parsed, "machines.live-update-supervisor");
|
|
707
|
+
assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
|
|
708
|
+
const operation = stringOption(parsed, "operation");
|
|
709
|
+
if (operation !== undefined) {
|
|
710
|
+
// Only the repeat can take an identity. Naming one alongside "start a new
|
|
711
|
+
// update" or "clear the record" would be two decisions in one flag.
|
|
712
|
+
if (intent !== "resume") {
|
|
713
|
+
throw usageError("machines live-update-supervisor accepts --operation only with --resume.", `--operation names an update to finish, which is what --resume does. To read one without sending anything, use \`cuna machines live-update-status MACHINE_ID --operation ${operation}\`.`);
|
|
714
|
+
}
|
|
715
|
+
assertCanonicalUuid(operation, "supervisor update operation ID");
|
|
716
|
+
}
|
|
717
|
+
return Object.freeze({ intent });
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* `machines live-update-status` — the read that recovers a lost answer.
|
|
721
|
+
*
|
|
722
|
+
* No confirmation, because it confirms nothing: the producer states that this
|
|
723
|
+
* route dispatches no installer, sends nothing to the Machine and changes no
|
|
724
|
+
* state. `--operation` is optional; without it the command reads the identity
|
|
725
|
+
* this computer recorded, which is the whole point of having recorded it.
|
|
726
|
+
*/
|
|
727
|
+
function preflightLiveUpdateStatus(parsed) {
|
|
728
|
+
rejectUnknownOptions(parsed, ["operation"]);
|
|
729
|
+
if (parsed.operands.length !== 2) {
|
|
730
|
+
throw usageError("machines live-update-status requires exactly one machine ID.");
|
|
731
|
+
}
|
|
732
|
+
assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
|
|
733
|
+
const operation = stringOption(parsed, "operation");
|
|
734
|
+
if (operation !== undefined)
|
|
735
|
+
assertCanonicalUuid(operation, "supervisor update operation ID");
|
|
736
|
+
}
|
|
737
|
+
function preflightMachines(parsed) {
|
|
738
|
+
if (parsed.operands.length === 0) {
|
|
739
|
+
rejectUnknownOptions(parsed, []);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const action = requireOperand(parsed.operands, 0, "machines action");
|
|
743
|
+
if (action === "list") {
|
|
744
|
+
rejectUnknownOptions(parsed, []);
|
|
745
|
+
if (parsed.operands.length !== 1)
|
|
746
|
+
throw usageError("machines list accepts no operands.");
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (action === "create") {
|
|
750
|
+
rejectUnknownOptions(parsed, ["name", "agent", "vcpus", "memory-mib", "background", "yes", "idempotency-key"]);
|
|
751
|
+
if (parsed.operands.length !== 1)
|
|
752
|
+
throw usageError("machines create accepts no operands.");
|
|
753
|
+
requireConfirmation(parsed, "machines.create");
|
|
754
|
+
idempotencyKey(parsed);
|
|
755
|
+
const rawName = stringOption(parsed, "name");
|
|
756
|
+
if (rawName === undefined)
|
|
757
|
+
throw usageError("Option --name is required.");
|
|
758
|
+
const name = assertSafeDisplayText(rawName, "machine name");
|
|
759
|
+
if (name.length < 1 || name.length > 80)
|
|
760
|
+
throw usageError("Option --name must contain 1 through 80 characters.");
|
|
761
|
+
agentOption(parsed, false);
|
|
762
|
+
integerOption(parsed, "vcpus", 1, 8);
|
|
763
|
+
integerOption(parsed, "memory-mib", 512, 16_384);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (action === "update-supervisor") {
|
|
767
|
+
rejectUnknownOptions(parsed, ["yes"]);
|
|
768
|
+
if (parsed.operands.length !== 2)
|
|
769
|
+
throw usageError("machines update-supervisor requires exactly one machine ID.");
|
|
770
|
+
requireConfirmation(parsed, "machines.update-supervisor");
|
|
771
|
+
assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (action === "live-update-supervisor") {
|
|
775
|
+
preflightLiveUpdateSupervisor(parsed);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (action === "live-update-status") {
|
|
779
|
+
preflightLiveUpdateStatus(parsed);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (action === "start" || action === "pause" || action === "resume" || action === "stop" || action === "delete") {
|
|
783
|
+
rejectUnknownOptions(parsed, ["yes"]);
|
|
784
|
+
if (parsed.operands.length !== 2)
|
|
785
|
+
throw usageError(`machines ${action} requires exactly one machine ID.`);
|
|
786
|
+
requireConfirmation(parsed, `machines.${action}`);
|
|
787
|
+
assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
throw usageError(`Unknown machines action ${action}.`);
|
|
791
|
+
}
|
|
792
|
+
function preflightAgentSessions(parsed) {
|
|
793
|
+
const action = requireOperand(parsed.operands, 0, "agent-sessions action");
|
|
794
|
+
if (action === "list") {
|
|
795
|
+
rejectUnknownOptions(parsed, ["machine", "limit", "cursor"]);
|
|
796
|
+
if (parsed.operands.length !== 1)
|
|
797
|
+
throw usageError("agent-sessions list accepts no operands.");
|
|
798
|
+
assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."));
|
|
799
|
+
integerOption(parsed, "limit", 1, 100);
|
|
800
|
+
const cursor = stringOption(parsed, "cursor");
|
|
801
|
+
if (cursor !== undefined && (cursor.length > 512 || /[\p{Cc}\p{Cf}]/u.test(cursor))) {
|
|
802
|
+
throw usageError("Option --cursor is malformed.");
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (action === "get") {
|
|
807
|
+
rejectUnknownOptions(parsed, []);
|
|
808
|
+
if (parsed.operands.length !== 2)
|
|
809
|
+
throw usageError("agent-sessions get requires exactly one AgentSession ID.");
|
|
810
|
+
assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (action === "create") {
|
|
814
|
+
rejectUnknownOptions(parsed, [
|
|
815
|
+
"machine", "workspace-binding-id", "workspace-generation", "name", "agent", "cwd",
|
|
816
|
+
"auth-mode", "credential-binding", "yes", "idempotency-key",
|
|
817
|
+
]);
|
|
818
|
+
if (parsed.operands.length !== 1)
|
|
819
|
+
throw usageError("agent-sessions create accepts no operands.");
|
|
820
|
+
requireConfirmation(parsed, "agent-sessions.create");
|
|
821
|
+
assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."));
|
|
822
|
+
// Both workspace options are optional here: absent, they are resolved from
|
|
823
|
+
// the folder's own binding at execution. Preflight still validates the
|
|
824
|
+
// shape of whatever WAS supplied, so a malformed value is refused before
|
|
825
|
+
// any network work — it just no longer refuses their absence.
|
|
826
|
+
const givenBinding = stringOption(parsed, "workspace-binding-id");
|
|
827
|
+
if (givenBinding !== undefined)
|
|
828
|
+
assertCanonicalUuid(givenBinding, "workspace binding ID");
|
|
829
|
+
integerOption(parsed, "workspace-generation", 1, Number.MAX_SAFE_INTEGER);
|
|
830
|
+
const agent = agentOption(parsed, true);
|
|
831
|
+
if (agent === undefined)
|
|
832
|
+
throw usageError("Option --agent is required.");
|
|
833
|
+
const cwd = assertSafeDisplayText(stringOption(parsed, "cwd") ?? "/workspace", "workspace path");
|
|
834
|
+
if (!cwd.startsWith("/workspace") || cwd.split("/").includes("..") || cwd.length > 1024) {
|
|
835
|
+
throw usageError("Option --cwd must be a safe absolute path inside /workspace.");
|
|
836
|
+
}
|
|
837
|
+
const name = stringOption(parsed, "name");
|
|
838
|
+
if (name !== undefined && (assertSafeDisplayText(name, "AgentSession name").length < 1 || name.length > 80)) {
|
|
839
|
+
throw usageError("Option --name must contain 1 through 80 characters.");
|
|
840
|
+
}
|
|
841
|
+
const binding = stringOption(parsed, "credential-binding");
|
|
842
|
+
const rawAuthMode = stringOption(parsed, "auth-mode");
|
|
843
|
+
if (agent === "opencode" && (rawAuthMode === "credential_binding" || binding !== undefined)) {
|
|
844
|
+
throw usageError("OpenCode supports interactive_login only; credential bindings are not accepted.", "Use OpenCode's interactive provider flow and omit --credential-binding.");
|
|
845
|
+
}
|
|
846
|
+
normalizedAgentSessionAuthMode(rawAuthMode, binding);
|
|
847
|
+
if (binding !== undefined)
|
|
848
|
+
assertPublicId(binding, "credential binding ID");
|
|
849
|
+
idempotencyKey(parsed);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (action === "terminate" || action === "rename") {
|
|
853
|
+
rejectUnknownOptions(parsed, action === "rename" ? ["name", "yes"] : ["yes"]);
|
|
854
|
+
if (parsed.operands.length !== 2)
|
|
855
|
+
throw usageError(`agent-sessions ${action} requires exactly one AgentSession ID.`);
|
|
856
|
+
requireConfirmation(parsed, `agent-sessions.${action}`);
|
|
857
|
+
assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
|
|
858
|
+
if (action === "rename") {
|
|
859
|
+
const name = stringOption(parsed, "name");
|
|
860
|
+
if (name === undefined || assertSafeDisplayText(name, "AgentSession name").length < 1 || name.length > 80) {
|
|
861
|
+
throw usageError("Option --name must contain 1 through 80 characters.");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (action === "attach") {
|
|
867
|
+
rejectUnknownOptions(parsed, []);
|
|
868
|
+
if (parsed.operands.length !== 2)
|
|
869
|
+
throw usageError("agent-sessions attach requires exactly one AgentSession ID.");
|
|
870
|
+
assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
throw usageError(`Unknown agent-sessions action ${action}.`);
|
|
874
|
+
}
|
|
875
|
+
export async function executeCommand(context) {
|
|
876
|
+
const { parsed, config, client } = context;
|
|
877
|
+
switch (parsed.command) {
|
|
878
|
+
case "config": {
|
|
879
|
+
rejectUnknownOptions(parsed, []);
|
|
880
|
+
if (parsed.operands.length !== 1 || parsed.operands[0] !== "get") {
|
|
881
|
+
throw unsupportedError("configuration mutation", "config_writes_not_implemented");
|
|
882
|
+
}
|
|
883
|
+
const data = publicConfig(config);
|
|
884
|
+
return Object.freeze({ command: "config.get", data, human: renderScalarRecord(data) });
|
|
885
|
+
}
|
|
886
|
+
case "capabilities": {
|
|
887
|
+
rejectUnknownOptions(parsed, ["scope", "resource-id"]);
|
|
888
|
+
requireCredential(context);
|
|
889
|
+
const scope = stringOption(parsed, "scope") ?? "account";
|
|
890
|
+
if (scope !== "account" && scope !== "machine" && scope !== "agent_session") {
|
|
891
|
+
throw usageError("Option --scope must be account, machine, or agent_session.");
|
|
892
|
+
}
|
|
893
|
+
const resourceId = stringOption(parsed, "resource-id");
|
|
894
|
+
if (scope !== "account" && resourceId === undefined) {
|
|
895
|
+
throw usageError("Option --resource-id is required for a resource-scoped capability query.");
|
|
896
|
+
}
|
|
897
|
+
const snapshot = await client.discoverCapabilities(scope, resourceId);
|
|
898
|
+
const data = capabilityRecord(snapshot);
|
|
899
|
+
return Object.freeze({
|
|
900
|
+
command: "capabilities",
|
|
901
|
+
data,
|
|
902
|
+
human: snapshot.capabilities.length === 0
|
|
903
|
+
? "No capabilities were advertised for this context."
|
|
904
|
+
: snapshot.capabilities
|
|
905
|
+
.map((capability) => `${capability.id}\t${capability.availability}\t${capability.interaction}`)
|
|
906
|
+
.join("\n"),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
case "machines":
|
|
910
|
+
return executeMachines(context);
|
|
911
|
+
case "records": {
|
|
912
|
+
requireCredential(context);
|
|
913
|
+
await requireCapability({
|
|
914
|
+
client,
|
|
915
|
+
scope: "account",
|
|
916
|
+
capabilityId: "records.list",
|
|
917
|
+
now: context.capabilityClock ?? context.now,
|
|
918
|
+
allowedInteractions: ["read_only"],
|
|
919
|
+
});
|
|
920
|
+
const records = await client.listRecords();
|
|
921
|
+
const data = Object.freeze({
|
|
922
|
+
items: records.map((record) => Object.freeze({
|
|
923
|
+
id: record.id,
|
|
924
|
+
machine_id: record.machineId,
|
|
925
|
+
kind: record.kind,
|
|
926
|
+
summary: record.summary,
|
|
927
|
+
detail: record.detail,
|
|
928
|
+
created_at: record.createdAt,
|
|
929
|
+
})),
|
|
930
|
+
});
|
|
931
|
+
return Object.freeze({
|
|
932
|
+
command: "records.list",
|
|
933
|
+
data,
|
|
934
|
+
human: records.length === 0
|
|
935
|
+
? "No records found."
|
|
936
|
+
: records.map((record) => `${record.createdAt}\t${record.machineId}\t${record.kind}\t${record.summary}`).join("\n"),
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
case "authorizations": {
|
|
940
|
+
requireCredential(context);
|
|
941
|
+
const machineId = assertCanonicalUuid(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."), "machine ID");
|
|
942
|
+
await requireCapability({
|
|
943
|
+
client,
|
|
944
|
+
scope: "machine",
|
|
945
|
+
resourceId: machineId,
|
|
946
|
+
capabilityId: "authorizations.list",
|
|
947
|
+
now: context.capabilityClock ?? context.now,
|
|
948
|
+
allowedInteractions: ["read_only"],
|
|
949
|
+
});
|
|
950
|
+
const authorizations = await client.listAuthorizations(machineId);
|
|
951
|
+
const data = Object.freeze({
|
|
952
|
+
machine_id: machineId,
|
|
953
|
+
...authorizations,
|
|
954
|
+
});
|
|
955
|
+
const lines = authorizations.secret_configuration.flatMap((configuration) => {
|
|
956
|
+
const binding = `secret:${configuration.secret_id}`;
|
|
957
|
+
const items = [
|
|
958
|
+
...configuration.environment.map((item) => `environment\t${item.name}\t${JSON.stringify(item.value_template)}\t${binding}`),
|
|
959
|
+
...configuration.egress_rules.map((item) => `egress:${item.action}\t${item.host_pattern}\tpath:${JSON.stringify(item.path_pattern) ?? "unspecified"}\t${item.name}\t${JSON.stringify(item.value_template)}\t${binding}`),
|
|
960
|
+
...configuration.files.map((item) => `file\t${item.path}\t${JSON.stringify(item.value_template)}\t${binding}`),
|
|
961
|
+
];
|
|
962
|
+
return items.length === 0 ? [`${binding}\tNo injection entries configured.`] : items;
|
|
963
|
+
});
|
|
964
|
+
return Object.freeze({
|
|
965
|
+
command: "authorizations.list",
|
|
966
|
+
data,
|
|
967
|
+
human: `Configuration revision ${authorizations.revision}\n${lines.length === 0
|
|
968
|
+
? "No inline secret configuration is set for this machine."
|
|
969
|
+
: lines.join("\n")}`,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
case "account": {
|
|
973
|
+
requireCredential(context);
|
|
974
|
+
const identity = await client.getIdentity();
|
|
975
|
+
const data = Object.freeze({ id: identity.id, email: identity.email });
|
|
976
|
+
return Object.freeze({ command: "account.show", data, human: `${identity.id}\t${identity.email}` });
|
|
977
|
+
}
|
|
978
|
+
case "workspace": {
|
|
979
|
+
requireCredential(context);
|
|
980
|
+
const identity = await client.getIdentity();
|
|
981
|
+
const data = Object.freeze({
|
|
982
|
+
assigned: identity.workspaceAssigned,
|
|
983
|
+
...(identity.waitlistPosition === undefined
|
|
984
|
+
? {}
|
|
985
|
+
: { waitlist_position: identity.waitlistPosition }),
|
|
986
|
+
});
|
|
987
|
+
return Object.freeze({
|
|
988
|
+
command: "workspace.show",
|
|
989
|
+
data,
|
|
990
|
+
human: identity.workspaceAssigned
|
|
991
|
+
? "A Cuna workspace is assigned to this account."
|
|
992
|
+
: `No Cuna workspace is assigned. Waitlist position: ${identity.waitlistPosition ?? "unknown"}.`,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
case "usage": {
|
|
996
|
+
requireCredential(context);
|
|
997
|
+
const identity = await client.getIdentity();
|
|
998
|
+
if (identity.workspaceUsage === undefined) {
|
|
999
|
+
// The refusal lives here, where the figure is read, and nowhere else.
|
|
1000
|
+
// A usage payload this build does not recognise stops this one command;
|
|
1001
|
+
// every other command still gets its identity.
|
|
1002
|
+
if (identity.workspaceUsageProblem !== undefined) {
|
|
1003
|
+
throw new CunaError({
|
|
1004
|
+
code: "cuna.remote.malformed_response",
|
|
1005
|
+
message: "Cuna reported spend in a shape this CLI does not accept.",
|
|
1006
|
+
exitCode: EXIT_CODES.remote,
|
|
1007
|
+
hint: "No figure is shown rather than a wrong one. Every other command is unaffected. " +
|
|
1008
|
+
"Run `cuna version --json` and report it with the details above at " +
|
|
1009
|
+
"https://github.com/Cuna-Labs/cuna-cli/issues.",
|
|
1010
|
+
details: { reason: "workspace_usage_off_contract", detail: identity.workspaceUsageProblem },
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
throw unsupportedError("workspace usage", "workspace_usage_unavailable");
|
|
1014
|
+
}
|
|
1015
|
+
const usage = identity.workspaceUsage;
|
|
1016
|
+
const data = Object.freeze({
|
|
1017
|
+
estimated_spend_usd: usage.estimatedSpendUsd,
|
|
1018
|
+
estimated_spend_is_lower_bound: usage.estimatedSpendIsLowerBound,
|
|
1019
|
+
balance_status: usage.balanceStatus,
|
|
1020
|
+
balance_usd: usage.balanceUsd,
|
|
1021
|
+
...(usage.balanceUnavailableReason === undefined
|
|
1022
|
+
? {}
|
|
1023
|
+
: { balance_unavailable_reason: usage.balanceUnavailableReason }),
|
|
1024
|
+
note: usage.note,
|
|
1025
|
+
});
|
|
1026
|
+
// "At least" is the whole claim and the sentence carries it. A balance is
|
|
1027
|
+
// printed only when the producer says it read one: an unavailable balance
|
|
1028
|
+
// is not zero.
|
|
1029
|
+
return Object.freeze({
|
|
1030
|
+
command: "usage.show",
|
|
1031
|
+
data,
|
|
1032
|
+
human: [
|
|
1033
|
+
`At least $${usage.estimatedSpendUsd.toFixed(2)} spent.`,
|
|
1034
|
+
usage.balanceStatus === "available" && usage.balanceUsd !== null
|
|
1035
|
+
? `Balance $${usage.balanceUsd.toFixed(2)}.`
|
|
1036
|
+
: "No balance available.",
|
|
1037
|
+
usage.note,
|
|
1038
|
+
].join(" "),
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
case "api-keys": {
|
|
1042
|
+
requireCredential(context);
|
|
1043
|
+
const action = requireOperand(parsed.operands, 0, "api-keys action");
|
|
1044
|
+
if (context.credentialMode !== "interactive") {
|
|
1045
|
+
throw new CunaError({
|
|
1046
|
+
code: "cuna.auth.interactive_required",
|
|
1047
|
+
message: "API-key management requires an interactive Cuna session.",
|
|
1048
|
+
exitCode: EXIT_CODES.auth,
|
|
1049
|
+
hint: "Unset the automation credential, run `cuna login`, then repeat this command.",
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
await requireCapability({ client, scope: "account", capabilityId: "api_keys.manage", now: context.capabilityClock ?? context.now });
|
|
1053
|
+
if (action === "list") {
|
|
1054
|
+
const keys = await client.listApiKeys();
|
|
1055
|
+
const data = Object.freeze({
|
|
1056
|
+
items: keys.map((key) => Object.freeze({
|
|
1057
|
+
id: key.id,
|
|
1058
|
+
name: key.name,
|
|
1059
|
+
prefix: key.prefix,
|
|
1060
|
+
last_four: key.lastFour,
|
|
1061
|
+
created_at: key.createdAt,
|
|
1062
|
+
expires_at: key.expiresAt,
|
|
1063
|
+
last_used_at: key.lastUsedAt,
|
|
1064
|
+
revoked_at: key.revokedAt,
|
|
1065
|
+
})),
|
|
1066
|
+
});
|
|
1067
|
+
return Object.freeze({
|
|
1068
|
+
command: "api-keys.list",
|
|
1069
|
+
data,
|
|
1070
|
+
human: keys.length === 0
|
|
1071
|
+
? "No API keys found."
|
|
1072
|
+
: keys.map((key) => `${key.id}\t${key.name}\t${key.prefix}…${key.lastFour}\t${apiKeyStatusLabel(key, context.now)}`).join("\n"),
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
if (action === "revoke") {
|
|
1076
|
+
const id = assertCanonicalUuid(requireOperand(parsed.operands, 1, "API key ID"), "API key ID");
|
|
1077
|
+
await client.revokeApiKey(id);
|
|
1078
|
+
const observed = (await client.listApiKeys()).find((key) => key.id === id);
|
|
1079
|
+
if (observed !== undefined && observed.revokedAt === null) {
|
|
1080
|
+
postconditionUnverified("API-key revocation", { api_key_id: id, observed_state: "active" });
|
|
1081
|
+
}
|
|
1082
|
+
return Object.freeze({
|
|
1083
|
+
command: "api-keys.revoke",
|
|
1084
|
+
data: Object.freeze({ id, revoked: true }),
|
|
1085
|
+
human: `Revoked API key ${id}.`,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
if (action === "create") {
|
|
1089
|
+
const input = apiKeyCreateInput(parsed, context.now);
|
|
1090
|
+
const prior = await client.listApiKeys();
|
|
1091
|
+
const priorIds = new Set(prior.map((key) => key.id));
|
|
1092
|
+
const operationKey = `cuna-api-key-create-${randomUUID()}`;
|
|
1093
|
+
let created;
|
|
1094
|
+
let createFailure;
|
|
1095
|
+
try {
|
|
1096
|
+
created = await client.createApiKey(input, operationKey);
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
createFailure = error;
|
|
1100
|
+
}
|
|
1101
|
+
if (created?.idempotencyReplayed === true) {
|
|
1102
|
+
createFailure = new Error("API-key creation replay did not return one-time secret material.");
|
|
1103
|
+
}
|
|
1104
|
+
if (createFailure !== undefined) {
|
|
1105
|
+
// Reuse the same operation authority once. This can recover the exact
|
|
1106
|
+
// committed ID after a timeout or malformed first response without
|
|
1107
|
+
// creating a sibling key. The replay secret, if any, is never used:
|
|
1108
|
+
// after an uncertain response the only safe outcome is reconciliation.
|
|
1109
|
+
let replayed;
|
|
1110
|
+
try {
|
|
1111
|
+
replayed = await client.createApiKey(input, operationKey);
|
|
1112
|
+
}
|
|
1113
|
+
catch { /* post-list remains authoritative */ }
|
|
1114
|
+
const observedAt = context.now + 120_000;
|
|
1115
|
+
const candidates = (await client.listApiKeys()).filter((key) => {
|
|
1116
|
+
const createdAt = Date.parse(key.createdAt);
|
|
1117
|
+
return !priorIds.has(key.id) && key.name === input.name && key.revokedAt === null &&
|
|
1118
|
+
key.expiresAt === (input.expiresAt ?? null) && Number.isFinite(createdAt) &&
|
|
1119
|
+
createdAt >= context.now - 5_000 && createdAt <= observedAt + 5_000;
|
|
1120
|
+
});
|
|
1121
|
+
if (candidates.length === 1) {
|
|
1122
|
+
await client.revokeApiKey(candidates[0].id);
|
|
1123
|
+
const remaining = (await client.listApiKeys()).filter((key) => key.id === candidates[0].id && key.revokedAt === null);
|
|
1124
|
+
if (remaining.length !== 0) {
|
|
1125
|
+
throw new CunaError({
|
|
1126
|
+
code: "cuna.api_keys.create_cleanup_unverified",
|
|
1127
|
+
message: "Cuna could not verify cleanup of an API key created during an uncertain response.",
|
|
1128
|
+
exitCode: EXIT_CODES.conflict,
|
|
1129
|
+
hint: "Revoke the listed API key ID in the Cuna dashboard before retrying.",
|
|
1130
|
+
details: { manual_cleanup_ids: [candidates[0].id], idempotency_key: operationKey },
|
|
1131
|
+
cause: createFailure,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
throw new CunaError({
|
|
1135
|
+
code: "cuna.api_keys.create_secret_unobserved",
|
|
1136
|
+
message: "Cuna created the API key, but its one-time secret response was not observed; the CLI revoked it safely.",
|
|
1137
|
+
exitCode: EXIT_CODES.network,
|
|
1138
|
+
hint: "Retry creation with a new name. The unobserved key was revoked and cannot authenticate.",
|
|
1139
|
+
retryable: true,
|
|
1140
|
+
details: { api_key_id: candidates[0].id, reconciled: true, revoked: true, cleanup_verified: true, idempotency_replayed: replayed?.idempotencyReplayed ?? null },
|
|
1141
|
+
cause: createFailure,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
if (candidates.length === 0) {
|
|
1145
|
+
throw new CunaError({
|
|
1146
|
+
code: "cuna.api_keys.create_failed_no_commit",
|
|
1147
|
+
message: "API-key creation failed and reconciliation found no newly created key.",
|
|
1148
|
+
exitCode: EXIT_CODES.network,
|
|
1149
|
+
hint: "No cleanup is required. Retry creation with the same name when connectivity is stable.",
|
|
1150
|
+
retryable: true,
|
|
1151
|
+
details: { reconciled: true, created: false, idempotency_key: operationKey },
|
|
1152
|
+
cause: createFailure,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
throw new CunaError({
|
|
1156
|
+
code: "cuna.api_keys.create_reconciliation_ambiguous",
|
|
1157
|
+
message: "API-key creation failed and reconciliation found multiple possible new keys.",
|
|
1158
|
+
exitCode: EXIT_CODES.conflict,
|
|
1159
|
+
hint: "Review and revoke the listed API key IDs in the Cuna dashboard before retrying.",
|
|
1160
|
+
details: { reconciled: false, manual_cleanup_ids: candidates.map((key) => key.id), idempotency_key: operationKey },
|
|
1161
|
+
cause: createFailure,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
if (created === undefined || created.idempotencyReplayed)
|
|
1165
|
+
throw new Error("API-key creation reconciliation invariant failed.");
|
|
1166
|
+
const data = Object.freeze({
|
|
1167
|
+
id: created.id,
|
|
1168
|
+
name: created.name,
|
|
1169
|
+
prefix: created.prefix,
|
|
1170
|
+
last_four: created.lastFour,
|
|
1171
|
+
created_at: created.createdAt,
|
|
1172
|
+
expires_at: created.expiresAt,
|
|
1173
|
+
key: created.key,
|
|
1174
|
+
});
|
|
1175
|
+
return Object.freeze({
|
|
1176
|
+
command: "api-keys.create",
|
|
1177
|
+
data,
|
|
1178
|
+
human: `Created API key ${created.name}. Copy it now; Cuna will not show it again.\n${created.key}`,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
throw usageError(`Unknown api-keys action ${action}.`);
|
|
1182
|
+
}
|
|
1183
|
+
case "agent-sessions":
|
|
1184
|
+
return executeAgentSessions(context);
|
|
1185
|
+
case "executions":
|
|
1186
|
+
return executeExecutions(context);
|
|
1187
|
+
case "agent": {
|
|
1188
|
+
requireCredential(context);
|
|
1189
|
+
rejectUnknownOptions(parsed, ["agent-session", "yes"]);
|
|
1190
|
+
if (parsed.operands.length !== 1 || parsed.operands[0] !== "logout") {
|
|
1191
|
+
throw usageError("agent requires the logout action.");
|
|
1192
|
+
}
|
|
1193
|
+
requireConfirmation(parsed, "agent.logout");
|
|
1194
|
+
const id = assertCanonicalUuid(requireOption(parsed, "agent-session", "Run `cuna agent-sessions list --machine <id>` to find an AgentSession ID."), "AgentSession ID");
|
|
1195
|
+
const session = await client.getAgentSession(id);
|
|
1196
|
+
if (session.processEpoch === undefined ||
|
|
1197
|
+
session.authMode !== "interactive_login" ||
|
|
1198
|
+
(session.agent !== "claude-code" && session.agent !== "codex")) {
|
|
1199
|
+
throw new CunaError({
|
|
1200
|
+
code: "cuna.agent.auth_logout_unavailable",
|
|
1201
|
+
message: "Provider sign-out is unavailable for this AgentSession.",
|
|
1202
|
+
exitCode: EXIT_CODES.policy,
|
|
1203
|
+
hint: "Select a running Claude Code or Codex AgentSession using interactive sign-in.",
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
await requireCapability({
|
|
1207
|
+
client,
|
|
1208
|
+
scope: "agent_session",
|
|
1209
|
+
resourceId: id,
|
|
1210
|
+
capabilityId: "agent_sessions.auth_logout",
|
|
1211
|
+
now: context.capabilityClock ?? context.now,
|
|
1212
|
+
});
|
|
1213
|
+
const receipt = await client.logoutAgentSessionAuth(id, session.processEpoch);
|
|
1214
|
+
if (receipt.agentSessionId !== session.id ||
|
|
1215
|
+
receipt.processEpoch !== session.processEpoch ||
|
|
1216
|
+
receipt.authMode !== session.authMode ||
|
|
1217
|
+
receipt.agent !== session.agent) {
|
|
1218
|
+
throw new CunaError({
|
|
1219
|
+
code: "cuna.remote.malformed_response",
|
|
1220
|
+
message: "Cuna returned a provider sign-out receipt for another AgentSession authority.",
|
|
1221
|
+
exitCode: EXIT_CODES.remote,
|
|
1222
|
+
hint: OFF_CONTRACT_RESPONSE_HINT,
|
|
1223
|
+
details: {
|
|
1224
|
+
operation: "POST /v1/agent-sessions/{id}/agent-auth/logout",
|
|
1225
|
+
// The first field that disagreed, not all four: a list of every
|
|
1226
|
+
// compared field says nothing about which one was wrong.
|
|
1227
|
+
field: receipt.agentSessionId !== session.id
|
|
1228
|
+
? "agent_session_id"
|
|
1229
|
+
: receipt.processEpoch !== session.processEpoch
|
|
1230
|
+
? "process_epoch"
|
|
1231
|
+
: receipt.authMode !== session.authMode
|
|
1232
|
+
? "auth_mode"
|
|
1233
|
+
: "agent",
|
|
1234
|
+
predicate: "matches_requested_resource",
|
|
1235
|
+
},
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
const observedAuth = await client.getAgentSessionAuth(id);
|
|
1239
|
+
const observationIsAboutThisSession = observedAuth.agentSessionId === id &&
|
|
1240
|
+
observedAuth.agent === session.agent &&
|
|
1241
|
+
observedAuth.processEpoch === session.processEpoch;
|
|
1242
|
+
// Absence of evidence is not a contradiction. Some providers abstain from
|
|
1243
|
+
// reporting a login state at all — the Edge does exactly that for Codex,
|
|
1244
|
+
// because a successful `codex login status` proves credentials exist and
|
|
1245
|
+
// not that the account would be accepted. That abstention arrives as
|
|
1246
|
+
// `state: "unavailable"`, and treating it as a failed postcondition
|
|
1247
|
+
// turned a sign-out the server had CONFIRMED into an error the person
|
|
1248
|
+
// could do nothing about. `postconditionUnverified` is reserved, by its
|
|
1249
|
+
// own contract above, for an observation that CONTRADICTS the write.
|
|
1250
|
+
const providerAbstains = observedAuth.state === "unavailable";
|
|
1251
|
+
if (!observationIsAboutThisSession ||
|
|
1252
|
+
(!providerAbstains && observedAuth.state !== "login_required")) {
|
|
1253
|
+
postconditionUnverified("AgentSession provider logout", {
|
|
1254
|
+
agent_session_id: id,
|
|
1255
|
+
observed_state: observedAuth.state,
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
return Object.freeze({
|
|
1259
|
+
command: "agent.logout",
|
|
1260
|
+
data: Object.freeze({
|
|
1261
|
+
observation_id: receipt.observationId,
|
|
1262
|
+
agent_session_id: receipt.agentSessionId,
|
|
1263
|
+
process_epoch: receipt.processEpoch,
|
|
1264
|
+
auth_mode: receipt.authMode,
|
|
1265
|
+
agent: receipt.agent,
|
|
1266
|
+
agent_version: receipt.agentVersion,
|
|
1267
|
+
adapter_version: receipt.adapterVersion,
|
|
1268
|
+
observed_at: receipt.observedAt,
|
|
1269
|
+
outcome: receipt.outcome,
|
|
1270
|
+
// Say which of the two happened. The sign-out is confirmed either
|
|
1271
|
+
// way; only one of them was independently observed afterwards.
|
|
1272
|
+
post_state: providerAbstains ? "unobserved" : "login_required",
|
|
1273
|
+
}),
|
|
1274
|
+
human: providerAbstains
|
|
1275
|
+
? `Signed out ${session.agent} in AgentSession ${session.id}. ${agentDisplayName(session.agent)} does not report a login state, so this was not independently re-checked.`
|
|
1276
|
+
: `Signed out ${session.agent} in AgentSession ${session.id}.`,
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
case "signup":
|
|
1280
|
+
case "login":
|
|
1281
|
+
case "logout":
|
|
1282
|
+
case "whoami":
|
|
1283
|
+
case "access":
|
|
1284
|
+
throw unsupportedError("browser authentication", "browser_auth_dispatch_unavailable");
|
|
1285
|
+
case "claude":
|
|
1286
|
+
case "codex":
|
|
1287
|
+
case "opencode":
|
|
1288
|
+
case "connect":
|
|
1289
|
+
// Public process dispatch is owned by runCli, which composes exact attach
|
|
1290
|
+
// and the automatic journey before this generic command dispatcher. A
|
|
1291
|
+
// direct executeCommand call has no TTY, sync lifecycle or credential
|
|
1292
|
+
// composition authority, so it must fail closed without claiming that
|
|
1293
|
+
// the shipped runtime itself is absent.
|
|
1294
|
+
throw unsupportedError("terminal workspace", "run_cli_composition_required");
|
|
1295
|
+
case "shell":
|
|
1296
|
+
throw unsupportedError("terminal workspace", "terminal_runtime_unavailable");
|
|
1297
|
+
case "sync":
|
|
1298
|
+
throw unsupportedError("workspace synchronization", "workspace_sync_runtime_unavailable");
|
|
1299
|
+
case "companion":
|
|
1300
|
+
throw unsupportedError("local companion", "local_companion_unavailable");
|
|
1301
|
+
case "doctor": {
|
|
1302
|
+
rejectUnknownOptions(parsed, ["check-browser-login"]);
|
|
1303
|
+
// `doctor` reads no credential, so an unusable one must not stop it —
|
|
1304
|
+
// that is the whole reason the refusal moved out of `resolveConfig`. It
|
|
1305
|
+
// reports the state instead, because a diagnostic that survives a broken
|
|
1306
|
+
// environment and then says nothing about it is no better than dying.
|
|
1307
|
+
const features = context.runtimeFeatures ?? INITIAL_RUNTIME_GATES;
|
|
1308
|
+
const data = Object.freeze({
|
|
1309
|
+
platform: process.platform,
|
|
1310
|
+
node: process.version,
|
|
1311
|
+
environment_credential: environmentCredentialState(config),
|
|
1312
|
+
environment_credential_variable: config.apiKeyVariable ?? null,
|
|
1313
|
+
runtime_features: features,
|
|
1314
|
+
});
|
|
1315
|
+
// `doctor` is the command the help text sends a stuck user to first, and
|
|
1316
|
+
// it answered with a JSON dump on a TTY. Every field the record carries is
|
|
1317
|
+
// still here; each feature now states its implementation AND the reason
|
|
1318
|
+
// code that names the prerequisite, on the line that reports it.
|
|
1319
|
+
return Object.freeze({
|
|
1320
|
+
command: "doctor",
|
|
1321
|
+
data,
|
|
1322
|
+
human: [
|
|
1323
|
+
`platform\t${data.platform}`,
|
|
1324
|
+
`node\t${data.node}`,
|
|
1325
|
+
`environment_credential\t${data.environment_credential}`,
|
|
1326
|
+
`environment_credential_variable\t${data.environment_credential_variable ?? "null"}`,
|
|
1327
|
+
"runtime_features",
|
|
1328
|
+
...features.map((gate) => ` ${gate.feature}\t${gate.implementation}\t${gate.reason}`),
|
|
1329
|
+
].join("\n"),
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
case "self-test": {
|
|
1333
|
+
rejectUnknownOptions(parsed, ["offline"]);
|
|
1334
|
+
if (parsed.operands.length !== 0)
|
|
1335
|
+
throw usageError("self-test accepts no operands.");
|
|
1336
|
+
if (!booleanOption(parsed, "offline")) {
|
|
1337
|
+
throw usageError("self-test requires --offline in this release.", "Run `cuna self-test --offline --json`.");
|
|
1338
|
+
}
|
|
1339
|
+
const runtimeSupport = evaluateRuntimeSupport({
|
|
1340
|
+
nodeVersion: process.versions.node,
|
|
1341
|
+
platform: process.platform,
|
|
1342
|
+
architecture: process.arch,
|
|
1343
|
+
});
|
|
1344
|
+
const buildDigest = await packageBuildDigest();
|
|
1345
|
+
const virtualTerminal = await verifyVirtualTerminalInterop();
|
|
1346
|
+
// `canonical_api_origin` used to live in `checks` as
|
|
1347
|
+
// `config.baseUrl === DEFAULT_BASE_URL || config.developmentProfile`.
|
|
1348
|
+
// That condition cannot be false: `normalizeBaseUrl` (config/config.ts)
|
|
1349
|
+
// returns `DEFAULT_BASE_URL` or throws unless a development profile is
|
|
1350
|
+
// active, so the check restated its own precondition. It is now reported
|
|
1351
|
+
// as what it always was — a configuration fact, not an integrity gate —
|
|
1352
|
+
// and `apiOriginIsCanonical` can and does read `false`.
|
|
1353
|
+
const checks = Object.freeze({
|
|
1354
|
+
node_runtime: runtimeSupport.nodeRuntime,
|
|
1355
|
+
supported_platform: runtimeSupport.platform,
|
|
1356
|
+
supported_architecture: runtimeSupport.architecture,
|
|
1357
|
+
package_identity: /^[0-9a-f]{64}$/u.test(buildDigest),
|
|
1358
|
+
virtual_terminal: virtualTerminal,
|
|
1359
|
+
network_requests: 0,
|
|
1360
|
+
});
|
|
1361
|
+
const ok = Object.values(checks).every((value) => value === true || value === 0);
|
|
1362
|
+
const data = Object.freeze({
|
|
1363
|
+
ok,
|
|
1364
|
+
mode: "offline",
|
|
1365
|
+
// `ok` answers one question: is the installed artifact intact and
|
|
1366
|
+
// admissible on this host? It said nothing about the six runtime
|
|
1367
|
+
// feature gates `doctor` reports, every one of which is currently
|
|
1368
|
+
// `unsupported` — so "Offline self-test passed." read as a verdict on
|
|
1369
|
+
// the product while covering only the installation.
|
|
1370
|
+
scope: "installation_integrity",
|
|
1371
|
+
notChecked: Object.freeze([
|
|
1372
|
+
"runtime_features",
|
|
1373
|
+
"server_contract",
|
|
1374
|
+
"credential_state",
|
|
1375
|
+
]),
|
|
1376
|
+
version: CLI_VERSION,
|
|
1377
|
+
buildDigest,
|
|
1378
|
+
platform: process.platform,
|
|
1379
|
+
architecture: process.arch,
|
|
1380
|
+
apiOrigin: config.baseUrl,
|
|
1381
|
+
apiOriginSource: config.baseUrlSource,
|
|
1382
|
+
apiOriginIsCanonical: config.baseUrl === DEFAULT_BASE_URL,
|
|
1383
|
+
updateChannel: ARTIFACT_CHANNEL,
|
|
1384
|
+
artifactChannel: ARTIFACT_CHANNEL,
|
|
1385
|
+
protocolRange: PROTOCOL_RANGE,
|
|
1386
|
+
checks,
|
|
1387
|
+
});
|
|
1388
|
+
if (!ok) {
|
|
1389
|
+
throw new CunaError({
|
|
1390
|
+
code: "cuna.self_test.failed",
|
|
1391
|
+
hint: `The installed CLI does not match its own build record. Reinstall the local Cuna .tgz package, and report it at ${SUPPORT_URL} if it recurs.`,
|
|
1392
|
+
message: "The installed Cuna CLI failed an offline integrity check.",
|
|
1393
|
+
exitCode: EXIT_CODES.internal,
|
|
1394
|
+
details: {
|
|
1395
|
+
failed_checks: Object.entries(checks)
|
|
1396
|
+
.filter(([, value]) => value !== true && value !== 0)
|
|
1397
|
+
.map(([name]) => name)
|
|
1398
|
+
.join(","),
|
|
1399
|
+
},
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
return Object.freeze({
|
|
1403
|
+
command: "self-test",
|
|
1404
|
+
data,
|
|
1405
|
+
human: "Offline self-test passed: installation integrity only. It does not check runtime feature availability — run `cuna doctor` for that.",
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
default:
|
|
1409
|
+
throw usageError(`Unknown command ${parsed.command ?? "<none>"}.`, "Run `cuna --help`.");
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
async function verifyVirtualTerminalInterop() {
|
|
1413
|
+
let viewport;
|
|
1414
|
+
try {
|
|
1415
|
+
const [{ ViewportRegistry }, { XtermViewportAdapter }] = await Promise.all([
|
|
1416
|
+
import("../terminal/viewport.js"),
|
|
1417
|
+
import("../terminal/xterm-vte.js"),
|
|
1418
|
+
]);
|
|
1419
|
+
const registry = new ViewportRegistry();
|
|
1420
|
+
viewport = new XtermViewportAdapter({
|
|
1421
|
+
tabId: "offline-self-test",
|
|
1422
|
+
binding: {
|
|
1423
|
+
userId: "offline",
|
|
1424
|
+
machineId: "offline",
|
|
1425
|
+
agentSessionId: "offline",
|
|
1426
|
+
processEpoch: "offline",
|
|
1427
|
+
fencingGeneration: 1,
|
|
1428
|
+
},
|
|
1429
|
+
columns: 20,
|
|
1430
|
+
rows: 2,
|
|
1431
|
+
scrollback: 0,
|
|
1432
|
+
registry,
|
|
1433
|
+
});
|
|
1434
|
+
const snapshot = await viewport.write(new TextEncoder().encode("cuna"), 1n, 1n);
|
|
1435
|
+
return snapshot.cells[0] === "cuna";
|
|
1436
|
+
}
|
|
1437
|
+
catch {
|
|
1438
|
+
return false;
|
|
1439
|
+
}
|
|
1440
|
+
finally {
|
|
1441
|
+
viewport?.dispose();
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Where this installation keeps its note about an in-place update it dispatched
|
|
1446
|
+
* and never saw settle.
|
|
1447
|
+
*
|
|
1448
|
+
* The adapter is optional on `CommandContext`, and its absence is a refusal
|
|
1449
|
+
* rather than a silent fall-through: dispatching a mutation this command could
|
|
1450
|
+
* not record would leave the next invocation free to repeat it.
|
|
1451
|
+
*/
|
|
1452
|
+
function liveUpdateNotesFor(context, platform) {
|
|
1453
|
+
// Two conditions, one refusal. The adapter may be absent entirely, or it may
|
|
1454
|
+
// predate the exclusive-create primitive the reservation is built on — and a
|
|
1455
|
+
// store that can only read-then-write is not a reservation at all.
|
|
1456
|
+
if (platform === undefined || typeof platform.createExclusiveConfig !== "function") {
|
|
1457
|
+
throw new CunaError({
|
|
1458
|
+
code: "cuna.machine.live_supervisor_update_unrecordable",
|
|
1459
|
+
message: "Cuna cannot record an in-place supervisor update on this host.",
|
|
1460
|
+
exitCode: EXIT_CODES.internal,
|
|
1461
|
+
hint: "Nothing was sent. This command reserves a local record before it dispatches, so that a second invocation cannot repeat an update whose outcome is unknown, and it will not start one it cannot reserve.",
|
|
1462
|
+
details: { reason: platform === undefined ? "no_platform_adapter" : "no_exclusive_reservation" },
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
// Scoped by the API origin alone. The profile is deliberately absent: the
|
|
1466
|
+
// suppression protects a Machine, and selecting a different local profile
|
|
1467
|
+
// against the same API must not clear it.
|
|
1468
|
+
return liveSupervisorUpdateNotes(platform, { baseUrl: context.config.baseUrl });
|
|
1469
|
+
}
|
|
1470
|
+
function outstandingLiveUpdate(machineId, operationId, dispatchedAt) {
|
|
1471
|
+
return new CunaError({
|
|
1472
|
+
code: "cuna.machine.live_supervisor_update_outcome_unknown",
|
|
1473
|
+
message: `An in-place supervisor update for Machine ${machineId} dispatched at ${dispatchedAt} has no known outcome on this computer.`,
|
|
1474
|
+
exitCode: EXIT_CODES.conflict,
|
|
1475
|
+
// The authoritative read comes first now, because one exists. It sends
|
|
1476
|
+
// nothing to the Machine and is the only thing that can say what this
|
|
1477
|
+
// operation did. Starting a NEW update is still absent from this list, and
|
|
1478
|
+
// so is switching profiles: the record covers this Machine on this API for
|
|
1479
|
+
// every profile.
|
|
1480
|
+
hint: `Read it with \`cuna machines live-update-status ${machineId}\`, which sends nothing to the Machine. If it reports next_action repeat_same_operation, resolve it with \`cuna machines live-update-supervisor ${machineId} --resume\`, which re-sends this same operation ${operationId} and never rotates control a second time. Cuna will not start a different update to find out, and another profile on this computer will not either.`,
|
|
1481
|
+
retryable: false,
|
|
1482
|
+
details: {
|
|
1483
|
+
machine_id: machineId,
|
|
1484
|
+
operation_id: operationId,
|
|
1485
|
+
dispatched_at: dispatchedAt,
|
|
1486
|
+
outcome: "unknown",
|
|
1487
|
+
},
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* What the producer's own record says about an operation whose answer was lost.
|
|
1492
|
+
*
|
|
1493
|
+
* Called only from the refusal path, and deliberately best-effort: it is one
|
|
1494
|
+
* bounded read that dispatches no installer, and a read that itself fails must
|
|
1495
|
+
* never replace the refusal the caller is owed with a story about a second
|
|
1496
|
+
* request. `undefined` means "this CLI could not ask", which is not "nothing
|
|
1497
|
+
* happened".
|
|
1498
|
+
*/
|
|
1499
|
+
async function reconcileLiveUpdateOperation(client, machineId, operationId) {
|
|
1500
|
+
const answer = await readLiveUpdateOperationAnswer(client, machineId, operationId);
|
|
1501
|
+
return answer.state === "read" ? answer.operation : undefined;
|
|
1502
|
+
}
|
|
1503
|
+
async function readLiveUpdateOperationAnswer(client, machineId, operationId) {
|
|
1504
|
+
try {
|
|
1505
|
+
return Object.freeze({
|
|
1506
|
+
state: "read",
|
|
1507
|
+
operation: await client.readMachineSupervisorInPlaceUpdate(machineId, operationId),
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
catch (error) {
|
|
1511
|
+
const reason = error instanceof CunaError ? error.details?.["reason"] : undefined;
|
|
1512
|
+
if (reason === "resource_not_found")
|
|
1513
|
+
return Object.freeze({ state: "absent" });
|
|
1514
|
+
return Object.freeze({ state: "unavailable" });
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
/** The per-AgentSession rows a record and a human line are both built from. */
|
|
1518
|
+
function liveUpdateSessionRecords(sessions) {
|
|
1519
|
+
return Object.freeze(sessions.map((session) => Object.freeze({
|
|
1520
|
+
agent_session_id: session.agentSessionId,
|
|
1521
|
+
process_epoch: session.processEpoch,
|
|
1522
|
+
...(session.outcome === undefined ? {} : { outcome: session.outcome }),
|
|
1523
|
+
})));
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* One operation, as a record and as prose.
|
|
1527
|
+
*
|
|
1528
|
+
* Two rules it holds and a renderer would otherwise be free to break: a
|
|
1529
|
+
* withheld per-session account is stated as withheld rather than expanded into
|
|
1530
|
+
* `unknown` outcomes, and `installed_at` is described by its evidence, because
|
|
1531
|
+
* `reconciled` says when Cuna LOOKED and not when the artifact arrived.
|
|
1532
|
+
*/
|
|
1533
|
+
function liveUpdateOperationReport(operation, machineId) {
|
|
1534
|
+
const reading = readLiveSupervisorUpdateOperation(operation);
|
|
1535
|
+
const data = {
|
|
1536
|
+
machine_id: machineId,
|
|
1537
|
+
operation_id: operation.operationId,
|
|
1538
|
+
phase: operation.phase,
|
|
1539
|
+
control_rotated: operation.controlRotated,
|
|
1540
|
+
installer_outcome: operation.installerOutcome,
|
|
1541
|
+
...(operation.controlGeneration === undefined
|
|
1542
|
+
? {}
|
|
1543
|
+
: { control_generation: operation.controlGeneration }),
|
|
1544
|
+
...(operation.artifactSha256 === undefined ? {} : { artifact_sha256: operation.artifactSha256 }),
|
|
1545
|
+
...(operation.installedAt === undefined ? {} : { installed_at: operation.installedAt }),
|
|
1546
|
+
...(operation.installationEvidence === undefined
|
|
1547
|
+
? {}
|
|
1548
|
+
: { installation_evidence: operation.installationEvidence }),
|
|
1549
|
+
declared_sessions: liveUpdateSessionRecords(operation.declaredSessions),
|
|
1550
|
+
agent_sessions: liveUpdateSessionRecords(operation.sessions),
|
|
1551
|
+
session_account_withheld: reading.accountWithheld,
|
|
1552
|
+
...(operation.failure === undefined
|
|
1553
|
+
? {}
|
|
1554
|
+
: {
|
|
1555
|
+
failure: Object.freeze({
|
|
1556
|
+
status: operation.failure.status,
|
|
1557
|
+
code: operation.failure.code,
|
|
1558
|
+
title: operation.failure.title,
|
|
1559
|
+
detail: operation.failure.detail,
|
|
1560
|
+
retryable: operation.failure.retryable,
|
|
1561
|
+
action: operation.failure.action,
|
|
1562
|
+
}),
|
|
1563
|
+
}),
|
|
1564
|
+
next_action: operation.nextAction,
|
|
1565
|
+
claimed_at: operation.claimedAt,
|
|
1566
|
+
updated_at: operation.updatedAt,
|
|
1567
|
+
...(operation.settledAt === undefined ? {} : { settled_at: operation.settledAt }),
|
|
1568
|
+
...(operation.retiredAt === undefined ? {} : { retired_at: operation.retiredAt }),
|
|
1569
|
+
...(operation.retirementOutcome === undefined
|
|
1570
|
+
? {}
|
|
1571
|
+
: { retirement_outcome: operation.retirementOutcome }),
|
|
1572
|
+
// The fence, as a decided value rather than two nullable fields a caller
|
|
1573
|
+
// has to combine. `installer_can_still_act` is the one an automated caller
|
|
1574
|
+
// needs before touching this Machine, and it is false ONLY on the producer's
|
|
1575
|
+
// own evidence -- never inferred from the phase or from a running Machine.
|
|
1576
|
+
installer_reach: reading.installerReach,
|
|
1577
|
+
installer_can_still_act: reading.installerCanStillAct,
|
|
1578
|
+
// Stated in the record because a caller reading only `--json` gets no prose,
|
|
1579
|
+
// and because this is the sentence the producer is most emphatic about.
|
|
1580
|
+
machine_running_implies_installed: false,
|
|
1581
|
+
// The same rule one layer out: the fence's record of an admitted installer
|
|
1582
|
+
// that finished is not Cuna's observation of an installation, and only
|
|
1583
|
+
// `installer_outcome` is that.
|
|
1584
|
+
retirement_outcome_implies_installed: false,
|
|
1585
|
+
};
|
|
1586
|
+
const lines = [
|
|
1587
|
+
`Operation ${operation.operationId} on Machine ${machineId}.`,
|
|
1588
|
+
`Phase ${liveSupervisorUpdatePhaseLabel(operation.phase)}.`,
|
|
1589
|
+
`Installer ${liveSupervisorInstallerOutcomeLabel(operation.installerOutcome, reading.installerReach)}.`,
|
|
1590
|
+
operation.controlRotated
|
|
1591
|
+
? "This Machine's supervisor control HAS rotated for this update. That is irreversible and stays true even if nothing was installed."
|
|
1592
|
+
: "This Machine's supervisor control has not rotated for this update.",
|
|
1593
|
+
...(operation.artifactSha256 === undefined
|
|
1594
|
+
? ["No supervisor artifact is bound to this operation yet."]
|
|
1595
|
+
: [`Target artifact ${operation.artifactSha256}.`]),
|
|
1596
|
+
...(operation.controlGeneration === undefined
|
|
1597
|
+
? []
|
|
1598
|
+
: [`Control generation ${operation.controlGeneration}.`]),
|
|
1599
|
+
...(operation.installedAt === undefined || operation.installationEvidence === undefined
|
|
1600
|
+
? []
|
|
1601
|
+
: [`Installation established ${operation.installedAt} by ${liveSupervisorInstallationEvidenceLabel(operation.installationEvidence)}.`]),
|
|
1602
|
+
reading.declared === 0
|
|
1603
|
+
? "This operation has not bound the AgentSessions it is accountable for."
|
|
1604
|
+
: `It measured ${reading.declared} AgentSession${reading.declared === 1 ? "" : "s"}:`,
|
|
1605
|
+
...operation.declaredSessions.map((session) => ` ${session.agentSessionId} process epoch ${session.processEpoch}`),
|
|
1606
|
+
...(reading.accountWithheld
|
|
1607
|
+
? ["Cuna has settled no per-AgentSession account for this operation. That is an answer it has not given, not a set of unknown outcomes."]
|
|
1608
|
+
: reading.accounted === 0
|
|
1609
|
+
? []
|
|
1610
|
+
: [
|
|
1611
|
+
`Per-AgentSession account, ${reading.accounted} of ${reading.declared} measured:`,
|
|
1612
|
+
...operation.sessions.map((session) => ` ${session.agentSessionId} ${liveSupervisorSessionOutcomeLabel(session.outcome)}`),
|
|
1613
|
+
]),
|
|
1614
|
+
...(operation.failure === undefined
|
|
1615
|
+
? []
|
|
1616
|
+
: [`Recorded refusal ${operation.failure.code} (HTTP ${operation.failure.status}): ${operation.failure.title}. ${operation.failure.detail}`]),
|
|
1617
|
+
"A running Machine is not evidence that this update did or did not apply. This read is.",
|
|
1618
|
+
// The install fence, ordered after the refusal and before the next action:
|
|
1619
|
+
// it is what decides whether "nothing further will change it" also means
|
|
1620
|
+
// "nothing further will change this Machine because of it".
|
|
1621
|
+
...(operation.retiredAt === undefined
|
|
1622
|
+
? []
|
|
1623
|
+
: [`Installer retired on the Machine ${operation.retiredAt}.`]),
|
|
1624
|
+
...liveSupervisorInstallerReachLines(reading, machineId),
|
|
1625
|
+
reading.mayRepeatSameOperation
|
|
1626
|
+
? `Next action: repeat this same operation. Run \`cuna machines live-update-supervisor ${machineId} --resume\`. Starting a different update is refused while this one is open.`
|
|
1627
|
+
: "Next action: none. This operation is settled and nothing further will change it.",
|
|
1628
|
+
];
|
|
1629
|
+
return Object.freeze({ data, lines: Object.freeze(lines) });
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* `machines live-update-supervisor` — the running-Machine supervisor update.
|
|
1633
|
+
*
|
|
1634
|
+
* The three rules that shape this function, in the order they bind:
|
|
1635
|
+
*
|
|
1636
|
+
* 1. Preservation is conditional on the SERVER's preflight, never on anything
|
|
1637
|
+
* read here. The local state check below is a cheap refusal that names the
|
|
1638
|
+
* other command; it is not the authority and does not weaken one.
|
|
1639
|
+
* 2. A 200 is an account, not a success. Every AgentSession outcome is
|
|
1640
|
+
* reported, and anything other than `preserved` is said plainly.
|
|
1641
|
+
* 3. An outcome this process did not learn is `unknown`. It is never retried,
|
|
1642
|
+
* never reconciled with a second mutation, and never resolved by falling
|
|
1643
|
+
* back to the stopped-Machine replacement.
|
|
1644
|
+
*/
|
|
1645
|
+
async function executeLiveUpdateSupervisor(context) {
|
|
1646
|
+
const { parsed, client, now } = context;
|
|
1647
|
+
const { intent } = preflightLiveUpdateSupervisor(parsed);
|
|
1648
|
+
const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
|
|
1649
|
+
const notes = liveUpdateNotesFor(context, context.platform);
|
|
1650
|
+
if (intent === "forget-unknown")
|
|
1651
|
+
return clearLiveUpdateRecord(context, notes, id);
|
|
1652
|
+
const reading = await notes.read(id);
|
|
1653
|
+
if (reading.state === "unreadable") {
|
|
1654
|
+
// Fail closed and name the way out, for both sending intents. The record may
|
|
1655
|
+
// describe a dispatch that is still in flight, so this is not a corrupt file
|
|
1656
|
+
// to step over -- and Cuna cannot read an operation identity out of it, so
|
|
1657
|
+
// there is nothing to resume either.
|
|
1658
|
+
throw new CunaError({
|
|
1659
|
+
code: "cuna.machine.live_supervisor_update_record_unreadable",
|
|
1660
|
+
message: `This computer holds a supervisor update record for Machine ${id} that Cuna cannot read.`,
|
|
1661
|
+
exitCode: EXIT_CODES.conflict,
|
|
1662
|
+
hint: `Nothing was sent. That record may describe an update whose outcome is still unknown, and Cuna cannot read the operation identity out of it to ask. Clear it with \`cuna machines live-update-supervisor ${id} --forget-unknown\`, which works on an unreadable record, and read \`cuna machines list\` and \`cuna agent-sessions list --machine ${id}\` before starting another update.`,
|
|
1663
|
+
retryable: false,
|
|
1664
|
+
details: {
|
|
1665
|
+
machine_id: id,
|
|
1666
|
+
record_path: reading.path,
|
|
1667
|
+
reason: reading.reason,
|
|
1668
|
+
outcome: "unknown",
|
|
1669
|
+
},
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
if (intent === "resume") {
|
|
1673
|
+
// A caller-known identity, for an update this computer never recorded. The
|
|
1674
|
+
// cross-client half of recovery: an owner who started one in the console, or
|
|
1675
|
+
// on another computer, holds its id and nothing else.
|
|
1676
|
+
const named = stringOption(parsed, "operation");
|
|
1677
|
+
if (named === undefined && reading.state === "none") {
|
|
1678
|
+
throw new CunaError({
|
|
1679
|
+
code: "cuna.machine.live_supervisor_update_nothing_to_resume",
|
|
1680
|
+
message: `This computer holds no in-place supervisor update record for Machine ${id}.`,
|
|
1681
|
+
exitCode: EXIT_CODES.conflict,
|
|
1682
|
+
hint: `Nothing was sent. --resume re-sends an operation identity, and there is none recorded for ${id} on ${context.config.baseUrl}. An update started from another computer or from the web console has an identity this CLI never held: name it with \`cuna machines live-update-supervisor ${id} --resume --operation UUID\`, or read it first with \`cuna machines live-update-status ${id} --operation UUID\`. To start a new update, run \`cuna machines live-update-supervisor ${id} --yes\`.`,
|
|
1683
|
+
retryable: false,
|
|
1684
|
+
details: { machine_id: id, outcome: "not_sent" },
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
if (named !== undefined && reading.state === "outstanding" && reading.note.operationId !== named) {
|
|
1688
|
+
// A live reservation for a different identity. It is not this command's
|
|
1689
|
+
// to overwrite, and the exclusive create below would refuse anyway.
|
|
1690
|
+
throw new CunaError({
|
|
1691
|
+
code: "cuna.machine.live_supervisor_update_already_reserved",
|
|
1692
|
+
message: `This computer is already holding in-place supervisor update ${reading.note.operationId} for Machine ${id}.`,
|
|
1693
|
+
exitCode: EXIT_CODES.conflict,
|
|
1694
|
+
hint: `Nothing was sent. Resolve the update this computer recorded before taking on another: read it with \`cuna machines live-update-status ${id}\`. Two updates must never rotate one Machine's control at once.`,
|
|
1695
|
+
retryable: false,
|
|
1696
|
+
details: {
|
|
1697
|
+
machine_id: id,
|
|
1698
|
+
operation_id: reading.note.operationId,
|
|
1699
|
+
requested_operation_id: named,
|
|
1700
|
+
outcome: "not_sent",
|
|
1701
|
+
},
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
const operationId = named ?? reading.note.operationId;
|
|
1705
|
+
/* Read the authoritative status BEFORE repeating anything. Two reasons, and
|
|
1706
|
+
the second is the one a reviewer drove out:
|
|
1707
|
+
- the producer's own `next_action` is what says a repeat resolves this,
|
|
1708
|
+
so offering one without asking would be the client deciding;
|
|
1709
|
+
- a repeat of an identity the producer never journalled is that
|
|
1710
|
+
identity's FIRST admission and runs a whole update. Calling that a
|
|
1711
|
+
"repeat" was true of the command and false of the effect. */
|
|
1712
|
+
const status = await readLiveUpdateOperationAnswer(client, id, operationId);
|
|
1713
|
+
if (status.state === "absent") {
|
|
1714
|
+
/* A not-found is two facts, and only the record's own account label tells
|
|
1715
|
+
them apart. The SAME account can see its own operations, so this really
|
|
1716
|
+
is "never admitted". A different account, or a record that names none,
|
|
1717
|
+
and a caller-supplied identity that no record backs, are all cases where
|
|
1718
|
+
the operation may exist and simply be out of view -- and saying it was
|
|
1719
|
+
never admitted there would be the false half of the same coin the
|
|
1720
|
+
reviewer caught on the other side. */
|
|
1721
|
+
const recordAccount = reading.state === "outstanding" ? reading.note.account : undefined;
|
|
1722
|
+
const signedIn = await liveUpdateAccountLabel(client);
|
|
1723
|
+
const neverAdmitted = named === undefined && recordAccount !== undefined && signedIn !== undefined &&
|
|
1724
|
+
recordAccount === signedIn;
|
|
1725
|
+
throw new CunaError({
|
|
1726
|
+
code: neverAdmitted
|
|
1727
|
+
? "cuna.machine.live_supervisor_update_nothing_to_resume"
|
|
1728
|
+
: "cuna.machine.live_supervisor_update_operation_inaccessible",
|
|
1729
|
+
message: neverAdmitted
|
|
1730
|
+
? `Cuna has no in-place supervisor update ${operationId} for Machine ${id} under this account, which is the account that recorded it.`
|
|
1731
|
+
: `The signed-in account cannot read in-place supervisor update ${operationId} on Machine ${id}.`,
|
|
1732
|
+
exitCode: EXIT_CODES.conflict,
|
|
1733
|
+
hint: neverAdmitted
|
|
1734
|
+
? `Nothing was sent. There is nothing to resume: this identity was never admitted, so re-sending it would be a NEW update's first admission rather than finishing one -- and Cuna will not do that under a flag that says "resume". Clear the record with \`cuna machines live-update-supervisor ${id} --forget-unknown\`, then start one with \`--yes\`.`
|
|
1735
|
+
: `Nothing was sent, and this is NOT evidence that the operation never existed: Cuna answers an operation outside this account's view exactly as it answers one that never existed. ${recordAccount === undefined
|
|
1736
|
+
? "This computer's record does not name the account that filed it."
|
|
1737
|
+
: `This computer's record was filed under account ${recordAccount}.`} Check the signed-in account with \`cuna whoami\`, sign in as the Machine's owner, then read it with \`cuna machines live-update-status ${id}${named === undefined ? "" : ` --operation ${operationId}`}\`.`,
|
|
1738
|
+
retryable: false,
|
|
1739
|
+
details: {
|
|
1740
|
+
machine_id: id,
|
|
1741
|
+
operation_id: operationId,
|
|
1742
|
+
outcome: neverAdmitted ? "not_sent" : "inaccessible",
|
|
1743
|
+
local_record_cleared: false,
|
|
1744
|
+
...(neverAdmitted ? { operation_admitted: false } : {}),
|
|
1745
|
+
...(recordAccount === undefined ? {} : { record_account: recordAccount }),
|
|
1746
|
+
...(signedIn === undefined ? {} : { signed_in_account: signedIn }),
|
|
1747
|
+
},
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
if (status.state === "read" && status.operation.nextAction === "none") {
|
|
1751
|
+
const report = liveUpdateOperationReport(status.operation, id);
|
|
1752
|
+
// Settled: repeating returns this same recorded answer, so the honest
|
|
1753
|
+
// action is to show it and release the record rather than re-send.
|
|
1754
|
+
if (reading.state === "outstanding" && reading.note.operationId === operationId) {
|
|
1755
|
+
await notes.settle(id, operationId);
|
|
1756
|
+
}
|
|
1757
|
+
return Object.freeze({
|
|
1758
|
+
command: "machines.live-update-status",
|
|
1759
|
+
data: Object.freeze({
|
|
1760
|
+
...report.data,
|
|
1761
|
+
operation_id_source: named === undefined ? "local_record" : "named",
|
|
1762
|
+
dispatched_installer: false,
|
|
1763
|
+
server_state_changed: false,
|
|
1764
|
+
resumed: false,
|
|
1765
|
+
}),
|
|
1766
|
+
human: [
|
|
1767
|
+
"Nothing was re-sent: this update is settled, and repeating it would return this same recorded answer.",
|
|
1768
|
+
...report.lines,
|
|
1769
|
+
].join("\n"),
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
/* A caller-known identity needs the same reservation every dispatch takes,
|
|
1773
|
+
so a concurrent invocation cannot send it at the same moment. An identity
|
|
1774
|
+
this computer already records IS the reservation, and re-reserving it
|
|
1775
|
+
would lose to itself. */
|
|
1776
|
+
let recordedAccount = reading.state === "outstanding" ? reading.note.account : undefined;
|
|
1777
|
+
if (reading.state === "none") {
|
|
1778
|
+
const account = await liveUpdateAccountLabel(client);
|
|
1779
|
+
const reserved = await notes.reserve(id, operationId, new Date(now).toISOString(), account);
|
|
1780
|
+
if (reserved === undefined) {
|
|
1781
|
+
throw new CunaError({
|
|
1782
|
+
code: "cuna.machine.live_supervisor_update_already_reserved",
|
|
1783
|
+
message: `Another Cuna process on this computer is already updating the supervisor of Machine ${id}.`,
|
|
1784
|
+
exitCode: EXIT_CODES.conflict,
|
|
1785
|
+
hint: "Nothing was sent. Let that invocation finish and read its answer; two in-place updates of one Machine must not run together.",
|
|
1786
|
+
retryable: false,
|
|
1787
|
+
details: { machine_id: id, operation_id: operationId, outcome: "not_sent" },
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
recordedAccount = account;
|
|
1791
|
+
}
|
|
1792
|
+
// No fresh identity: the whole point of this path is that it does not
|
|
1793
|
+
// change. The producer fences concurrent advances of one identity itself,
|
|
1794
|
+
// and refuses outright to issue a second enrollment under it.
|
|
1795
|
+
return dispatchLiveUpdate(context, notes, id, {
|
|
1796
|
+
operationId,
|
|
1797
|
+
dispatchedAt: reading.state === "outstanding" ? reading.note.dispatchedAt : new Date(now).toISOString(),
|
|
1798
|
+
origin: "recorded_identity",
|
|
1799
|
+
...(recordedAccount === undefined ? {} : { recordedAccount }),
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
if (reading.state === "outstanding") {
|
|
1803
|
+
throw outstandingLiveUpdate(id, reading.note.operationId, reading.note.dispatchedAt);
|
|
1804
|
+
}
|
|
1805
|
+
// The same `machines:update` authority the stopped-boundary replacement takes.
|
|
1806
|
+
// The deliberately unsupported AgentSession-create capability is NOT
|
|
1807
|
+
// substituted for it: this operation is not an OpenCode remediation.
|
|
1808
|
+
await requireCapability({
|
|
1809
|
+
client, scope: "machine", resourceId: id, capabilityId: "machines.lifecycle",
|
|
1810
|
+
now: context.capabilityClock ?? now,
|
|
1811
|
+
});
|
|
1812
|
+
const current = await client.getMachine(id);
|
|
1813
|
+
if (current.state !== "running") {
|
|
1814
|
+
throw new CunaError({
|
|
1815
|
+
code: "cuna.machine.live_supervisor_update_requires_running",
|
|
1816
|
+
message: "The in-place supervisor update replaces the supervisor while the Machine keeps running.",
|
|
1817
|
+
exitCode: EXIT_CODES.conflict,
|
|
1818
|
+
hint: `Cuna will not start ${current.name} to satisfy this command. For a stopped Machine use \`cuna machines update-supervisor ${id} --yes\`, which is the separate stopped-boundary action.`,
|
|
1819
|
+
details: { machine_id: id, observed_state: current.state },
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
// Chosen HERE, before anything is sent, because a caller that learns its
|
|
1823
|
+
// operation identity from the response cannot use it when the response is the
|
|
1824
|
+
// thing that was lost.
|
|
1825
|
+
const operationId = randomUUID();
|
|
1826
|
+
const dispatchedAt = new Date(now).toISOString();
|
|
1827
|
+
// A label, recorded only because the two reads above already established that
|
|
1828
|
+
// the API is answering, and never consulted to admit anything. It exists for
|
|
1829
|
+
// one sentence: when a later read of this record is refused as not-found, the
|
|
1830
|
+
// owner is told the record was filed under a different account rather than
|
|
1831
|
+
// that the operation never existed.
|
|
1832
|
+
const account = await liveUpdateAccountLabel(client);
|
|
1833
|
+
// The reservation is the gate, not the read above. Between that read and this
|
|
1834
|
+
// line another process on this computer may have taken the slot, and only an
|
|
1835
|
+
// exclusive create can say which of them did. `undefined` means it lost.
|
|
1836
|
+
const reserved = await notes.reserve(id, operationId, dispatchedAt, account);
|
|
1837
|
+
if (reserved === undefined) {
|
|
1838
|
+
const rival = await notes.read(id);
|
|
1839
|
+
throw rival.state === "outstanding"
|
|
1840
|
+
? outstandingLiveUpdate(id, rival.note.operationId, rival.note.dispatchedAt)
|
|
1841
|
+
: new CunaError({
|
|
1842
|
+
code: "cuna.machine.live_supervisor_update_already_reserved",
|
|
1843
|
+
message: `Another Cuna process on this computer is already updating the supervisor of Machine ${id}.`,
|
|
1844
|
+
exitCode: EXIT_CODES.conflict,
|
|
1845
|
+
hint: "Nothing was sent. Let that invocation finish and read its answer; two in-place updates of one Machine must not run together.",
|
|
1846
|
+
retryable: false,
|
|
1847
|
+
details: { machine_id: id, outcome: "not_sent" },
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
return dispatchLiveUpdate(context, notes, id, {
|
|
1851
|
+
operationId,
|
|
1852
|
+
dispatchedAt,
|
|
1853
|
+
origin: "new_identity",
|
|
1854
|
+
...(account === undefined ? {} : { recordedAccount: account }),
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* The account this dispatch is filed under, when asking costs nothing.
|
|
1859
|
+
*
|
|
1860
|
+
* Best effort on purpose. A label that cannot be fetched is omitted, never
|
|
1861
|
+
* waited for and never a reason to refuse: the record must stay writable in
|
|
1862
|
+
* exactly the state it exists for, and this runs only after two successful
|
|
1863
|
+
* reads have already established that the API is answering.
|
|
1864
|
+
*/
|
|
1865
|
+
async function liveUpdateAccountLabel(client) {
|
|
1866
|
+
try {
|
|
1867
|
+
return (await client.getIdentity()).id;
|
|
1868
|
+
}
|
|
1869
|
+
catch {
|
|
1870
|
+
return undefined;
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* Send one in-place update under an identity that is already recorded.
|
|
1875
|
+
*
|
|
1876
|
+
* Shared by the two intents that send, because the only difference between them
|
|
1877
|
+
* is where the identity came from -- and that difference is carried explicitly
|
|
1878
|
+
* in `origin` rather than re-derived, because it decides whether a not-found
|
|
1879
|
+
* answer means "nothing was admitted" or "this account cannot see it".
|
|
1880
|
+
*/
|
|
1881
|
+
async function dispatchLiveUpdate(context, notes, id, dispatch) {
|
|
1882
|
+
const { client } = context;
|
|
1883
|
+
const { operationId, origin } = dispatch;
|
|
1884
|
+
const repeat = origin === "recorded_identity";
|
|
1885
|
+
let result;
|
|
1886
|
+
try {
|
|
1887
|
+
result = await client.updateMachineSupervisorInPlace(id, operationId);
|
|
1888
|
+
}
|
|
1889
|
+
catch (error) {
|
|
1890
|
+
let disposition = classifyLiveSupervisorUpdateFailure(error, origin);
|
|
1891
|
+
// Dropping the note is bookkeeping; the outcome is the answer. A local
|
|
1892
|
+
// filesystem fault must never replace the producer's own refusal with a
|
|
1893
|
+
// story about this computer's state directory, and a note that survives
|
|
1894
|
+
// fails in the safe direction.
|
|
1895
|
+
//
|
|
1896
|
+
// Bound to THIS identity: `settle` removes the record only when the record
|
|
1897
|
+
// names the operation this invocation sent. A record another process wrote
|
|
1898
|
+
// is not ours to remove.
|
|
1899
|
+
const drop = async () => {
|
|
1900
|
+
try {
|
|
1901
|
+
await notes.settle(id, operationId);
|
|
1902
|
+
}
|
|
1903
|
+
catch { /* the outcome below is the answer */ }
|
|
1904
|
+
};
|
|
1905
|
+
if (disposition === "not_admitted_machine_busy") {
|
|
1906
|
+
/* The producer's claim ordering says nothing was journalled under this
|
|
1907
|
+
identity, and the producer itself can confirm it. One bounded read that
|
|
1908
|
+
dispatches no installer and changes no state turns a reading of the
|
|
1909
|
+
ordering into an answer before a record is released. Only `absent` is
|
|
1910
|
+
that answer; a row, or a read that could not ask, keeps the record. */
|
|
1911
|
+
const answer = await readLiveUpdateOperationAnswer(client, id, operationId);
|
|
1912
|
+
if (answer.state !== "absent")
|
|
1913
|
+
disposition = "operation_conflict";
|
|
1914
|
+
}
|
|
1915
|
+
if (!liveSupervisorUpdateRecordSurvives(disposition, origin)) {
|
|
1916
|
+
await drop();
|
|
1917
|
+
// Nothing was admitted under an identity this process had just minted, so
|
|
1918
|
+
// the producer's own words are already the best available explanation.
|
|
1919
|
+
if (disposition === "not_admitted")
|
|
1920
|
+
throw error;
|
|
1921
|
+
if (disposition === "not_admitted_machine_busy") {
|
|
1922
|
+
throw new CunaError({
|
|
1923
|
+
code: "cuna.machine.live_supervisor_update_machine_busy",
|
|
1924
|
+
message: `Another in-place supervisor update is already running on Machine ${id}.`,
|
|
1925
|
+
exitCode: EXIT_CODES.conflict,
|
|
1926
|
+
hint: `Nothing was sent to this Machine and Cuna recorded no update for you: it refused before admitting one, and confirmed by reading that it has no operation ${operationId}. Two updates must never rotate one Machine's control at once. Read the one that is running with \`cuna machines live-update-status ${id} --operation UUID\` if you know its identity, and start yours again once it settles.`,
|
|
1927
|
+
retryable: true,
|
|
1928
|
+
details: { machine_id: id, operation_id: operationId, outcome: "not_admitted", local_record_cleared: true },
|
|
1929
|
+
cause: error,
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
throw new CunaError({
|
|
1933
|
+
code: disposition === "applied_with_ended_sessions"
|
|
1934
|
+
? "cuna.machine.live_supervisor_update_ended_sessions"
|
|
1935
|
+
: "cuna.machine.live_supervisor_update_control_rotated",
|
|
1936
|
+
message: disposition === "applied_with_ended_sessions"
|
|
1937
|
+
? `The in-place supervisor update on Machine ${id} was installed and ended AgentSessions.`
|
|
1938
|
+
: `The in-place supervisor update on Machine ${id} rotated its supervisor control and installed nothing.`,
|
|
1939
|
+
exitCode: EXIT_CODES.conflict,
|
|
1940
|
+
hint: `${error instanceof CunaError && error.hint !== undefined ? error.hint : "The update cannot be undone."} This operation is settled, so repeating this identity returns this same answer. Read \`cuna agent-sessions list --machine ${id}\`.`,
|
|
1941
|
+
retryable: false,
|
|
1942
|
+
details: { machine_id: id, operation_id: operationId, outcome: disposition },
|
|
1943
|
+
cause: error,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
// The record stays. Before reporting an unknown effect, ask the one thing
|
|
1947
|
+
// that can answer: a read that dispatches no installer and changes no state.
|
|
1948
|
+
// "Reconcile uncertain mutations before retrying" is the rule; this is the
|
|
1949
|
+
// reconciliation, and it is bounded to a single request.
|
|
1950
|
+
const reconciled = await reconcileLiveUpdateOperation(client, id, operationId);
|
|
1951
|
+
if (reconciled !== undefined) {
|
|
1952
|
+
const report = liveUpdateOperationReport(reconciled, id);
|
|
1953
|
+
const settled = reconciled.phase === "settled";
|
|
1954
|
+
if (settled)
|
|
1955
|
+
await drop();
|
|
1956
|
+
throw new CunaError({
|
|
1957
|
+
code: settled
|
|
1958
|
+
? "cuna.machine.live_supervisor_update_settled_refusal"
|
|
1959
|
+
: "cuna.machine.live_supervisor_update_outcome_unknown",
|
|
1960
|
+
message: settled
|
|
1961
|
+
? `In-place supervisor update ${operationId} on Machine ${id} is settled, and its recorded answer is a refusal.`
|
|
1962
|
+
: `In-place supervisor update ${operationId} on Machine ${id} has not settled.`,
|
|
1963
|
+
exitCode: EXIT_CODES.conflict,
|
|
1964
|
+
hint: `${report.lines.join(" ")} ${settled
|
|
1965
|
+
? "This computer's record was cleared because the operation is terminal."
|
|
1966
|
+
: "This computer keeps the record, so the identity survives."}`,
|
|
1967
|
+
retryable: false,
|
|
1968
|
+
details: {
|
|
1969
|
+
operation_outcome: settled ? "settled" : "open",
|
|
1970
|
+
local_record_cleared: settled,
|
|
1971
|
+
...report.data,
|
|
1972
|
+
},
|
|
1973
|
+
cause: error,
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
if (disposition === "operation_inaccessible") {
|
|
1977
|
+
throw new CunaError({
|
|
1978
|
+
code: "cuna.machine.live_supervisor_update_operation_inaccessible",
|
|
1979
|
+
message: `The signed-in account cannot read in-place supervisor update ${operationId} on Machine ${id}.`,
|
|
1980
|
+
exitCode: EXIT_CODES.conflict,
|
|
1981
|
+
hint: `Nothing on this Machine was changed by this attempt, and this is NOT evidence that the operation never existed: Cuna answers an unowned Machine and an operation outside this account's view with the same not-found. ${dispatch.recordedAccount === undefined
|
|
1982
|
+
? "This computer's record does not name the account that filed it."
|
|
1983
|
+
: `This computer's record was filed under account ${dispatch.recordedAccount}.`} Check the signed-in account with \`cuna whoami\`, sign in as the owner, then read it with \`cuna machines live-update-status ${id}\`. The record is kept, so the identity survives.`,
|
|
1984
|
+
retryable: false,
|
|
1985
|
+
details: {
|
|
1986
|
+
machine_id: id,
|
|
1987
|
+
operation_id: operationId,
|
|
1988
|
+
outcome: "inaccessible",
|
|
1989
|
+
local_record_cleared: false,
|
|
1990
|
+
...(dispatch.recordedAccount === undefined ? {} : { record_account: dispatch.recordedAccount }),
|
|
1991
|
+
},
|
|
1992
|
+
cause: error,
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
// The read did not answer either. The record stays exactly where it is.
|
|
1996
|
+
throw outstandingLiveUpdate(id, operationId, dispatch.dispatchedAt);
|
|
1997
|
+
}
|
|
1998
|
+
const summary = summarizeLiveSupervisorUpdate(result);
|
|
1999
|
+
// A 200 the producer only emits with every session decided. If a deployment
|
|
2000
|
+
// ever answers 200 with an `unknown` in it, that is still an outcome this
|
|
2001
|
+
// computer does not know, so the note survives and the result says so.
|
|
2002
|
+
if (!summary.anyUnknown)
|
|
2003
|
+
await notes.settle(id, operationId);
|
|
2004
|
+
const sessions = result.sessions.map((session) => Object.freeze({
|
|
2005
|
+
agent_session_id: session.agentSessionId,
|
|
2006
|
+
process_epoch: session.processEpoch,
|
|
2007
|
+
outcome: session.outcome,
|
|
2008
|
+
}));
|
|
2009
|
+
const lines = result.sessions.map((session) => ` ${session.agentSessionId} ${liveSupervisorSessionOutcomeLabel(session.outcome)}`);
|
|
2010
|
+
const subject = repeat
|
|
2011
|
+
? `Cuna resolved the in-place supervisor update it had already sent for ${result.machine.name}`
|
|
2012
|
+
: `Cuna installed a new terminal supervisor on ${result.machine.name} without stopping it`;
|
|
2013
|
+
const headline = summary.total === 0
|
|
2014
|
+
? `${subject}. No AgentSession existed on it, so nothing was preserved or lost.`
|
|
2015
|
+
: summary.allPreserved
|
|
2016
|
+
? `${subject}, and all ${summary.total} AgentSessions were preserved.`
|
|
2017
|
+
: `${subject}. ${summary.preserved} of ${summary.total} AgentSessions were preserved; this update is not complete.`;
|
|
2018
|
+
return Object.freeze({
|
|
2019
|
+
command: "machines.live-update-supervisor",
|
|
2020
|
+
data: Object.freeze({
|
|
2021
|
+
machine: machineRecord(result.machine),
|
|
2022
|
+
operation_id: result.operationId,
|
|
2023
|
+
// What this invocation did with the identity, which a caller reading only
|
|
2024
|
+
// `--json` cannot otherwise tell apart -- and which is the difference
|
|
2025
|
+
// between spending a control generation and not spending one.
|
|
2026
|
+
dispatch: repeat ? "repeat_same_operation" : "new_operation",
|
|
2027
|
+
control_generation: result.controlGeneration,
|
|
2028
|
+
artifact_sha256: result.artifactSha256,
|
|
2029
|
+
installed_at: result.installedAt,
|
|
2030
|
+
agent_sessions: Object.freeze(sessions),
|
|
2031
|
+
summary: Object.freeze({
|
|
2032
|
+
total: summary.total,
|
|
2033
|
+
preserved: summary.preserved,
|
|
2034
|
+
exited: summary.exited,
|
|
2035
|
+
ended: summary.ended,
|
|
2036
|
+
unknown: summary.unknown,
|
|
2037
|
+
all_preserved: summary.allPreserved,
|
|
2038
|
+
}),
|
|
2039
|
+
// An installed supervisor is software, not an authorization. Stated in the
|
|
2040
|
+
// record because a caller reading only `--json` gets no prose.
|
|
2041
|
+
grants_observation_or_control: false,
|
|
2042
|
+
establishes_provider_login: false,
|
|
2043
|
+
}),
|
|
2044
|
+
human: [
|
|
2045
|
+
headline,
|
|
2046
|
+
...(repeat
|
|
2047
|
+
? [`This re-sent operation ${result.operationId}, the one this computer had already recorded. It did not start a second update and did not rotate this Machine's control again.`]
|
|
2048
|
+
: [`Operation ${result.operationId}.`]),
|
|
2049
|
+
`Control generation ${result.controlGeneration}, artifact ${result.artifactSha256}, installed ${result.installedAt}.`,
|
|
2050
|
+
...(lines.length === 0 ? [] : ["AgentSession custody:", ...lines]),
|
|
2051
|
+
"This installed software only. It grants no observation or control of any AgentSession and signs no provider in.",
|
|
2052
|
+
...(summary.anyUnknown
|
|
2053
|
+
? [`Cuna could not re-read ${summary.unknown} of ${summary.total} AgentSessions, so this computer's record of this update stays open. Read it with \`cuna machines live-update-status ${id}\`.`]
|
|
2054
|
+
: []),
|
|
2055
|
+
].join("\n"),
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* `--forget-unknown`, and why it now asks before it forgets.
|
|
2060
|
+
*
|
|
2061
|
+
* It used to be a person saying "I have looked and I am done with this record",
|
|
2062
|
+
* and that was honest while the record held nothing but a timestamp: there was
|
|
2063
|
+
* no identity to lose and no read that could resolve it.
|
|
2064
|
+
*
|
|
2065
|
+
* The record now holds the operation identity, and that changes what discarding
|
|
2066
|
+
* it costs. An unsettled operation still holds the Machine on the producer's
|
|
2067
|
+
* side -- every DIFFERENT identity is refused until it settles, and the only
|
|
2068
|
+
* thing that settles it is a repeat of the identity in this file. Clearing it
|
|
2069
|
+
* while it is open does not free the Machine; it strands it, with no way to
|
|
2070
|
+
* resume and no way to start afresh. So this path reads the operation first and
|
|
2071
|
+
* clears only what is terminal.
|
|
2072
|
+
*
|
|
2073
|
+
* That read is also what prevents clearing an ACTIVELY EXECUTING reservation,
|
|
2074
|
+
* without inventing an age threshold or a liveness file that could go stale: an
|
|
2075
|
+
* attempt in flight is by construction not settled, and a settled operation has
|
|
2076
|
+
* no attempt whose effect is still open.
|
|
2077
|
+
*
|
|
2078
|
+
* Two things it still clears without asking anyone: bytes that carry no
|
|
2079
|
+
* identity to ask about, and nothing at all.
|
|
2080
|
+
*/
|
|
2081
|
+
async function clearLiveUpdateRecord(context, notes, id) {
|
|
2082
|
+
const { client } = context;
|
|
2083
|
+
const reading = await notes.read(id);
|
|
2084
|
+
const unchanged = "Nothing was sent to the Machine by this command, and clearing a record is not a cancellation.";
|
|
2085
|
+
if (reading.state === "none") {
|
|
2086
|
+
return Object.freeze({
|
|
2087
|
+
command: "machines.live-update-supervisor",
|
|
2088
|
+
data: Object.freeze({
|
|
2089
|
+
machine_id: id,
|
|
2090
|
+
local_record_cleared: false,
|
|
2091
|
+
record_readable: true,
|
|
2092
|
+
server_state_changed: false,
|
|
2093
|
+
scope: "api_origin_and_machine",
|
|
2094
|
+
}),
|
|
2095
|
+
human: `No local record of an in-place supervisor update exists for Machine ${id} on ${context.config.baseUrl}. Nothing was sent and nothing changed.`,
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
if (reading.state === "unreadable") {
|
|
2099
|
+
const removed = await notes.discard(id);
|
|
2100
|
+
return Object.freeze({
|
|
2101
|
+
command: "machines.live-update-supervisor",
|
|
2102
|
+
data: Object.freeze({
|
|
2103
|
+
machine_id: id,
|
|
2104
|
+
local_record_cleared: removed,
|
|
2105
|
+
record_readable: false,
|
|
2106
|
+
unreadable_reason: reading.reason,
|
|
2107
|
+
server_state_changed: false,
|
|
2108
|
+
scope: "api_origin_and_machine",
|
|
2109
|
+
}),
|
|
2110
|
+
human: `Cleared an unreadable local record for Machine ${id} (${reading.reason}). It carried no operation identity, so there is nothing Cuna could have read about it and nothing an update could still have been resumed with. ${unchanged}`,
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
const { operationId, dispatchedAt } = reading.note;
|
|
2114
|
+
const answer = await readLiveUpdateOperationAnswer(client, id, operationId);
|
|
2115
|
+
if (answer.state === "absent") {
|
|
2116
|
+
/* The producer has no operation with this identity. That is two different
|
|
2117
|
+
facts depending on WHO asked, and the record's own account label is what
|
|
2118
|
+
tells them apart -- evidence, never authority, exactly as it has always
|
|
2119
|
+
been used here.
|
|
2120
|
+
Same account: this owner can see their own operations, so there is
|
|
2121
|
+
genuinely none under this identity. Nothing was admitted, nothing can be
|
|
2122
|
+
resumed, and refusing to clear it would strand the Machine for no reason.
|
|
2123
|
+
Different or unrecorded account: the producer answers an operation
|
|
2124
|
+
outside the caller's view exactly as it answers one that never existed,
|
|
2125
|
+
so this says nothing about whether the owner's operation exists. The
|
|
2126
|
+
record stays. */
|
|
2127
|
+
const signedIn = await liveUpdateAccountLabel(client);
|
|
2128
|
+
const sameAccount = reading.note.account !== undefined && signedIn !== undefined && reading.note.account === signedIn;
|
|
2129
|
+
if (!sameAccount) {
|
|
2130
|
+
throw new CunaError({
|
|
2131
|
+
code: "cuna.machine.live_supervisor_update_record_unresolved",
|
|
2132
|
+
message: `The signed-in account cannot see in-place supervisor update ${operationId} on Machine ${id}, so Cuna kept this computer's record of it.`,
|
|
2133
|
+
exitCode: EXIT_CODES.conflict,
|
|
2134
|
+
hint: `The record was NOT cleared, and that is the safe direction: a not-found answer here does not mean the operation is gone, because Cuna answers an unowned Machine and an operation outside this account's view the same way. ${reading.note.account === undefined
|
|
2135
|
+
? "This record does not name the account that filed it, so Cuna cannot tell the two apart."
|
|
2136
|
+
: `This record was filed under account ${reading.note.account}.`} Check the signed-in account with \`cuna whoami\`, sign in as the owner, then read it with \`cuna machines live-update-status ${id}\`.`,
|
|
2137
|
+
retryable: false,
|
|
2138
|
+
details: {
|
|
2139
|
+
machine_id: id,
|
|
2140
|
+
operation_id: operationId,
|
|
2141
|
+
dispatched_at: dispatchedAt,
|
|
2142
|
+
local_record_cleared: false,
|
|
2143
|
+
...(reading.note.account === undefined ? {} : { record_account: reading.note.account }),
|
|
2144
|
+
...(signedIn === undefined ? {} : { signed_in_account: signedIn }),
|
|
2145
|
+
},
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
const removedAbsent = await notes.settle(id, operationId);
|
|
2149
|
+
return Object.freeze({
|
|
2150
|
+
command: "machines.live-update-supervisor",
|
|
2151
|
+
data: Object.freeze({
|
|
2152
|
+
machine_id: id,
|
|
2153
|
+
operation_id: operationId,
|
|
2154
|
+
local_record_cleared: removedAbsent === "settled",
|
|
2155
|
+
record_readable: true,
|
|
2156
|
+
dispatched_at: dispatchedAt,
|
|
2157
|
+
account: reading.note.account,
|
|
2158
|
+
operation_admitted: false,
|
|
2159
|
+
server_state_changed: false,
|
|
2160
|
+
scope: "api_origin_and_machine",
|
|
2161
|
+
}),
|
|
2162
|
+
human: [
|
|
2163
|
+
`Cuna has no in-place supervisor update ${operationId} for Machine ${id} under this account, which is the account this record was filed by, so the record was cleared.`,
|
|
2164
|
+
"That identity was never admitted: nothing was installed, no supervisor control was issued, and nothing on this Machine was changed by it. Starting a new update is now possible again.",
|
|
2165
|
+
unchanged,
|
|
2166
|
+
].join("\n"),
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
if (answer.state === "unavailable") {
|
|
2170
|
+
throw new CunaError({
|
|
2171
|
+
code: "cuna.machine.live_supervisor_update_record_unresolved",
|
|
2172
|
+
message: `Cuna could not read in-place supervisor update ${operationId} on Machine ${id}, so it kept this computer's record of it.`,
|
|
2173
|
+
exitCode: EXIT_CODES.conflict,
|
|
2174
|
+
hint: `The record was NOT cleared, and that is the safe direction: while that operation is open the producer refuses every different update identity for this Machine, so clearing this would leave nothing able to resume it and nothing able to replace it. Read it again with \`cuna machines live-update-status ${id}\`.`,
|
|
2175
|
+
retryable: true,
|
|
2176
|
+
details: {
|
|
2177
|
+
machine_id: id,
|
|
2178
|
+
operation_id: operationId,
|
|
2179
|
+
dispatched_at: dispatchedAt,
|
|
2180
|
+
local_record_cleared: false,
|
|
2181
|
+
...(reading.note.account === undefined ? {} : { record_account: reading.note.account }),
|
|
2182
|
+
},
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
const operation = answer.operation;
|
|
2186
|
+
const report = liveUpdateOperationReport(operation, id);
|
|
2187
|
+
if (operation.phase !== "settled") {
|
|
2188
|
+
throw new CunaError({
|
|
2189
|
+
code: "cuna.machine.live_supervisor_update_not_settled",
|
|
2190
|
+
message: `In-place supervisor update ${operationId} on Machine ${id} has not settled, so this computer's record of it was kept.`,
|
|
2191
|
+
exitCode: EXIT_CODES.conflict,
|
|
2192
|
+
hint: `${report.lines.join(" ")} The record was NOT cleared: it holds the only identity that can resolve this operation, and the producer refuses every different one until it does.`,
|
|
2193
|
+
retryable: false,
|
|
2194
|
+
details: { local_record_cleared: false, ...report.data },
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
const removed = await notes.settle(id, operationId);
|
|
2198
|
+
return Object.freeze({
|
|
2199
|
+
command: "machines.live-update-supervisor",
|
|
2200
|
+
data: Object.freeze({
|
|
2201
|
+
local_record_cleared: removed === "settled",
|
|
2202
|
+
record_readable: true,
|
|
2203
|
+
dispatched_at: dispatchedAt,
|
|
2204
|
+
// Present only if the note carried the label. It is shown, never used.
|
|
2205
|
+
...(reading.note.account === undefined ? {} : { account: reading.note.account }),
|
|
2206
|
+
server_state_changed: false,
|
|
2207
|
+
// The exclusion is keyed by API origin and Machine, so this is what was
|
|
2208
|
+
// cleared -- not "this profile's" record.
|
|
2209
|
+
scope: "api_origin_and_machine",
|
|
2210
|
+
...report.data,
|
|
2211
|
+
}),
|
|
2212
|
+
human: [
|
|
2213
|
+
`Cuna read in-place supervisor update ${operationId} on Machine ${id} and it is settled, so this computer's record of it was cleared. That record covered every profile on this computer.`,
|
|
2214
|
+
...report.lines,
|
|
2215
|
+
unchanged,
|
|
2216
|
+
].join("\n"),
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
/**
|
|
2220
|
+
* `machines live-update-status` -- the authoritative read, and the only thing
|
|
2221
|
+
* that can say what one in-place update did.
|
|
2222
|
+
*
|
|
2223
|
+
* It sends no mutation. The producer states that this route dispatches no
|
|
2224
|
+
* installer, sends nothing to the Machine and writes nothing, which is exactly
|
|
2225
|
+
* why it is safe to reach for after an interruption and why a second POST with
|
|
2226
|
+
* a new identity is not.
|
|
2227
|
+
*/
|
|
2228
|
+
async function executeLiveUpdateStatus(context) {
|
|
2229
|
+
const { parsed, client } = context;
|
|
2230
|
+
preflightLiveUpdateStatus(parsed);
|
|
2231
|
+
const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
|
|
2232
|
+
const notes = liveUpdateNotesFor(context, context.platform);
|
|
2233
|
+
const named = stringOption(parsed, "operation");
|
|
2234
|
+
const reading = named === undefined ? await notes.read(id) : undefined;
|
|
2235
|
+
if (reading !== undefined && reading.state !== "outstanding") {
|
|
2236
|
+
throw new CunaError({
|
|
2237
|
+
code: "cuna.machine.live_supervisor_update_operation_unnamed",
|
|
2238
|
+
message: `Cuna does not know which in-place supervisor update to read for Machine ${id}.`,
|
|
2239
|
+
exitCode: EXIT_CODES.usage,
|
|
2240
|
+
hint: reading.state === "unreadable"
|
|
2241
|
+
? `This computer holds a record for this Machine that Cuna cannot read (${reading.reason}), so it carries no operation identity. Name one with --operation UUID, or clear the record with \`cuna machines live-update-supervisor ${id} --forget-unknown\`.`
|
|
2242
|
+
: `This computer holds no record of an update for this Machine on ${context.config.baseUrl}. Name the operation with --operation UUID: an update started from another computer or from the web console has an identity this CLI never held.`,
|
|
2243
|
+
retryable: false,
|
|
2244
|
+
details: {
|
|
2245
|
+
machine_id: id,
|
|
2246
|
+
...(reading.state === "unreadable" ? { record_path: reading.path, reason: reading.reason } : {}),
|
|
2247
|
+
},
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
const operationId = named ?? reading.note.operationId;
|
|
2251
|
+
const operation = await client.readMachineSupervisorInPlaceUpdate(id, operationId);
|
|
2252
|
+
const report = liveUpdateOperationReport(operation, id);
|
|
2253
|
+
return Object.freeze({
|
|
2254
|
+
command: "machines.live-update-status",
|
|
2255
|
+
data: Object.freeze({
|
|
2256
|
+
...report.data,
|
|
2257
|
+
operation_id_source: named === undefined ? "local_record" : "named",
|
|
2258
|
+
// Said in the record as well as in the prose: this command is a read, and
|
|
2259
|
+
// a caller consuming only `--json` must not have to infer that.
|
|
2260
|
+
dispatched_installer: false,
|
|
2261
|
+
server_state_changed: false,
|
|
2262
|
+
}),
|
|
2263
|
+
human: report.lines.join("\n"),
|
|
2264
|
+
});
|
|
2265
|
+
}
|
|
2266
|
+
async function executeMachines(context) {
|
|
2267
|
+
const { parsed, client, now } = context;
|
|
2268
|
+
requireCredential(context);
|
|
2269
|
+
const action = parsed.operands[0] ?? "overview";
|
|
2270
|
+
if (action === "overview") {
|
|
2271
|
+
rejectUnknownOptions(parsed, []);
|
|
2272
|
+
const machines = await listAllMachines(client);
|
|
2273
|
+
const overview = await Promise.all(machines.map(async (machine) => {
|
|
2274
|
+
try {
|
|
2275
|
+
const sessions = (await listAllMachineAgentSessions(client, machine.id))
|
|
2276
|
+
.filter(isAgentSessionIntendedActive)
|
|
2277
|
+
.slice()
|
|
2278
|
+
.sort((left, right) => left.agent.localeCompare(right.agent) || left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
|
|
2279
|
+
return Object.freeze({ machine, sessions: Object.freeze(sessions) });
|
|
2280
|
+
}
|
|
2281
|
+
catch (error) {
|
|
2282
|
+
return Object.freeze({
|
|
2283
|
+
machine,
|
|
2284
|
+
sessions: Object.freeze([]),
|
|
2285
|
+
sessionsError: safeSessionsErrorReason(error),
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
}));
|
|
2289
|
+
overview.sort((left, right) => left.machine.name.localeCompare(right.machine.name) || left.machine.id.localeCompare(right.machine.id));
|
|
2290
|
+
return Object.freeze({
|
|
2291
|
+
command: "machines.overview",
|
|
2292
|
+
data: Object.freeze({
|
|
2293
|
+
items: Object.freeze(overview.map(({ machine, sessions, sessionsError }) => Object.freeze({
|
|
2294
|
+
...machineRecord(machine),
|
|
2295
|
+
session_counts: machineSessionCounts(machine, sessions, now),
|
|
2296
|
+
agent_sessions: Object.freeze(sessions.map((session) => agentSessionRecord(session, machine, now))),
|
|
2297
|
+
...(sessionsError === undefined ? {} : { agent_sessions_error: sessionsError }),
|
|
2298
|
+
}))),
|
|
2299
|
+
}),
|
|
2300
|
+
human: renderMachineOverview(overview, now),
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
if (action === "list") {
|
|
2304
|
+
rejectUnknownOptions(parsed, []);
|
|
2305
|
+
if (parsed.operands.length !== 1)
|
|
2306
|
+
throw usageError("machines list accepts no operands.");
|
|
2307
|
+
const page = await client.listMachines();
|
|
2308
|
+
const items = page.items.map(machineRecord);
|
|
2309
|
+
return Object.freeze({
|
|
2310
|
+
command: "machines.list",
|
|
2311
|
+
data: Object.freeze({ items, ...(page.nextCursor === undefined ? {} : { next_cursor: page.nextCursor }) }),
|
|
2312
|
+
// The provider verdict is what decides whether the ID on this row can host
|
|
2313
|
+
// a session at all. Without it a machine declaring `opencode` and a machine
|
|
2314
|
+
// declaring nothing printed byte-identically, and the person picked an ID
|
|
2315
|
+
// the very next command refuses. `machines list` takes no `--cursor`, so a
|
|
2316
|
+
// truncated page says so without promising an option that does not exist.
|
|
2317
|
+
human: [
|
|
2318
|
+
...(items.length === 0
|
|
2319
|
+
? ["No machines found."]
|
|
2320
|
+
: page.items.map((machine) => {
|
|
2321
|
+
const provider = machineProviderAvailability(machine);
|
|
2322
|
+
return `${machine.id}\t${machine.name}\t${machine.state}\t${provider.displayName} ${providerVerdict(provider)}`;
|
|
2323
|
+
})),
|
|
2324
|
+
...(page.nextCursor === undefined
|
|
2325
|
+
? []
|
|
2326
|
+
: ["-- truncated; more machines exist beyond this page"]),
|
|
2327
|
+
].join("\n"),
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
if (action === "create") {
|
|
2331
|
+
rejectUnknownOptions(parsed, ["name", "agent", "vcpus", "memory-mib", "background", "yes", "idempotency-key"]);
|
|
2332
|
+
if (parsed.operands.length !== 1)
|
|
2333
|
+
throw usageError("machines create accepts no operands.");
|
|
2334
|
+
requireConfirmation(parsed, "machines.create");
|
|
2335
|
+
const key = idempotencyKey(parsed);
|
|
2336
|
+
const rawName = stringOption(parsed, "name");
|
|
2337
|
+
if (rawName === undefined)
|
|
2338
|
+
throw usageError("Option --name is required.");
|
|
2339
|
+
const name = assertSafeDisplayText(rawName, "machine name");
|
|
2340
|
+
if (name.length < 1 || name.length > 80) {
|
|
2341
|
+
throw usageError("Option --name must contain 1 through 80 characters.");
|
|
2342
|
+
}
|
|
2343
|
+
const agent = agentOption(parsed, false);
|
|
2344
|
+
const vcpus = integerOption(parsed, "vcpus", 1, 8);
|
|
2345
|
+
const memoryMiB = integerOption(parsed, "memory-mib", 512, 16_384);
|
|
2346
|
+
await requireCapability({ client, scope: "account", capabilityId: "machines.create", now: context.capabilityClock ?? now });
|
|
2347
|
+
const input = {
|
|
2348
|
+
name,
|
|
2349
|
+
...(agent === undefined ? {} : { agent }),
|
|
2350
|
+
...(vcpus === undefined ? {} : { vcpus }),
|
|
2351
|
+
...(memoryMiB === undefined ? {} : { memoryMiB }),
|
|
2352
|
+
...(booleanOption(parsed, "background") ? { background: true } : {}),
|
|
2353
|
+
};
|
|
2354
|
+
const machine = await client.createMachine(input, key);
|
|
2355
|
+
const observed = await client.getMachine(machine.id);
|
|
2356
|
+
if (observed.id !== machine.id || observed.name !== name ||
|
|
2357
|
+
(agent !== undefined && observed.agent !== agent) ||
|
|
2358
|
+
(vcpus !== undefined && observed.vcpus !== vcpus) ||
|
|
2359
|
+
(memoryMiB !== undefined && observed.memoryMiB !== memoryMiB)) {
|
|
2360
|
+
postconditionUnverified("machine creation", {
|
|
2361
|
+
machine_id: machine.id,
|
|
2362
|
+
observed_id: observed.id,
|
|
2363
|
+
observed_name: observed.name,
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
return Object.freeze({
|
|
2367
|
+
command: "machines.create",
|
|
2368
|
+
data: machineRecord(observed),
|
|
2369
|
+
human: `Created machine ${observed.name} (${observed.id}) in state ${observed.state}.`,
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
if (action === "update-supervisor") {
|
|
2373
|
+
rejectUnknownOptions(parsed, ["yes"]);
|
|
2374
|
+
if (parsed.operands.length !== 2)
|
|
2375
|
+
throw usageError("machines update-supervisor requires exactly one machine ID.");
|
|
2376
|
+
requireConfirmation(parsed, "machines.update-supervisor");
|
|
2377
|
+
const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
|
|
2378
|
+
// A normal AgentSession create treats this capability refusal as its stop
|
|
2379
|
+
// condition. This is the one explicit remediation action: it is admitted
|
|
2380
|
+
// only when the server itself reported the OpenCode-specific prerequisite.
|
|
2381
|
+
let updateRequired = false;
|
|
2382
|
+
let stoppedRuntimeUnverified;
|
|
2383
|
+
try {
|
|
2384
|
+
await requireCapability({
|
|
2385
|
+
client,
|
|
2386
|
+
scope: "machine",
|
|
2387
|
+
resourceId: id,
|
|
2388
|
+
capabilityId: "agent_sessions.create",
|
|
2389
|
+
now: context.capabilityClock ?? now,
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
catch (error) {
|
|
2393
|
+
if (isOpenCodeSupervisorRepairCapabilityRejection(error)) {
|
|
2394
|
+
updateRequired = true;
|
|
2395
|
+
}
|
|
2396
|
+
else if (isOpenCodeRuntimeUnverifiedCapabilityRejection(error)) {
|
|
2397
|
+
// A stopped Machine cannot run the OpenCode binary probe. That is the
|
|
2398
|
+
// required precondition for this explicit repair, not evidence that
|
|
2399
|
+
// the supervisor update itself is forbidden. Re-read the exact
|
|
2400
|
+
// Machine below; the endpoint remains the authority for whether a
|
|
2401
|
+
// compatible supervisor already exists.
|
|
2402
|
+
stoppedRuntimeUnverified = error;
|
|
2403
|
+
}
|
|
2404
|
+
else {
|
|
2405
|
+
throw error;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
// The replacement route has the same `machines:update` authority as a
|
|
2409
|
+
// start. Do not substitute the deliberately unsupported create capability
|
|
2410
|
+
// for the authorization that permits the remediation.
|
|
2411
|
+
await requireCapability({
|
|
2412
|
+
client,
|
|
2413
|
+
scope: "machine",
|
|
2414
|
+
resourceId: id,
|
|
2415
|
+
capabilityId: "machines.lifecycle",
|
|
2416
|
+
now: context.capabilityClock ?? now,
|
|
2417
|
+
});
|
|
2418
|
+
const current = await client.getMachine(id);
|
|
2419
|
+
if (current.state !== "stopped") {
|
|
2420
|
+
throw new CunaError({
|
|
2421
|
+
code: "cuna.machine.supervisor_update_requires_stopped",
|
|
2422
|
+
message: "The Machine must be stopped before its terminal supervisor can be updated.",
|
|
2423
|
+
exitCode: EXIT_CODES.conflict,
|
|
2424
|
+
hint: `Cuna will not stop ${current.name} or terminate any AgentSessions. End only the sessions you intend to end, then run \`cuna machines stop ${id} --yes\`.`,
|
|
2425
|
+
details: { machine_id: id, observed_state: current.state },
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
if (stoppedRuntimeUnverified !== undefined) {
|
|
2429
|
+
if (current.agent !== "opencode")
|
|
2430
|
+
throw stoppedRuntimeUnverified;
|
|
2431
|
+
updateRequired = true;
|
|
2432
|
+
}
|
|
2433
|
+
if (!updateRequired) {
|
|
2434
|
+
throw new CunaError({
|
|
2435
|
+
code: "cuna.machine.supervisor_update_not_required",
|
|
2436
|
+
message: "Cuna does not report that this Machine needs an OpenCode terminal-supervisor update.",
|
|
2437
|
+
exitCode: EXIT_CODES.unsupported,
|
|
2438
|
+
hint: "Create an OpenCode AgentSession normally. This action is available only after Cuna reports the exact supervisor prerequisite.",
|
|
2439
|
+
details: { machine_id: id },
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
const updated = await client.replaceMachineSupervisor(id);
|
|
2443
|
+
if (updated.id !== id) {
|
|
2444
|
+
postconditionUnverified("terminal supervisor update", { machine_id: id, observed_id: updated.id });
|
|
2445
|
+
}
|
|
2446
|
+
return Object.freeze({
|
|
2447
|
+
command: "machines.update-supervisor",
|
|
2448
|
+
data: machineRecord(updated),
|
|
2449
|
+
human: `Cuna confirmed a new terminal supervisor for ${updated.name}; the Machine is ${updated.state}.`,
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
if (action === "live-update-supervisor") {
|
|
2453
|
+
return executeLiveUpdateSupervisor(context);
|
|
2454
|
+
}
|
|
2455
|
+
if (action === "live-update-status") {
|
|
2456
|
+
return executeLiveUpdateStatus(context);
|
|
2457
|
+
}
|
|
2458
|
+
if (action === "start" || action === "pause" || action === "resume" || action === "stop") {
|
|
2459
|
+
rejectUnknownOptions(parsed, ["yes"]);
|
|
2460
|
+
if (parsed.operands.length !== 2)
|
|
2461
|
+
throw usageError(`machines ${action} requires exactly one machine ID.`);
|
|
2462
|
+
requireConfirmation(parsed, `machines.${action}`);
|
|
2463
|
+
const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
|
|
2464
|
+
// The public capability registry deliberately groups the four reversible
|
|
2465
|
+
// lifecycle transitions under one semantic authority. The operation path
|
|
2466
|
+
// still binds the exact action; discovery must not invent per-action IDs
|
|
2467
|
+
// that the producer never advertises.
|
|
2468
|
+
await requireCapability({ client, scope: "machine", resourceId: id, capabilityId: "machines.lifecycle", now: context.capabilityClock ?? now });
|
|
2469
|
+
const expectedState = action === "pause" ? "paused" : action === "stop" ? "stopped" : "running";
|
|
2470
|
+
// Read the state first so the result can say whether anything moved. The
|
|
2471
|
+
// transition is still requested either way: this side's idea of the state
|
|
2472
|
+
// and the provider's can diverge, so a start against a Machine we already
|
|
2473
|
+
// call running may be a real repair.
|
|
2474
|
+
const before = await client.getMachine(id);
|
|
2475
|
+
const alreadyInState = before.id === id && before.state === expectedState;
|
|
2476
|
+
const machine = await client.transitionMachine(id, action);
|
|
2477
|
+
if (machine.id !== id) {
|
|
2478
|
+
// An identity contradiction: the producer answered about a different
|
|
2479
|
+
// machine. No amount of waiting converges that.
|
|
2480
|
+
postconditionUnverified(`machine ${action}`, { machine_id: id, observed_id: machine.id });
|
|
2481
|
+
}
|
|
2482
|
+
// A lifecycle transition is asynchronous on the producer, so the state the
|
|
2483
|
+
// very next read returns is usually the state BEFORE the transition. Read
|
|
2484
|
+
// back until it converges or until the CLI's own budget elapses.
|
|
2485
|
+
const observed = await convergeOnRemoteState(context, {
|
|
2486
|
+
operation: `machine ${action}`,
|
|
2487
|
+
settleWith: "cuna machines list",
|
|
2488
|
+
probe: async () => {
|
|
2489
|
+
const machineNow = await client.getMachine(id);
|
|
2490
|
+
return Object.freeze({
|
|
2491
|
+
settled: machineNow.id === id && machineNow.state === expectedState,
|
|
2492
|
+
observation: machineNow,
|
|
2493
|
+
details: Object.freeze({
|
|
2494
|
+
machine_id: id,
|
|
2495
|
+
expected_state: expectedState,
|
|
2496
|
+
observed_state: machineNow.state,
|
|
2497
|
+
}),
|
|
2498
|
+
});
|
|
2499
|
+
},
|
|
2500
|
+
});
|
|
2501
|
+
return Object.freeze({
|
|
2502
|
+
command: `machines.${action}`,
|
|
2503
|
+
data: Object.freeze({ ...machineRecord(observed), state_changed: !alreadyInState }),
|
|
2504
|
+
human: alreadyInState
|
|
2505
|
+
? `Machine ${observed.name} was already ${observed.state}; nothing changed.`
|
|
2506
|
+
: `Machine ${observed.name} is ${observed.state}.`,
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
if (action === "delete") {
|
|
2510
|
+
rejectUnknownOptions(parsed, ["yes"]);
|
|
2511
|
+
if (parsed.operands.length !== 2)
|
|
2512
|
+
throw usageError("machines delete requires exactly one machine ID.");
|
|
2513
|
+
requireConfirmation(parsed, "machines.delete");
|
|
2514
|
+
const id = assertMachineId(requireOperand(parsed.operands, 1, "machine ID"));
|
|
2515
|
+
await requireCapability({ client, scope: "machine", resourceId: id, capabilityId: "machines.delete", now: context.capabilityClock ?? now });
|
|
2516
|
+
await client.deleteMachine(id);
|
|
2517
|
+
// MEASURED 2026-08-19: the immediate read that used to stand here saw
|
|
2518
|
+
// `present` and the command reported `cuna.remote.postcondition_unverified`,
|
|
2519
|
+
// `retryable: false`, for a machine that `cuna machines list` showed gone six
|
|
2520
|
+
// seconds later. The producer accepts a durable delete before the resource
|
|
2521
|
+
// disappears from reads; the CLI has to let it.
|
|
2522
|
+
await convergeOnRemoteState(context, {
|
|
2523
|
+
operation: "machine deletion",
|
|
2524
|
+
settleWith: "cuna machines list",
|
|
2525
|
+
probe: async () => {
|
|
2526
|
+
try {
|
|
2527
|
+
const observed = await client.getMachine(id);
|
|
2528
|
+
return Object.freeze({
|
|
2529
|
+
settled: observed.state === "deleted",
|
|
2530
|
+
observation: undefined,
|
|
2531
|
+
details: Object.freeze({ machine_id: id, observed_state: observed.state }),
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
catch (error) {
|
|
2535
|
+
if (error instanceof CunaError && error.code === "cuna.remote.not_found") {
|
|
2536
|
+
return Object.freeze({
|
|
2537
|
+
settled: true,
|
|
2538
|
+
observation: undefined,
|
|
2539
|
+
details: Object.freeze({ machine_id: id, observed_state: "absent" }),
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
throw error;
|
|
2543
|
+
}
|
|
2544
|
+
},
|
|
2545
|
+
});
|
|
2546
|
+
return Object.freeze({ command: "machines.delete", data: { id, acknowledged: true }, human: `Delete acknowledged for ${id}.` });
|
|
2547
|
+
}
|
|
2548
|
+
throw usageError(`Unknown machines action ${action}.`);
|
|
2549
|
+
}
|
|
2550
|
+
function preflightExecutions(parsed) {
|
|
2551
|
+
const action = parsed.operands[0];
|
|
2552
|
+
rejectUnknownOptions(parsed, action === "list" ? ["machine", "execution-workspace-id", "after"] :
|
|
2553
|
+
action === "cancel" ? ["machine", "yes"] : ["machine"]);
|
|
2554
|
+
assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a Machine ID."));
|
|
2555
|
+
if (action === "list") {
|
|
2556
|
+
if (parsed.operands.length !== 1)
|
|
2557
|
+
throw usageError("executions list accepts no operands.");
|
|
2558
|
+
for (const name of ["execution-workspace-id", "after"]) {
|
|
2559
|
+
const value = stringOption(parsed, name);
|
|
2560
|
+
if (value !== undefined)
|
|
2561
|
+
assertCanonicalUuid(value, name);
|
|
2562
|
+
}
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
if ((action !== "get" && action !== "cancel") || parsed.operands.length !== 2) {
|
|
2566
|
+
throw usageError("executions requires list, get EXECUTION_ID, or cancel EXECUTION_ID.");
|
|
2567
|
+
}
|
|
2568
|
+
assertCanonicalUuid(requireOperand(parsed.operands, 1, "execution ID"), "execution ID");
|
|
2569
|
+
if (action === "cancel")
|
|
2570
|
+
requireConfirmation(parsed, "executions.cancel");
|
|
2571
|
+
}
|
|
2572
|
+
function executionRecord(item) {
|
|
2573
|
+
return Object.freeze({ operation_id: item.operationId, machine_id: item.machineId,
|
|
2574
|
+
execution_workspace_id: item.executionWorkspaceId, leader_state: item.leaderState, ownership_state: item.ownershipState,
|
|
2575
|
+
cancel_requested: item.cancelRequested, exit_code: item.exitCode, duration_ms: item.durationMs,
|
|
2576
|
+
reason: item.reason, created_at: item.createdAt, observed_at: item.observedAt });
|
|
2577
|
+
}
|
|
2578
|
+
function executionLine(item) {
|
|
2579
|
+
return `${item.operationId}\tleader=${item.leaderState}\townership=${item.ownershipState}` +
|
|
2580
|
+
`\tworkspace=${item.executionWorkspaceId ?? "legacy"}` +
|
|
2581
|
+
(item.exitCode === null ? "" : `\texit=${item.exitCode}`) +
|
|
2582
|
+
(item.cancelRequested ? "\tcancellation requested" : "") +
|
|
2583
|
+
(item.reason === null ? "" : `\t${item.reason}`);
|
|
2584
|
+
}
|
|
2585
|
+
async function executeExecutions(context) {
|
|
2586
|
+
preflightExecutions(context.parsed);
|
|
2587
|
+
requireCredential(context);
|
|
2588
|
+
const { parsed, client } = context;
|
|
2589
|
+
const machineId = requireOption(parsed, "machine");
|
|
2590
|
+
const action = parsed.operands[0];
|
|
2591
|
+
// Recovery reads and cancellation remain usable without a live supervisor.
|
|
2592
|
+
// The server authorizes exact ownership; machines.exec availability describes
|
|
2593
|
+
// new admission and must not hide an outstanding operation during an outage.
|
|
2594
|
+
if (action === "list") {
|
|
2595
|
+
const executionWorkspaceId = stringOption(parsed, "execution-workspace-id");
|
|
2596
|
+
const after = stringOption(parsed, "after");
|
|
2597
|
+
const page = await client.listManagedExecutions(machineId, {
|
|
2598
|
+
...(executionWorkspaceId === undefined ? {} : { executionWorkspaceId }), ...(after === undefined ? {} : { after }),
|
|
2599
|
+
});
|
|
2600
|
+
return Object.freeze({ command: "executions.list", data: { machine_id: page.machineId,
|
|
2601
|
+
items: page.items.map(executionRecord), next_cursor: page.nextCursor },
|
|
2602
|
+
human: `Remote executions on Machine ${machineId}\n` +
|
|
2603
|
+
(page.items.length === 0 ? "No executions in this page." : page.items.map(executionLine).join("\n")) +
|
|
2604
|
+
(page.nextCursor === null ? "" : `\nNext: cuna executions list --machine ${machineId}` +
|
|
2605
|
+
(executionWorkspaceId === undefined ? "" : ` --execution-workspace-id ${executionWorkspaceId}`) + ` --after ${page.nextCursor}`),
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
const operationId = requireOperand(parsed.operands, 1, "execution ID");
|
|
2609
|
+
const item = action === "cancel" ? await client.cancelManagedExecution(machineId, operationId) :
|
|
2610
|
+
await client.getManagedExecution(machineId, operationId);
|
|
2611
|
+
const followup = `cuna executions get ${operationId} --machine ${machineId}`;
|
|
2612
|
+
return Object.freeze({ command: `executions.${action}`, data: executionRecord(item),
|
|
2613
|
+
human: (action === "cancel" ? "Cancellation accepted.\n" : "") + executionLine(item) +
|
|
2614
|
+
(item.ownershipState === "cleared" ? "\nProcess ownership is cleared." :
|
|
2615
|
+
`\nProcess cleanup is not confirmed. Inspect: ${followup}`),
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
async function executeAgentSessions(context) {
|
|
2619
|
+
const { parsed, client, now } = context;
|
|
2620
|
+
requireCredential(context);
|
|
2621
|
+
const action = requireOperand(parsed.operands, 0, "agent-sessions action");
|
|
2622
|
+
if (action === "list") {
|
|
2623
|
+
rejectUnknownOptions(parsed, ["machine", "limit", "cursor"]);
|
|
2624
|
+
if (parsed.operands.length !== 1)
|
|
2625
|
+
throw usageError("agent-sessions list accepts no operands.");
|
|
2626
|
+
const machineId = assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."));
|
|
2627
|
+
const limit = integerOption(parsed, "limit", 1, 100);
|
|
2628
|
+
const cursor = stringOption(parsed, "cursor");
|
|
2629
|
+
const page = await client.listAgentSessions(machineId, {
|
|
2630
|
+
...(limit === undefined ? {} : { limit }),
|
|
2631
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
2632
|
+
});
|
|
2633
|
+
const items = page.items.map((session) => agentSessionRecord(session));
|
|
2634
|
+
return Object.freeze({
|
|
2635
|
+
command: "agent-sessions.list",
|
|
2636
|
+
data: { items, ...(page.nextCursor === undefined ? {} : { next_cursor: page.nextCursor }) },
|
|
2637
|
+
human: [
|
|
2638
|
+
...(items.length === 0
|
|
2639
|
+
? ["No AgentSessions found."]
|
|
2640
|
+
: page.items.map((item) => `${item.id}\t${item.name}\t${item.agent}\t${agentSessionStateLabel(item)}\t${item.cwd}`)),
|
|
2641
|
+
...truncationFooter(page.nextCursor),
|
|
2642
|
+
].join("\n"),
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2645
|
+
if (action === "get") {
|
|
2646
|
+
rejectUnknownOptions(parsed, []);
|
|
2647
|
+
if (parsed.operands.length !== 2)
|
|
2648
|
+
throw usageError("agent-sessions get requires exactly one AgentSession ID.");
|
|
2649
|
+
const id = assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
|
|
2650
|
+
const session = await client.getAgentSession(id);
|
|
2651
|
+
return Object.freeze({ command: "agent-sessions.get", data: agentSessionRecord(session), human: `${session.id}\t${session.name}\t${session.agent}\t${agentSessionStateLabel(session)}\t${session.cwd}` });
|
|
2652
|
+
}
|
|
2653
|
+
if (action === "create") {
|
|
2654
|
+
rejectUnknownOptions(parsed, [
|
|
2655
|
+
"machine", "workspace-binding-id", "workspace-generation", "name", "agent", "cwd",
|
|
2656
|
+
"auth-mode", "credential-binding", "yes", "idempotency-key",
|
|
2657
|
+
]);
|
|
2658
|
+
if (parsed.operands.length !== 1)
|
|
2659
|
+
throw usageError("agent-sessions create accepts no operands.");
|
|
2660
|
+
requireConfirmation(parsed, "agent-sessions.create");
|
|
2661
|
+
const machineId = assertMachineId(requireOption(parsed, "machine", "Run `cuna machines list` to find a machine ID."));
|
|
2662
|
+
// Nothing in the product publishes a binding id, so both options are
|
|
2663
|
+
// resolved from `.cuna/workspace.json`, which the journey commands write.
|
|
2664
|
+
// An explicit flag still wins, so a caller who knows better decides.
|
|
2665
|
+
const givenBindingId = stringOption(parsed, "workspace-binding-id");
|
|
2666
|
+
const givenGeneration = integerOption(parsed, "workspace-generation", 1, Number.MAX_SAFE_INTEGER);
|
|
2667
|
+
// Read the folder only when something is missing: a fully specified
|
|
2668
|
+
// invocation costs no extra round trip.
|
|
2669
|
+
const localBinding = givenBindingId !== undefined && givenGeneration !== undefined
|
|
2670
|
+
? undefined
|
|
2671
|
+
: await resolveLocalWorkspaceBinding(context);
|
|
2672
|
+
const workspaceBindingId = assertCanonicalUuid(givenBindingId ?? localBinding?.bindingId ?? "", "workspace binding ID");
|
|
2673
|
+
const workspaceGeneration = givenGeneration ?? localBinding?.generation;
|
|
2674
|
+
if (workspaceGeneration === undefined) {
|
|
2675
|
+
throw usageError("Option --workspace-generation is required.");
|
|
2676
|
+
}
|
|
2677
|
+
const agent = agentOption(parsed, true);
|
|
2678
|
+
if (agent === undefined)
|
|
2679
|
+
throw usageError("Option --agent is required.");
|
|
2680
|
+
const cwd = assertSafeDisplayText(stringOption(parsed, "cwd") ?? "/workspace", "workspace path");
|
|
2681
|
+
if (!cwd.startsWith("/workspace") || cwd.split("/").includes("..") || cwd.length > 1024) {
|
|
2682
|
+
throw usageError("Option --cwd must be a safe absolute path inside /workspace.");
|
|
2683
|
+
}
|
|
2684
|
+
const rawName = stringOption(parsed, "name");
|
|
2685
|
+
const name = rawName === undefined ? undefined : assertSafeDisplayText(rawName, "AgentSession name");
|
|
2686
|
+
if (name !== undefined && (name.length < 1 || name.length > 80)) {
|
|
2687
|
+
throw usageError("Option --name must contain 1 through 80 characters.");
|
|
2688
|
+
}
|
|
2689
|
+
const credentialBinding = stringOption(parsed, "credential-binding");
|
|
2690
|
+
const rawAuthMode = stringOption(parsed, "auth-mode");
|
|
2691
|
+
if (agent === "opencode" &&
|
|
2692
|
+
(rawAuthMode === "credential_binding" || credentialBinding !== undefined)) {
|
|
2693
|
+
throw usageError("OpenCode supports interactive_login only; credential bindings are not accepted.", "Use OpenCode's interactive provider flow and omit --credential-binding.");
|
|
2694
|
+
}
|
|
2695
|
+
const requestedAuthMode = normalizedAgentSessionAuthMode(rawAuthMode, credentialBinding);
|
|
2696
|
+
const authMode = agent === "opencode" ? "interactive_login" : requestedAuthMode;
|
|
2697
|
+
const machine = await client.getMachine(machineId);
|
|
2698
|
+
if (!machineSupportsProvider(machine, agent)) {
|
|
2699
|
+
const installed = machineProviderAvailability(machine);
|
|
2700
|
+
throw new CunaError({
|
|
2701
|
+
code: "cuna.agent.provider_not_installed",
|
|
2702
|
+
message: `${providerDisplayName(agent)} is unavailable on machine ${machine.name}. Declared installed provider: ${installed.displayName}.`,
|
|
2703
|
+
exitCode: EXIT_CODES.unsupported,
|
|
2704
|
+
hint: `Choose a machine whose installed provider is ${providerDisplayName(agent)}, or create one with \`cuna machines create --agent ${agent} ...\`.`,
|
|
2705
|
+
details: {
|
|
2706
|
+
machine_id: machine.id,
|
|
2707
|
+
requested_provider: agent,
|
|
2708
|
+
installed_provider: installed.declaredId ?? "unknown",
|
|
2709
|
+
},
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
try {
|
|
2713
|
+
await requireCapability({ client, scope: "machine", resourceId: machineId, capabilityId: "agent_sessions.create", now: context.capabilityClock ?? now });
|
|
2714
|
+
}
|
|
2715
|
+
catch (error) {
|
|
2716
|
+
if (agent === "opencode" && isOpenCodeSupervisorUpgradeCapabilityRejection(error)) {
|
|
2717
|
+
throw openCodeSupervisorUpgradeRequired({
|
|
2718
|
+
...(error.details === undefined ? {} : { details: error.details }),
|
|
2719
|
+
machineId,
|
|
2720
|
+
cause: error,
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
if (agent === "opencode" && isOpenCodeRuntimeUnverifiedCapabilityRejection(error)) {
|
|
2724
|
+
throw openCodeRuntimeUnverified({
|
|
2725
|
+
...(error.details === undefined ? {} : { details: error.details }),
|
|
2726
|
+
machineId,
|
|
2727
|
+
cause: error,
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
throw error;
|
|
2731
|
+
}
|
|
2732
|
+
const session = await client.createAgentSession(machineId, {
|
|
2733
|
+
...(name === undefined ? {} : { name }),
|
|
2734
|
+
agent,
|
|
2735
|
+
cwd,
|
|
2736
|
+
workspaceBindingId,
|
|
2737
|
+
workspaceGeneration,
|
|
2738
|
+
...(authMode === undefined ? {} : { authMode }),
|
|
2739
|
+
...(credentialBinding === undefined
|
|
2740
|
+
? {}
|
|
2741
|
+
: { credentialBindingId: assertCanonicalUuid(credentialBinding, "credential binding ID") }),
|
|
2742
|
+
}, idempotencyKey(parsed));
|
|
2743
|
+
const observed = await client.getAgentSession(session.id);
|
|
2744
|
+
if (observed.id !== session.id || observed.machineId !== machineId || observed.agent !== agent ||
|
|
2745
|
+
observed.cwd !== cwd || observed.workspaceBindingId !== workspaceBindingId ||
|
|
2746
|
+
observed.workspaceGeneration !== workspaceGeneration ||
|
|
2747
|
+
(name !== undefined && observed.name !== name) ||
|
|
2748
|
+
(authMode !== undefined && observed.authMode !== authMode)) {
|
|
2749
|
+
postconditionUnverified("AgentSession creation", {
|
|
2750
|
+
agent_session_id: session.id,
|
|
2751
|
+
machine_id: machineId,
|
|
2752
|
+
observed_agent: observed.agent,
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
return Object.freeze({ command: "agent-sessions.create", data: agentSessionRecord(observed), human: `Created ${observed.agent} AgentSession ${observed.id}.` });
|
|
2756
|
+
}
|
|
2757
|
+
if (action === "terminate") {
|
|
2758
|
+
rejectUnknownOptions(parsed, ["yes"]);
|
|
2759
|
+
if (parsed.operands.length !== 2)
|
|
2760
|
+
throw usageError("agent-sessions terminate requires exactly one AgentSession ID.");
|
|
2761
|
+
requireConfirmation(parsed, "agent-sessions.terminate");
|
|
2762
|
+
const id = assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
|
|
2763
|
+
await requireCapability({ client, scope: "agent_session", resourceId: id, capabilityId: "agent_sessions.terminate", now: context.capabilityClock ?? now });
|
|
2764
|
+
await client.terminateAgentSession(id);
|
|
2765
|
+
const observed = await convergeOnRemoteState(context, {
|
|
2766
|
+
operation: "AgentSession termination",
|
|
2767
|
+
settleWith: `cuna agent-sessions get ${id}`,
|
|
2768
|
+
probe: async () => {
|
|
2769
|
+
const session = await client.getAgentSession(id);
|
|
2770
|
+
return Object.freeze({
|
|
2771
|
+
settled: session.id === id && agentSessionTerminationConfirmed(session),
|
|
2772
|
+
observation: session,
|
|
2773
|
+
details: Object.freeze({
|
|
2774
|
+
agent_session_id: id,
|
|
2775
|
+
observed_desired_state: session.desiredState,
|
|
2776
|
+
observed_request_state: session.requestState,
|
|
2777
|
+
observed_process_state: session.processState,
|
|
2778
|
+
}),
|
|
2779
|
+
});
|
|
2780
|
+
},
|
|
2781
|
+
});
|
|
2782
|
+
return Object.freeze({ command: "agent-sessions.terminate", data: agentSessionRecord(observed), human: `AgentSession ${observed.id} is ${observed.requestState}/${observed.processState}.` });
|
|
2783
|
+
}
|
|
2784
|
+
if (action === "rename") {
|
|
2785
|
+
rejectUnknownOptions(parsed, ["name", "yes"]);
|
|
2786
|
+
if (parsed.operands.length !== 2)
|
|
2787
|
+
throw usageError("agent-sessions rename requires exactly one AgentSession ID.");
|
|
2788
|
+
requireConfirmation(parsed, "agent-sessions.rename");
|
|
2789
|
+
const id = assertCanonicalUuid(requireOperand(parsed.operands, 1, "AgentSession ID"), "AgentSession ID");
|
|
2790
|
+
const rawName = stringOption(parsed, "name");
|
|
2791
|
+
const name = rawName === undefined ? undefined : assertSafeDisplayText(rawName, "AgentSession name");
|
|
2792
|
+
if (name === undefined || name.length < 1 || name.length > 80) {
|
|
2793
|
+
throw usageError("Option --name must contain 1 through 80 characters.");
|
|
2794
|
+
}
|
|
2795
|
+
await requireCapability({ client, scope: "agent_session", resourceId: id, capabilityId: "agent_sessions.rename", now: context.capabilityClock ?? now });
|
|
2796
|
+
await client.renameAgentSession(id, name);
|
|
2797
|
+
const observed = await client.getAgentSession(id);
|
|
2798
|
+
if (observed.id !== id || observed.name !== name) {
|
|
2799
|
+
postconditionUnverified("AgentSession rename", { agent_session_id: id, expected_name: name, observed_name: observed.name });
|
|
2800
|
+
}
|
|
2801
|
+
return Object.freeze({
|
|
2802
|
+
command: "agent-sessions.rename",
|
|
2803
|
+
data: agentSessionRecord(observed),
|
|
2804
|
+
human: `Renamed AgentSession ${observed.id} to ${observed.name}.`,
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
if (action === "attach") {
|
|
2808
|
+
// See the root command arm above: runCli intercepts this public path after
|
|
2809
|
+
// exact identity and TTY admission. The generic dispatcher cannot attach.
|
|
2810
|
+
throw unsupportedError("AgentSession attach", "run_cli_composition_required");
|
|
2811
|
+
}
|
|
2812
|
+
throw usageError(`Unknown agent-sessions action ${action}.`);
|
|
2813
|
+
}
|
|
2814
|
+
//# sourceMappingURL=commands.js.map
|