You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/20 09:08:32 UTC
(maka) branch feat/runtime-host-rust updated: refactor(plugins): unify public capabilities and remove legacy paths
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
new 00e52fc2de refactor(plugins): unify public capabilities and remove legacy paths
00e52fc2de is described below
commit 00e52fc2de736fd16ca22660f87ddac8b4439e0e
Author: Wang <m4...@gmail.com>
AuthorDate: Sun Sep 20 17:08:12 2026 +0800
refactor(plugins): unify public capabilities and remove legacy paths
Expose scoped host services and Rust/JS interoperability through the public plugin API. Add durable background authorization, effect settlement, and client call provenance; migrate skills entry points and remove obsolete compatibility paths.
Generated-by: OpenAI Codex
---
Cargo.lock | 2 +-
.../src/main/__tests__/browser-message-box.test.ts | 14 +
.../src/main/__tests__/browser-tools.test.ts | 3 +-
.../main/__tests__/client-plugin-remote.test.ts | 69 +-
.../__tests__/main-process-diagnostics.test.ts | 2 +-
.../__tests__/managed-artifact-preview.test.ts | 2 +-
.../src/main/__tests__/mcp-runtime-e2e.test.ts | 9 +-
.../permission-response-ipc-boundary.test.ts | 10 +-
.../runtime-host-bot-session-adapter.test.ts | 11 +-
.../main/__tests__/runtime-host-client-uds.test.ts | 12 +-
.../runtime-host-desktop-candidate.test.ts | 9 +-
.../__tests__/runtime-host-desktop-manager.test.ts | 5 +-
.../runtime-host-local-remote-access.test.ts | 89 +-
.../runtime-host-managed-services.test.ts | 27 +-
.../main/__tests__/runtime-host-management.test.ts | 19 +-
.../runtime-host-native-capabilities.test.ts | 48 +-
.../main/__tests__/runtime-host-onboarding.test.ts | 220 +---
.../__tests__/runtime-host-profile-service.test.ts | 122 +-
...runtime-host-session-execution-ipc-main.test.ts | 86 +-
.../__tests__/runtime-host-ssh-terminal.test.ts | 354 ++----
.../__tests__/runtime-host-wsl-controller.test.ts | 121 --
.../__tests__/runtime-host-wsl-handoff.test.ts | 27 +-
.../src/main/__tests__/session-local.test.ts | 4 +-
.../__tests__/workbar-services-adapter.test.ts | 2 +-
apps/desktop/src/main/browser-message-box.ts | 10 +
apps/desktop/src/main/browser/browser-tools.ts | 18 +-
apps/desktop/src/main/client-plugin-remote-ipc.ts | 49 +-
apps/desktop/src/main/main-process-diagnostics.ts | 2 +-
.../src/main/managed-artifact-preview-tools.ts | 6 +-
apps/desktop/src/main/permission-response-guard.ts | 29 +-
.../src/main/plugin-authorization-dialog.ts | 51 +
apps/desktop/src/main/runtime-host-boot.ts | 21 +-
.../src/main/runtime-host-bot-session-adapter.ts | 8 +-
apps/desktop/src/main/runtime-host-client.ts | 40 +-
.../src/main/runtime-host-local-remote-access.ts | 171 +--
.../src/main/runtime-host-managed-services.ts | 137 +--
apps/desktop/src/main/runtime-host-management.ts | 52 +-
.../src/main/runtime-host-native-capabilities.ts | 70 +-
apps/desktop/src/main/runtime-host-onboarding.ts | 220 +---
.../src/main/runtime-host-pairing-journal.ts | 5 +-
.../src/main/runtime-host-profile-service.ts | 90 +-
.../runtime-host-session-execution-ipc-main.ts | 29 +-
apps/desktop/src/main/runtime-host-ssh-terminal.ts | 158 +--
.../src/main/runtime-host-wsl-controller.ts | 110 --
apps/desktop/src/main/runtime-host-wsl-handoff.ts | 4 +-
apps/desktop/src/main/session-local-service.ts | 4 +-
apps/desktop/src/preload/bridge-contract.d.ts | 23 +-
apps/desktop/src/preload/preload.ts | 11 +
.../desktop/src/renderer/app-shell-chat-actions.ts | 10 +-
.../features/client-plugins/host-runtime.ts | 2 +-
.../src/renderer/features/client-plugins/ports.ts | 3 +-
.../platform/desktop/client-plugin-remote.ts | 109 +-
.../desktop/create-client-plugin-services.ts | 31 +
.../src/renderer/skill-invocation-feedback.ts | 14 +-
crates/agent/src/executor.rs | 9 +-
crates/agent/src/history.rs | 3 +-
crates/agent/src/history/compatible.rs | 13 +-
crates/agent/src/history/replay.rs | 5 +-
crates/agent/src/lib.rs | 10 +-
crates/agent/src/model_attempt.rs | 2 +-
crates/agent/src/runner.rs | 2 -
.../tests/integration/attachment_projection.rs | 2 -
crates/agent/tests/integration/auto_context.rs | 2 +-
.../agent/tests/integration/code_mode_lifetime.rs | 1 -
crates/agent/tests/integration/context_overflow.rs | 8 +-
crates/agent/tests/integration/dynamic_tools.rs | 2 +-
.../agent/tests/integration/frozen_projection.rs | 4 +-
crates/agent/tests/integration/handoff.rs | 2 +-
crates/agent/tests/integration/image_projection.rs | 4 +-
crates/agent/tests/integration/lifetime.rs | 1 -
.../agent/tests/integration/model_interruption.rs | 5 +-
.../agent/tests/integration/plugin_composition.rs | 2 +-
crates/agent/tests/integration/pruning.rs | 5 +-
crates/agent/tests/integration/stale_pruning.rs | 6 +-
crates/agent/tests/integration/startup_recovery.rs | 1 -
.../agent/tests/integration/support/agent_loop.rs | 2 -
.../agent/tests/integration/support/code_mode.rs | 1 -
crates/agent/tests/integration/support/context.rs | 1 -
.../tests/integration/support/image_projection.rs | 1 -
crates/agent/tests/integration/support/recovery.rs | 3 +-
.../cli/migrations/deployment/0001_deployment.sql | 12 +
crates/cli/migrations/deployment/0002_update.sql | 24 -
.../migrations/deployment/0003_update_policy.sql | 24 -
crates/cli/src/deployment/policy/storage.rs | 7 +-
crates/cli/src/deployment/query.rs | 6 -
crates/cli/src/deployment/store.rs | 44 +-
crates/cli/src/deployment/updates.rs | 56 +-
crates/cli/src/host_client.rs | 1 -
crates/cli/tests/integration/candidate.rs | 1 -
crates/client-capability/src/broker.rs | 7 +-
crates/client-capability/src/broker/dispatch.rs | 8 +-
crates/client-capability/tests/client_service.rs | 13 +-
.../client-capability/tests/journal_admission.rs | 7 +-
crates/config/migrations/0001_configuration.sql | 54 +-
.../config/migrations/0002_access_credentials.sql | 26 -
crates/config/migrations/0003_runtime_policy.sql | 28 -
crates/config/migrations/0004_model_facts.sql | 24 -
crates/config/migrations/0005_oauth_login.sql | 27 -
crates/config/migrations/0006_external_agents.sql | 34 -
crates/config/migrations/0007_model_overrides.sql | 36 -
.../migrations/0008_connection_model_authority.sql | 27 -
.../config/migrations/0009_skill_preferences.sql | 32 -
.../config/migrations/0010_plugin_credentials.sql | 28 -
crates/config/src/database.rs | 26 +-
crates/config/src/lib.rs | 2 +-
crates/config/src/plugin_authorization.rs | 187 ++++
crates/config/src/policy.rs | 10 +-
crates/config/src/skills.rs | 134 ---
crates/config/tests/migrations.rs | 224 +---
crates/config/tests/plugin_authorization.rs | 191 ++++
crates/config/tests/runtime_policy.rs | 60 +-
crates/config/tests/skills.rs | 117 --
crates/event-log/migrations/0001_runtime.sql | 411 ++++++-
.../migrations/0002_session_read_state.sql | 36 -
crates/event-log/migrations/0003_interactions.sql | 36 -
crates/event-log/migrations/0004_artifacts.sql | 41 -
.../migrations/0005_tool_result_payloads.sql | 25 -
crates/event-log/migrations/0006_shell_runs.sql | 38 -
crates/event-log/migrations/0007_steering.sql | 29 -
.../event-log/migrations/0008_message_sources.sql | 25 -
.../migrations/0009_message_admissions.sql | 28 -
crates/event-log/migrations/0010_message_queue.sql | 30 -
.../migrations/0011_queue_command_receipts.sql | 30 -
.../migrations/0012_message_submit_receipts.sql | 28 -
.../migrations/0013_message_interrupts.sql | 34 -
.../migrations/0014_continuation_claims.sql | 31 -
crates/event-log/migrations/0015_projects.sql | 46 -
.../migrations/0016_interrupted_transcript.sql | 25 -
crates/event-log/migrations/0018_workhub_stops.sql | 30 -
.../event-log/migrations/0019_workhub_resume.sql | 34 -
.../event-log/migrations/0020_session_events.sql | 118 --
.../migrations/0021_workhub_assignment_target.sql | 25 -
.../migrations/0022_workhub_correction.sql | 67 --
.../event-log/migrations/0023_handoff_claims.sql | 43 -
crates/event-log/migrations/0024_plugins.sql | 58 -
crates/event-log/migrations/0025_graph.sql | 52 -
.../migrations/0026_request_composition.sql | 29 -
.../event-log/migrations/0027_session_managers.sql | 26 -
crates/event-log/src/archive/effective.rs | 32 +-
crates/event-log/src/context/boundary.rs | 11 +-
crates/event-log/src/context/latest_main.rs | 10 +-
crates/event-log/src/context/proof.rs | 17 +-
crates/event-log/src/context/request.rs | 30 +-
crates/event-log/src/effects.rs | 179 +++
crates/event-log/src/lib.rs | 1 +
crates/event-log/src/message_admissions.rs | 6 +-
.../event-log/src/message_admissions/receipts.rs | 6 +-
crates/event-log/src/message_queue.rs | 1 -
crates/event-log/src/message_queue/edit.rs | 2 -
crates/event-log/src/observation/delivery.rs | 2 +-
crates/event-log/src/observation/streams.rs | 2 +-
crates/event-log/src/plugins/execution.rs | 3 +-
crates/event-log/src/root.rs | 21 +-
crates/event-log/src/root_lock.rs | 1 -
crates/event-log/src/schema.rs | 26 +-
crates/event-log/src/sessions/activity.rs | 42 +-
crates/event-log/src/sessions/partial.rs | 2 +-
crates/event-log/src/steering_delivery.rs | 1 -
crates/event-log/src/transcript.rs | 2 +-
crates/event-log/src/transcript/evidence.rs | 2 +-
crates/event-log/src/transcript/tools.rs | 2 +-
crates/event-log/src/workhub/correction.rs | 16 +-
crates/event-log/src/workhub/correction/admit.rs | 3 +-
crates/event-log/tests/append_batch.rs | 1 -
crates/event-log/tests/archive/fixtures.rs | 5 +-
.../event-log/tests/archive/lineage_checkpoint.rs | 2 +-
crates/event-log/tests/async_commit.rs | 1 -
crates/event-log/tests/catalog_changes.rs | 1 -
crates/event-log/tests/catalog_execution.rs | 12 +-
crates/event-log/tests/catalog_partial.rs | 3 +-
crates/event-log/tests/compact_presentation.rs | 18 +-
.../tests/compact_presentation/automatic.rs | 8 +-
.../tests/compact_presentation/automatic/steps.rs | 2 +-
crates/event-log/tests/context_compaction.rs | 3 -
.../event-log/tests/context_compaction/active.rs | 4 +-
.../event-log/tests/context_compaction/fixtures.rs | 5 +-
.../event-log/tests/context_compaction/frozen.rs | 3 -
.../event-log/tests/context_compaction/legacy.rs | 68 --
.../event-log/tests/context_compaction/lineage.rs | 4 +-
crates/event-log/tests/continuation.rs | 1 -
crates/event-log/tests/continuation/fixtures.rs | 1 -
crates/event-log/tests/continuation/handoff.rs | 4 +-
.../event-log/tests/continuation/handoff_budget.rs | 2 +-
.../tests/continuation/handoff_consumers.rs | 24 +-
crates/event-log/tests/effects.rs | 86 ++
crates/event-log/tests/executor.rs | 1 -
crates/event-log/tests/graph.rs | 1 -
crates/event-log/tests/message_queue.rs | 24 +-
crates/event-log/tests/message_submit_receipts.rs | 14 +-
crates/event-log/tests/migrations.rs | 161 +--
crates/event-log/tests/navigation.rs | 1 -
crates/event-log/tests/observation.rs | 4 +-
crates/event-log/tests/plugins.rs | 2 -
crates/event-log/tests/projects.rs | 1 -
crates/event-log/tests/provider_call_namespace.rs | 3 +-
crates/event-log/tests/read_state.rs | 90 --
crates/event-log/tests/root_messages.rs | 20 +-
crates/event-log/tests/run_prefix.rs | 1 -
crates/event-log/tests/session_metadata.rs | 1 -
crates/event-log/tests/session_prefix.rs | 1 -
crates/event-log/tests/sessions.rs | 2 -
crates/event-log/tests/shell_runs.rs | 17 +-
crates/event-log/tests/steering.rs | 3 +-
crates/event-log/tests/stream_delivery.rs | 6 +-
crates/event-log/tests/support/message_queue.rs | 2 -
crates/event-log/tests/support/steering.rs | 3 +-
crates/event-log/tests/tool_ancestry.rs | 28 +-
crates/event-log/tests/tool_rejection.rs | 1 -
.../event-log/tests/transcript_index/fixtures.rs | 3 +-
crates/event-log/tests/transcript_tools.rs | 3 +-
.../event-log/tests/transcript_tools/payloads.rs | 1 -
crates/event-log/tests/turn_boundary.rs | 4 +-
crates/event-log/tests/visible_tail.rs | 29 +-
crates/event-log/tests/workhub.rs | 3 -
crates/event-log/tests/workhub_attachments.rs | 1 -
crates/event-log/tests/workhub_candidates.rs | 1 -
crates/event-log/tests/workhub_correction.rs | 53 +-
crates/event-log/tests/workhub_creation.rs | 1 -
crates/event-log/tests/workhub_resume.rs | 1 -
crates/event-log/tests/workhub_revision.rs | 1 -
crates/event-log/tests/workhub_stop.rs | 16 +-
crates/js-runtime/src/plugin/sdk.js | 237 ++--
crates/plugins/src/authorization.rs | 218 ++++
crates/plugins/src/call.rs | 199 ++++
.../invocation => plugins/src/call}/resources.rs | 4 +-
crates/plugins/src/client_capability.rs | 19 +-
crates/plugins/src/contributions.rs | 76 +-
crates/plugins/src/execution.rs | 59 +-
crates/plugins/src/execution/root.rs | 56 +
crates/plugins/src/executor.rs | 70 +-
crates/plugins/src/fiber.rs | 4 +-
crates/plugins/src/filesystem.rs | 29 +-
crates/plugins/src/host.rs | 45 +
crates/plugins/src/http.rs | 85 ++
crates/plugins/src/input.rs | 12 +-
crates/plugins/src/kernel.rs | 12 +-
crates/plugins/src/kernel/reconcile.rs | 10 +-
crates/plugins/src/lib.rs | 7 +
crates/plugins/src/llm.rs | 8 +
.../src/preferences.rs} | 23 +-
crates/plugins/src/process.rs | 134 +++
crates/plugins/src/remote.rs | 32 +-
crates/plugins/src/services.rs | 49 +
crates/plugins/src/services/method.rs | 306 ++++++
crates/plugins/src/session.rs | 27 +-
crates/plugins/src/storage.rs | 1 +
crates/plugins/src/storage/files.rs | 339 ++++++
crates/plugins/src/terminal.rs | 83 ++
crates/plugins/tests/executor.rs | 10 +-
crates/plugins/tests/kernel.rs | 152 ++-
crates/plugins/tests/services.rs | 121 ++
crates/presentation/src/compact.rs | 7 +-
crates/presentation/tests/projection.rs | 3 +-
crates/presentation/tests/reference_projection.rs | 1 -
crates/presentation/tests/tools/fixtures.rs | 3 +-
crates/protocol/Cargo.toml | 1 -
crates/protocol/src/authority.rs | 7 +-
crates/protocol/src/capability/host_frame.rs | 30 +-
crates/protocol/src/codec.rs | 23 +-
crates/protocol/src/handshake.rs | 45 +-
crates/protocol/src/host/status.rs | 8 +-
crates/protocol/src/lib.rs | 3 +-
crates/protocol/src/message/input.rs | 16 +-
crates/protocol/src/message/output.rs | 39 +-
crates/protocol/src/operation.rs | 7 +-
crates/protocol/src/plugin.rs | 5 +
crates/protocol/src/plugin/authorization.rs | 84 ++
crates/protocol/src/plugin/input.rs | 6 +
crates/protocol/src/plugin/output.rs | 4 +
crates/protocol/src/plugin/remote.rs | 15 +-
crates/protocol/src/session.rs | 17 +-
crates/protocol/src/session_mutation.rs | 10 +-
crates/protocol/src/session_types.rs | 27 -
crates/protocol/src/skills.rs | 140 ---
crates/protocol/src/skills/catalog.rs | 144 ---
crates/protocol/src/skills/governance.rs | 56 -
crates/protocol/src/skills/import.rs | 42 -
crates/protocol/src/skills/mutation.rs | 75 --
crates/protocol/src/skills/path.rs | 38 -
crates/protocol/src/skills/preview.rs | 82 --
crates/protocol/src/turn.rs | 75 +-
crates/protocol/src/turn_skills.rs | 20 -
crates/protocol/src/workhub.rs | 4 +-
crates/protocol/tests/capability_frames.rs | 30 +-
crates/protocol/tests/message_contract.rs | 12 +-
crates/protocol/tests/session_contract.rs | 28 +-
crates/protocol/tests/session_mutation.rs | 23 +-
crates/protocol/tests/turn_contract.rs | 25 +-
crates/protocol/tests/wire_contract.rs | 28 +-
crates/runtime-host/Cargo.toml | 1 +
crates/runtime-host/src/execution.rs | 9 +-
crates/runtime-host/src/execution/admission.rs | 34 +-
crates/runtime-host/src/execution/archive.rs | 34 +-
crates/runtime-host/src/execution/client_view.rs | 132 ---
crates/runtime-host/src/execution/input.rs | 78 +-
.../runtime-host/src/execution/input/prepared.rs | 19 +-
crates/runtime-host/src/execution/message.rs | 46 +-
.../runtime-host/src/execution/message/queued.rs | 15 +-
crates/runtime-host/src/execution/plugins.rs | 63 +-
.../src/execution/plugins/authority.rs | 163 ++-
.../runtime-host/src/execution/plugins/children.rs | 4 +-
.../runtime-host/src/execution/plugins/effects.rs | 339 ++++++
.../src/execution/plugins/effects/client.rs | 214 ++++
crates/runtime-host/src/execution/plugins/llm.rs | 79 +-
crates/runtime-host/src/execution/plugins/root.rs | 194 +++-
.../runtime-host/src/execution/plugins/scopes.rs | 295 +++++
crates/runtime-host/src/execution/prepare.rs | 26 +-
.../src/execution/prepare/environment.rs | 87 +-
crates/runtime-host/src/execution/read.rs | 5 +-
.../src/execution/scheduler/delivery.rs | 1 +
crates/runtime-host/src/execution/skills.rs | 85 --
crates/runtime-host/src/execution/snapshot.rs | 10 +-
crates/runtime-host/src/execution/successor.rs | 9 +-
.../runtime-host/src/execution/workhub/answer.rs | 1 -
.../src/execution/workhub/correction.rs | 2 +-
.../src/execution/workhub/correction/settlement.rs | 5 +-
crates/runtime-host/src/plugins.rs | 5 +
crates/runtime-host/src/plugins/assistant.rs | 31 +-
.../runtime-host/src/plugins/assistant/prompt.rs | 23 +-
crates/runtime-host/src/plugins/effects.rs | 194 ++++
.../runtime-host/src/plugins/graph/definitions.rs | 6 +-
crates/runtime-host/src/plugins/host.rs | 227 ++++
crates/runtime-host/src/plugins/http.rs | 245 +++++
.../src/plugins/{javascript => }/http/worker.rs | 2 -
crates/runtime-host/src/plugins/javascript.rs | 40 +-
.../runtime-host/src/plugins/javascript/bridge.rs | 431 +++++---
.../src/plugins/javascript/bridge/service.rs | 141 ++-
.../src/plugins/javascript/bridge/wire.rs | 205 +++-
.../src/plugins/javascript/callbacks.rs | 40 +-
.../runtime-host/src/plugins/javascript/effects.rs | 142 ---
.../src/plugins/javascript/executor.rs | 14 +-
crates/runtime-host/src/plugins/javascript/http.rs | 275 -----
.../runtime-host/src/plugins/javascript/input.rs | 28 +-
.../src/plugins/javascript/invocation.rs | 106 +-
.../runtime-host/src/plugins/javascript/process.rs | 327 ------
.../src/plugins/javascript/registration.rs | 66 +-
.../runtime-host/src/plugins/javascript/remote.rs | 54 +-
crates/runtime-host/src/plugins/process.rs | 366 ++++++
.../src/plugins/{javascript => }/process/worker.rs | 3 +-
crates/runtime-host/src/plugins/remote.rs | 32 +-
crates/runtime-host/src/plugins/skills.rs | 63 +-
.../src/plugins/{javascript => }/terminal.rs | 243 ++--
.../plugins/{javascript => }/terminal/resource.rs | 55 +-
crates/runtime-host/src/plugins/wire.rs | 3 +-
crates/runtime-host/src/plugins/workhub.rs | 4 -
.../src/plugins/workhub/coordinator.rs | 6 +-
crates/runtime-host/src/plugins/workhub/remote.rs | 2 +-
.../src/plugins/workhub/tools/input.rs | 2 +-
crates/runtime-host/src/server.rs | 13 +-
crates/runtime-host/src/server/authority.rs | 48 +-
crates/runtime-host/src/server/diagnostics.rs | 2 +-
crates/runtime-host/src/server/dispatch.rs | 28 +-
crates/runtime-host/src/server/handshake.rs | 16 +-
.../runtime-host/src/server/messages/capacity.rs | 5 -
.../runtime-host/src/server/messages/mutations.rs | 15 +-
crates/runtime-host/src/server/operations.rs | 61 -
.../src/server/plugin_authorization.rs | 441 ++++++++
crates/runtime-host/src/server/plugin_remote.rs | 29 +-
.../runtime-host/src/server/plugin_remote/views.rs | 232 ++++
.../src/server/plugin_remote/worker.rs | 12 +-
crates/runtime-host/src/server/sessions.rs | 26 +-
crates/runtime-host/src/server/sessions/create.rs | 2 +-
.../runtime-host/src/server/sessions/mutation.rs | 2 +-
crates/runtime-host/src/server/skills.rs | 127 ---
crates/runtime-host/src/server/skills/sources.rs | 159 ---
.../src/server/subscriptions/transcript.rs | 1 -
crates/runtime-host/src/session/projection.rs | 2 +-
.../tests/fixtures/background-plugin.mjs | 303 +++++
.../runtime-host/tests/fixtures/files-plugin.mjs | 2 +-
crates/runtime-host/tests/fixtures/host-plugin.mjs | 74 +-
crates/runtime-host/tests/fixtures/http-plugin.mjs | 2 +-
.../runtime-host/tests/fixtures/remote-plugin.mjs | 52 +
.../tests/integration/access_expiry.rs | 1 -
.../tests/integration/artifact_boundary.rs | 2 +-
.../runtime-host/tests/integration/auto_context.rs | 2 +-
.../tests/integration/client_capability.rs | 5 +-
.../runtime-host/tests/integration/client_forms.rs | 3 +-
.../tests/integration/client_interactions.rs | 3 +-
.../tests/integration/connection_multiplex.rs | 3 +-
.../tests/integration/execution_drain.rs | 3 +-
crates/runtime-host/tests/integration/handoff.rs | 2 -
.../runtime-host/tests/integration/host_drain.rs | 3 +-
.../runtime-host/tests/integration/input_plugin.rs | 4 +-
.../tests/integration/javascript_plugins.rs | 25 +-
.../integration/javascript_plugins/services.rs | 231 ++++
crates/runtime-host/tests/integration/main.rs | 1 +
.../tests/integration/message_recovery.rs | 12 -
.../tests/integration/message_submit.rs | 40 +-
.../tests/integration/model_overrides.rs | 2 +-
.../tests/integration/plugin_authorization.rs | 422 +++++++
.../tests/integration/plugin_clients.rs | 4 +-
.../tests/integration/plugin_commands.rs | 2 +-
.../tests/integration/plugin_commands/boundary.rs | 12 +
.../runtime-host/tests/integration/plugin_files.rs | 2 +-
.../tests/integration/plugin_remote.rs | 19 +
.../tests/integration/plugin_remote/fixture.rs | 27 +-
.../tests/integration/plugin_remote/javascript.rs | 75 ++
crates/runtime-host/tests/integration/pruning.rs | 2 +-
.../tests/integration/question_boundary.rs | 2 +-
.../tests/integration/remote_capacity.rs | 1 -
.../runtime-host/tests/integration/retirement.rs | 8 +-
.../tests/integration/skills_management.rs | 140 +--
.../tests/integration/skills_plugin.rs | 107 +-
.../tests/integration/skills_plugin/client.rs | 58 +
.../tests/integration/support/message_recovery.rs | 2 +-
.../runtime-host/tests/integration/support/peer.rs | 3 +-
.../tests/integration/transcript_limits.rs | 1 -
.../tests/integration/transcript_pager.rs | 1 -
crates/runtime-host/tests/integration/workhub.rs | 53 +-
crates/runtime/src/archive/reference.rs | 75 +-
crates/runtime/src/capability.rs | 2 +-
crates/runtime/src/capability/frame.rs | 27 +-
crates/runtime/src/configuration/policy.rs | 2 +-
crates/runtime/src/configuration/policy/codec.rs | 8 +-
crates/runtime/src/context.rs | 37 +-
crates/runtime/src/context/checkpoint_mode.rs | 6 -
crates/runtime/src/event.rs | 8 +-
crates/runtime/src/event_write.rs | 12 +-
crates/runtime/src/handoff/execution.rs | 60 +-
crates/runtime/src/input.rs | 39 +-
crates/runtime/src/input/selections.rs | 46 +
crates/runtime/src/message.rs | 38 +-
crates/runtime/src/skills.rs | 14 -
crates/runtime/src/tools/journal.rs | 20 +-
crates/runtime/src/tools/preparation.rs | 19 +
crates/runtime/src/workhub.rs | 1 -
crates/runtime/src/workhub/correction.rs | 5 +-
crates/runtime/tests/archive.rs | 56 +-
crates/skills/src/catalog.rs | 15 +-
crates/skills/src/discovery/sources.rs | 10 +-
crates/skills/src/plugin.rs | 39 +-
crates/skills/src/plugin/mutation.rs | 19 +-
crates/skills/src/plugin/preferences.rs | 161 +++
crates/skills/src/plugin/remote.rs | 28 +-
crates/skills/src/publication.rs | 17 -
crates/skills/tests/management.rs | 323 ------
crates/skills/tests/plugin.rs | 294 -----
crates/skills/tests/publication.rs | 18 +-
crates/tool-catalog/src/plugins.rs | 24 +-
crates/tool-catalog/src/plugins/binding.rs | 1 +
crates/tools/src/client.rs | 6 +-
.../tools/tests/integration/client_preparation.rs | 7 +-
crates/tools/tests/integration/commit_faults.rs | 3 +-
crates/tools/tests/integration/plugins.rs | 54 +-
.../tests/integration/support/model_fixture.rs | 3 +-
.../tools/tests/integration/support/preflight.rs | 3 +-
package-lock.json | 1 +
.../cli/src/__tests__/acp-session-registry.test.ts | 48 +-
packages/cli/src/__tests__/pi-tui-runner.test.ts | 88 +-
...untime-host-capability-provider-command.test.ts | 3 +-
.../__tests__/runtime-host-profile-command.test.ts | 9 +-
.../__tests__/runtime-host-service-manager.test.ts | 30 +-
.../__tests__/runtime-host-session-driver.test.ts | 47 +-
.../__tests__/runtime-host-session-update.test.ts | 13 -
packages/cli/src/acp/session-registry.ts | 10 +-
packages/cli/src/pi-tui-runner.ts | 13 +-
packages/cli/src/pi-tui-turn.ts | 4 +-
packages/cli/src/runtime-host-cli.ts | 17 +-
packages/cli/src/runtime-host-run-command.ts | 9 +-
packages/cli/src/runtime-host-service-manager.ts | 24 +-
packages/cli/src/runtime-host-session-driver.ts | 61 +-
packages/cli/src/runtime-host-session-update.ts | 26 +-
packages/cli/src/runtime-host-skills.ts | 94 ++
packages/cli/src/runtime-host-tui-context.ts | 11 +-
packages/cli/src/runtime-host-update-command.ts | 42 +-
packages/cli/src/session-driver.ts | 20 +-
packages/core/src/skill-invocation.ts | 25 +
packages/plugin-sdk/README.md | 3 +
packages/plugin-sdk/README.zh-CN.md | 4 +
packages/plugin-sdk/src/authorization.ts | 60 +
packages/plugin-sdk/src/client.ts | 9 +
packages/plugin-sdk/src/execution.ts | 49 +-
packages/plugin-sdk/src/filesystem.ts | 9 +-
packages/plugin-sdk/src/host.ts | 143 ++-
packages/plugin-sdk/tsconfig.acceptance.json | 1 +
packages/runtime-host/package.json | 1 +
.../src/__tests__/authenticated-websocket.test.ts | 18 -
.../src/__tests__/catalog-reader.test.ts | 83 +-
.../__tests__/client-capability-channel.test.ts | 12 +-
.../client-capability-coordinator.test.ts | 7 +-
.../__tests__/client-capability-protocol.test.ts | 58 +-
.../src/__tests__/connection-session.test.ts | 4 +-
.../src/__tests__/execution-composition.test.ts | 495 ---------
.../src/__tests__/execution-host-recovery.test.ts | 21 +-
.../src/__tests__/host-handoff.test.ts | 42 +-
.../runtime-host/src/__tests__/host-kernel.test.ts | 25 +-
.../src/__tests__/host-profile.test.ts | 55 +-
.../src/__tests__/message-coordinator.test.ts | 2 +-
.../runtime-host/src/__tests__/protocol.test.ts | 346 ++----
.../src/__tests__/root-turn-coordinator.test.ts | 10 +-
.../src/__tests__/session-catalog-protocol.test.ts | 40 +-
.../session-catalog-two-client-uds.test.ts | 84 +-
.../session-collaboration-authority.test.ts | 6 +-
.../session-revision-two-client-uds.test.ts | 8 +-
.../__tests__/skill-catalog-coordinator.test.ts | 301 -----
.../src/__tests__/skill-catalog-protocol.test.ts | 725 ------------
.../src/__tests__/skill-catalog-repository.test.ts | 180 +--
.../__tests__/skill-catalog-two-client-uds.test.ts | 254 -----
.../src/__tests__/ssh-operator-activation.test.ts | 11 +-
packages/runtime-host/src/client/catalog-reader.ts | 100 +-
packages/runtime-host/src/client/connection.ts | 15 +-
.../runtime-host/src/client/host-handoff-copy.ts | 11 +-
packages/runtime-host/src/client/host-profile.ts | 133 +--
packages/runtime-host/src/client/index.ts | 4 +-
packages/runtime-host/src/client/plugin-remote.ts | 151 +++
.../src/client/ssh-operator-activation.ts | 2 +-
packages/runtime-host/src/operator/index.ts | 2 -
.../runtime-host/src/operator/operator-command.ts | 35 +-
packages/runtime-host/src/operator/setup-frame.ts | 6 +-
.../runtime-host/src/protocol/client-capability.ts | 45 +-
.../runtime-host/src/protocol/external-session.ts | 14 +-
packages/runtime-host/src/protocol/host-status.ts | 25 +-
packages/runtime-host/src/protocol/index.ts | 395 +------
packages/runtime-host/src/protocol/message.ts | 61 +-
packages/runtime-host/src/protocol/operations.ts | 15 +-
.../src/protocol/plugin-authorization.ts | 223 ++++
.../runtime-host/src/protocol/plugin-remote.ts | 25 +-
.../runtime-host/src/protocol/session-catalog.ts | 51 +-
.../src/protocol/session-retirement.ts | 9 +-
.../runtime-host/src/protocol/session-revision.ts | 9 +-
.../runtime-host/src/protocol/session-status.ts | 10 +-
.../runtime-host/src/protocol/skill-catalog.ts | 1048 +-----------------
packages/runtime-host/src/protocol/turn.ts | 138 ++-
.../src/protocol/workhub-coordination.ts | 8 +-
.../server/client-capability-invocation-broker.ts | 3 +-
.../src/server/execution-composition.ts | 68 +-
packages/runtime-host/src/server/host-kernel.ts | 27 +-
.../src/server/interactive-turn-coordinator.ts | 20 +-
.../runtime-host/src/server/message-coordinator.ts | 28 +-
.../src/server/operation-dispatcher.ts | 3 -
.../src/server/session-catalog-coordinator.ts | 24 +-
.../src/server/session-retirement-coordinator.ts | 6 +-
.../src/server/skill-catalog-coordinator.ts | 249 +----
packages/runtime/src/tool-runtime.ts | 17 +-
packages/storage/package.json | 1 -
.../storage/src/__tests__/credential-store.test.ts | 53 +-
.../storage/src/__tests__/file-update-lock.test.ts | 4 -
.../__tests__/fixtures/file-update-lock-holder.ts | 14 +-
packages/storage/src/credential-store.ts | 37 +-
packages/storage/src/file-update-lock.ts | 52 -
.../src/process-lifetime-file-update-lock.ts | 125 +--
packages/ui/src/client-plugins/index.ts | 2 +-
packages/ui/src/client-plugins/instance.ts | 24 +-
packages/ui/src/client-plugins/runtime.ts | 5 +-
tests/fixtures/client-artifact-entry.mjs | 2 +-
tests/fixtures/client-candidate-entry.mjs | 3 +-
tests/fixtures/client-capability-host.mjs | 4 +-
tests/fixtures/client-capability-service.mjs | 8 +-
tests/fixtures/client-entry.mjs | 2 +-
tests/fixtures/client-message-recovery.mjs | 2 +-
tests/fixtures/client-message-submit.mjs | 1162 ++++++++++----------
tests/fixtures/client-model-overrides-entry.mjs | 2 +-
tests/fixtures/client-plugin-remote.mjs | 41 +
tests/fixtures/client-runtime-policy-entry.mjs | 2 +-
tests/fixtures/client-skills-entry.mjs | 32 +-
tests/fixtures/client-system-prompt-entry.mjs | 2 +-
tests/fixtures/client-turn.mjs | 2 +-
tests/fixtures/client-workhub-answer.mjs | 4 +-
tests/fixtures/client-workhub-delegation.mjs | 4 +-
tests/fixtures/client-workhub-entry.mjs | 20 +-
tests/fixtures/client-workhub-plugin.mjs | 42 +-
tests/fixtures/client-workhub-queue.mjs | 2 +-
562 files changed, 13151 insertions(+), 15406 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 650fdd679a..a1491c8723 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4416,7 +4416,6 @@ dependencies = [
"maka-plugins",
"maka-presentation",
"maka-runtime",
- "maka-skills",
"regress",
"serde",
"serde_json",
@@ -4454,6 +4453,7 @@ dependencies = [
"hyper",
"hyper-util",
"jiff",
+ "jsonschema",
"libc",
"maka-agent",
"maka-client-capability",
diff --git a/apps/desktop/src/main/__tests__/browser-message-box.test.ts b/apps/desktop/src/main/__tests__/browser-message-box.test.ts
index cbb8e717f8..13beed42ca 100644
--- a/apps/desktop/src/main/__tests__/browser-message-box.test.ts
+++ b/apps/desktop/src/main/__tests__/browser-message-box.test.ts
@@ -141,6 +141,20 @@ test('drives the BrowserWindow lifecycle through a safe response URL', async ()
});
test('maps close to cancel and falls back after each BrowserWindow presentation failure', async () => {
+ const cancelled = fakeBrowserWindow({pendingLoad:true});
+ const cancellation = new AbortController();
+ const cancelledResult = showBrowserMessageBoxWithRuntime(
+ {message:'Allow background work?',buttons:['Cancel','Allow'],cancelId:0,signal:cancellation.signal},
+ undefined, ACTIVE_APPEARANCE, runtimeForWindow(cancelled),
+ );
+ cancellation.abort();
+ assert.deepEqual(await cancelledResult,{response:0,checkboxChecked:false});
+ assert.equal(cancelled.destroyed(),true);
+ const failed = fakeBrowserWindow({loadError:new Error('renderer failed')});
+ await assert.rejects(showBrowserMessageBoxWithRuntime(
+ {message:'Allow background work?',signal:new AbortController().signal},
+ undefined, ACTIVE_APPEARANCE, runtimeForWindow(failed),
+ ), /renderer failed/); // Never enter a synchronous, uninterruptible native fallback.
const closed = fakeBrowserWindow();
const closeResult = showBrowserMessageBoxWithRuntime(
{ message: 'Recover Maka', buttons: ['Recover', 'Cancel'], cancelId: 1 },
diff --git a/apps/desktop/src/main/__tests__/browser-tools.test.ts b/apps/desktop/src/main/__tests__/browser-tools.test.ts
index 6ecce3c0e2..390238dee7 100644
--- a/apps/desktop/src/main/__tests__/browser-tools.test.ts
+++ b/apps/desktop/src/main/__tests__/browser-tools.test.ts
@@ -280,8 +280,7 @@ describe('browser tool execution', () => {
serverId: 'desktop_browser',
toolName: 'browser_snapshot',
arguments: {},
- sessionId: 's1',
- turnId: 't1',
+ source: { kind: 'agent', sessionId: 's1', turnId: 't1' },
toolCallId: 'c1',
cwd: '/tmp',
},
diff --git a/apps/desktop/src/main/__tests__/client-plugin-remote.test.ts b/apps/desktop/src/main/__tests__/client-plugin-remote.test.ts
index 374d8b4855..5ff2d56758 100644
--- a/apps/desktop/src/main/__tests__/client-plugin-remote.test.ts
+++ b/apps/desktop/src/main/__tests__/client-plugin-remote.test.ts
@@ -41,6 +41,73 @@ function deferred<T>() {
return { resolve, promise };
}
+test('plugin consent is explicit and cannot outlive its document or publication', async () => {
+ const owner = renderer();
+ const handlers = new Map<string, IpcHandler>();
+ let nonce = randomUUID();
+ let published = true;
+ let confirmation = deferred<boolean>();
+ let showing = deferred<AbortSignal>();
+ let writes = 0;
+ const identity = {entryId:'view',extensionId:'example',activation:randomUUID(),contentDigest:'sha256-'+'a'.repeat(64),clientDigest:'sha256-'+'b'.repeat(64)};
+ const proposal = {client:identity,scope:'profile',command:{kind:'approve',request:{operationId:randomUUID(),title:'Send reminders',target:{kind:'profile'},capabilities:['notifications']}}};
+ const dispose = registerClientPluginRemoteIpc({
+ ipcMain: {handle: (name, handler) => {handlers.set(name,handler);}},
+ ownsRenderer: (contents) => contents === owner.emitter as unknown as WebContents,
+ report: assert.ifError,
+ client: {hostEpoch:'host',async request() {throw new Error('Unexpected Remote command');}},
+ authorization: {
+ async validate() {if (!published) throw new Error('Client publication retired');},
+ confirm(_input, signal) {showing.resolve(signal); return confirmation.promise;},
+ async request() {writes++; return {kind:'grant',grant:null};},
+ },
+ });
+ try {
+ const {epoch} = await handlers.get('plugins:connection')!(owner.event, nonce);
+ const invoke = (requestId = randomUUID()) => handlers.get('plugins:authorization')!(owner.event, nonce, epoch, proposal, requestId);
+ let pending = invoke();
+ await showing.promise;
+ confirmation.resolve(false);
+ assert.deepEqual(await pending,{kind:'grant',grant:null});
+ assert.equal(writes,0);
+ confirmation = deferred(); showing = deferred();
+ pending = invoke();
+ await showing.promise;
+ confirmation.resolve(true);
+ await pending;
+ assert.equal(writes,1);
+ confirmation = deferred(); showing = deferred();
+ const requestId = randomUUID();
+ pending = invoke(requestId);
+ const retiredSignal = await showing.promise;
+ await handlers.get('plugins:authorization-cancel')!(owner.event, randomUUID(), epoch, requestId);
+ assert.equal(retiredSignal.aborted,false,'another document cannot cancel this approval');
+ await handlers.get('plugins:authorization-cancel')!(owner.event, nonce, epoch, requestId);
+ assert.equal(retiredSignal.aborted,true);
+ confirmation.resolve(true);
+ assert.deepEqual(await pending,{kind:'grant',grant:null});
+ assert.equal(writes,1);
+ confirmation = deferred(); showing = deferred();
+ pending = invoke();
+ const signal = await showing.promise;
+ owner.emitter.emit('did-start-navigation',{},'new-page',false,true);
+ assert.equal(signal.aborted,true);
+ confirmation.resolve(true); // A late click must never reach Host.
+ assert.deepEqual(await pending,{kind:'grant',grant:null});
+ assert.equal(writes,1);
+ nonce = randomUUID();
+ await handlers.get('plugins:connection')!(owner.event, nonce);
+ confirmation = deferred(); showing = deferred();
+ const rejected = assert.rejects(invoke(),/publication retired/);
+ await showing.promise;
+ published = false;
+ confirmation.resolve(true);
+ await rejected;
+ assert.equal(writes,1);
+ } finally {await dispose();}
+ assert.deepEqual(owner.emitter.eventNames(),[]);
+});
+
test('reconnecting to the same Host revokes old Remote leases without fencing the replacement Client', async () => {
const owner = renderer();
const nonce = randomUUID();
@@ -101,7 +168,7 @@ test('Remote documents belong to one Renderer and drain navigation, late opens a
let late: ReturnType<typeof deferred<string>> | undefined;
let attempts = 0;
const dispose = registerClientPluginRemoteIpc({
- ipcMain: { handle: (channel, listener) => { if (channel === 'plugins:remote') handler = listener; else if (channel === 'plugins:files') files = listener; else connection = listener; } },
+ ipcMain: { handle: (channel, listener) => { if (channel === 'plugins:remote') handler = listener; else if (channel === 'plugins:files') files = listener; else if (channel === 'plugins:connection') connection = listener; } },
ownsRenderer: (contents: WebContents) => allowed.has(contents as unknown as typeof one.emitter),
report: (error) => errors.push(error),
client: { hostEpoch: 'host-process', async request(_operation, input) {
diff --git a/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts b/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts
index dc75925683..a6f9ebd10c 100644
--- a/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts
+++ b/apps/desktop/src/main/__tests__/main-process-diagnostics.test.ts
@@ -62,12 +62,12 @@ const runtimeHostDiagnostics = {
activeOperations: 1,
activeResidencies: 0,
upgradeBlockingActivity: true,
+ drainResidencies: 0,
residencies: [],
protocolVersion: 0,
compatibilityEpoch: 16,
pid: 42,
processUptimeSeconds: 37,
- nodeVersion: '22.0.0',
platform: 'linux' as const,
arch: 'x64',
osRelease: '6.6.0',
diff --git a/apps/desktop/src/main/__tests__/managed-artifact-preview.test.ts b/apps/desktop/src/main/__tests__/managed-artifact-preview.test.ts
index 61c99344a7..e3265d7123 100644
--- a/apps/desktop/src/main/__tests__/managed-artifact-preview.test.ts
+++ b/apps/desktop/src/main/__tests__/managed-artifact-preview.test.ts
@@ -158,7 +158,7 @@ test('tool binds to the admitted session and returns endpoint evidence only', as
return service.prepare('h', client(), sessionId, artifactId, signal);
});
assert.equal((tool!.parameters as z.ZodType).safeParse({ artifactId: 'a1', sessionId: 'other' }).success, false);
- const result = await tool!.impl({ artifactId: 'a1' }, { sessionId: 's1', turnId: 't1', cwd: '/tmp', toolCallId: 'c1', abortSignal: new AbortController().signal, emitOutput: () => {} });
+ const result = await tool!.impl({ artifactId: 'a1' }, { sessionId: 's1', cwd: '/tmp', toolCallId: 'c1', abortSignal: new AbortController().signal, emitOutput: () => {} });
assert.equal((result as { loaded: boolean }).loaded, false);
} finally { await service.close(); }
});
diff --git a/apps/desktop/src/main/__tests__/mcp-runtime-e2e.test.ts b/apps/desktop/src/main/__tests__/mcp-runtime-e2e.test.ts
index 7d14c551ed..4d76c4eb8b 100644
--- a/apps/desktop/src/main/__tests__/mcp-runtime-e2e.test.ts
+++ b/apps/desktop/src/main/__tests__/mcp-runtime-e2e.test.ts
@@ -118,8 +118,7 @@ test('MCP tools stay bound to the connection generation that advertised them', a
serverId: 'fixture',
toolName: 'missing',
arguments: {},
- sessionId: 'session',
- turnId: 'turn',
+ source: { kind: 'agent', sessionId: 'session', turnId: 'turn' },
toolCallId: 'incompatible-capability-call',
cwd: process.cwd(),
},
@@ -142,8 +141,7 @@ test('MCP tools stay bound to the connection generation that advertised them', a
serverId: 'fixture',
toolName: 'annotated',
arguments: { fallback: 'desktop-capability' },
- sessionId: 'session',
- turnId: 'turn',
+ source: { kind: 'agent', sessionId: 'session', turnId: 'turn' },
toolCallId: 'annotated-capability-call',
cwd: process.cwd(),
},
@@ -171,8 +169,7 @@ test('MCP tools stay bound to the connection generation that advertised them', a
serverId: 'fixture',
toolName: 'echo',
arguments: { value: 'desktop-capability' },
- sessionId: 'session',
- turnId: 'turn',
+ source: { kind: 'agent', sessionId: 'session', turnId: 'turn' },
toolCallId: 'capability-call',
cwd: process.cwd(),
},
diff --git a/apps/desktop/src/main/__tests__/permission-response-ipc-boundary.test.ts b/apps/desktop/src/main/__tests__/permission-response-ipc-boundary.test.ts
index d7dcff031c..5eb378e9f8 100644
--- a/apps/desktop/src/main/__tests__/permission-response-ipc-boundary.test.ts
+++ b/apps/desktop/src/main/__tests__/permission-response-ipc-boundary.test.ts
@@ -173,7 +173,7 @@ describe('permission response IPC boundary', () => {
turnId: 'turn-1',
text: 'review @packages/ui/src/chat turn.tsx',
displayText: 'review @packages/ui/src/chat turn.tsx',
- skillIds: ['weekly-report', 'project:maka:writer'],
+ inputSelections: { "maka.skills": ['weekly-report', 'project:maka:writer'] },
attachmentItems: [{ approvalId: 'a', name: 'n' }],
retainedAttachments: [
{
@@ -207,7 +207,7 @@ describe('permission response IPC boundary', () => {
turnId: 'turn-1',
text: 'review @packages/ui/src/chat turn.tsx',
displayText: 'review @packages/ui/src/chat turn.tsx',
- skillIds: ['weekly-report', 'project:maka:writer'],
+ inputSelections: { "maka.skills": ['weekly-report', 'project:maka:writer'] },
attachmentItems: [{ approvalId: 'a', name: 'n' }],
retainedAttachments: [
{
@@ -233,10 +233,10 @@ describe('permission response IPC boundary', () => {
},
);
assert.equal(normalizeSessionSendCommand({ type: 'stop' }), undefined);
- assert.deepEqual(normalizeSessionSendCommand({ type: 'send', text: '', skillIds: ['writer'] }), {
+ assert.deepEqual(normalizeSessionSendCommand({ type: 'send', text: '', inputSelections: { "maka.skills": ['writer'] } }), {
type: 'send',
text: '',
- skillIds: ['writer'],
+ inputSelections: { "maka.skills": ['writer'] },
});
});
@@ -256,7 +256,7 @@ describe('permission response IPC boundary', () => {
// base64 bytes before IPC, and main resolves only the encoded shapes.
{ type: 'send', text: 'hello', attachmentItems: [{ file: {} }] },
{ type: 'send', text: 'hello', turnId: 1 },
- { type: 'send', text: 'hello', skillIds: ['/bad'] },
+ { type: 'send', text: 'hello', inputSelections: { "maka.skills": ['bad\nselector'] } },
{ type: 'send', text: 'hello', turnOrchestration: { mode: 'swarm', source: 'prompt' } },
{ type: 'send', text: 'hello', quotes: {} },
{ type: 'send', text: 'hello', quotes: Array(17).fill({ text: 'x' }) },
diff --git a/apps/desktop/src/main/__tests__/runtime-host-bot-session-adapter.test.ts b/apps/desktop/src/main/__tests__/runtime-host-bot-session-adapter.test.ts
index 780eaae806..9fe7a16b59 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-bot-session-adapter.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-bot-session-adapter.test.ts
@@ -325,11 +325,12 @@ test('returns blocked Skill feedback without waiting for a Turn that was not cre
}),
startTurn: async () => ({
kind: 'blocked',
- skillInvocation: {
+ message: 'writer: not_found',
+ preparation: fixturePreparation({
loaded: [],
failed: [{ request: 'writer', reason: 'not_found' }],
receipts: [],
- },
+ }),
}),
}),
resolveCreateTarget: hostPathCreateTarget,
@@ -449,7 +450,7 @@ function startedTurn(turn: TurnSnapshot) {
return {
kind: 'started' as const,
turn,
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
}
@@ -538,3 +539,7 @@ class AsyncFrameQueue implements AsyncIterable<SubscriptionFrame> {
};
}
}
+
+function fixturePreparation(receipt: unknown): import('@maka/runtime-host/protocol').InputReceipt[] {
+ return [{ source: { kind: 'input', name: 'maka.skills', packageId: 'maka.skills', entryId: 'skills', activation: "fixture", revision: "1" }, receipt: JSON.parse(JSON.stringify(receipt)) }];
+}
diff --git a/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts b/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
index d0b67b69b2..7e8c75301e 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-client-uds.test.ts
@@ -25,7 +25,7 @@ import test from 'node:test';
import type { IpcMain } from 'electron';
import type { BotRegistry } from '@maka/runtime/bots';
import type { ComputerUseToolSet } from '@maka/runtime/computer-use-tools';
-import type { MakaTool } from '@maka/runtime/tool-runtime';
+import type { SessionTool } from '@maka/runtime/tool-runtime';
import { connectRuntimeHost } from '@maka/runtime-host/client';
import { acquireOperationalStateDatabase } from '@maka/storage/operational-state-store';
import {
@@ -82,7 +82,7 @@ test('drives Desktop Session operations through a real Runtime Host connection',
result: {
disposition: 'steering',
queueRevision: 1,
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
};
},
@@ -118,7 +118,7 @@ test('drives Desktop Session operations through a real Runtime Host connection',
{
disposition: 'steering',
queueRevision: 1,
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
);
@@ -390,7 +390,7 @@ test('drives the renderer Session execution facade through real UDS framing', as
result: {
disposition: 'turn_started',
turnId: 'turn-host-1',
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
};
},
@@ -444,7 +444,7 @@ test('drives the renderer Session execution facade through real UDS framing', as
turnId: 'turn-host-1',
attachments: [],
inlineReferences: [],
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
);
@@ -643,7 +643,7 @@ function session(
};
}
-function nativeTool(): MakaTool {
+function nativeTool(): SessionTool {
return {
name: 'browser_snapshot',
description: 'Capture the current page.',
diff --git a/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts b/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts
index 7509f10fd9..5d2fa0d2e5 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-desktop-candidate.test.ts
@@ -30,6 +30,7 @@ import type { BotIncomingMessage, BotRegistry } from '@maka/runtime/bots';
import type { ComputerUseToolSet } from '@maka/runtime/computer-use-tools';
import type { ShellRunUpdate } from '@maka/core/events';
import type { MakaTool } from '@maka/runtime/tool-runtime';
+import type { SessionTool } from '@maka/runtime/tool-runtime';
import type {
ClientCapabilityProvider,
ConnectOrSpawnRuntimeHostInput,
@@ -150,6 +151,7 @@ test('updates a protocol-compatible managed Host before exposing a candidate ove
activeOperations: 0,
processUptimeSeconds: 1,
residencies: [],
+ drainResidencies: 0,
},
replacement: {
kind: 'repair',
@@ -733,7 +735,7 @@ test('closes the claimed Host connection when native capability construction fai
const invalidTool = {
...nativeTool(),
parameters: z.string(),
- } as unknown as MakaTool;
+ } as unknown as SessionTool;
await assert.rejects(
() =>
@@ -1280,7 +1282,7 @@ function emptyComputerUseTools(): ComputerUseToolSet {
return Object.assign([], { clearSession() {} }) as unknown as ComputerUseToolSet;
}
-function nativeTool(): MakaTool {
+function nativeTool(): SessionTool {
return {
name: 'browser_snapshot',
description: 'Capture the current page.',
@@ -1696,8 +1698,7 @@ function capabilityFrame(sessionId: string): ClientCapabilityCallFrame {
offerId: 'desktop_browser',
serverId: 'desktop_browser',
toolName: 'browser_snapshot',
- sessionId,
- turnId: `turn-${sessionId}`,
+ source: { kind: 'agent', sessionId, turnId: `turn-${sessionId}` },
cwd: '/workspace',
toolCallId: `tool-${sessionId}`,
arguments: {},
diff --git a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts
index 47bfb39e9c..9fe810c91d 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts
@@ -1867,7 +1867,7 @@ test('an exact legacy ephemeral stop always requires consent and preserves the O
registration: { ...observed.registration, lifecycleMode: 'ephemeral' as const },
processIdentity: { startIdentity: 'darwin:1700000000:123456' },
handshake: { ...observed.handshake, activity: {
- connections: 0, activeOperations: 0, processUptimeSeconds: 60, residencies: [],
+ connections: 0, activeOperations: 0, processUptimeSeconds: 60, residencies: [], drainResidencies: 0,
} },
};
const replacement = candidateHarness();
@@ -1924,7 +1924,7 @@ test('keeps a known repair actionable when its first authority inspection fails'
return {
identity: 'verified-repair', target: { name: 'Local', location: 'local' },
reason: 'repair', mayExitNaturally: false,
- activity: { connections: 0, activeOperations: 0, processUptimeSeconds: 1, residencies: [] },
+ activity: { connections: 0, activeOperations: 0, processUptimeSeconds: 1, residencies: [], drainResidencies: 0 },
replacement: { kind: 'repair', canReplaceIdle: true, canInterrupt: false,
execute: async () => { didRepair = true; return { kind: 'completed' }; } },
};
@@ -2008,6 +2008,7 @@ function upgradeRequired(
activeOperations,
processUptimeSeconds: 60,
residencies,
+ drainResidencies: residencies.reduce((count, item) => count + item.count, 0),
},
}
: {}),
diff --git a/apps/desktop/src/main/__tests__/runtime-host-local-remote-access.test.ts b/apps/desktop/src/main/__tests__/runtime-host-local-remote-access.test.ts
index 3c788c7f50..8ab42ea048 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-local-remote-access.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-local-remote-access.test.ts
@@ -518,76 +518,7 @@ test('does not persist recoverable setup authority before Desktop ownership comm
assert.equal(setupCalls, 0);
});
-test('adopts a released handoff through its existing legacy operator', async (t) => {
- const base = await mkdtemp(join(tmpdir(), 'maka-local-remote-access-prestart-'));
- t.after(() => rm(base, { recursive: true, force: true }));
- const clientDataRoot = join(base, 'client');
- const rootPath = join(clientDataRoot, 'workspaces', 'default');
- const rootId = 'a'.repeat(64);
- const deploymentId = '22222222-2222-4222-8222-222222222222';
- const deploymentRoot = join(base, 'installed');
- await mkdir(rootPath, { recursive: true });
- await writeFile(
- join(clientDataRoot, 'runtime-host-local-service.json'),
- `${JSON.stringify({
- schemaVersion: 1,
- state: 'handoff',
- rootPath,
- rootId,
- coordinationRelays: [],
- allowInterruptActiveTasks: true,
- })}\n`,
- );
- const service = createDesktopLocalRuntimeHostRemoteAccess({
- ipcMain: { handle() {}, removeHandler() {} },
- clientDataRoot,
- rootPath,
- rootId,
- directPeerAvailable: false,
- manager: () => assert.fail('pre-start reconciliation must not require the Local manager'),
- resolveManagedDeploymentAuthority: async () => ({
- kind: 'active',
- lifecycleMode: 'supervised',
- deploymentRoot,
- target: {
- schemaVersion: 2,
- serviceId: rootId,
- operator: testOperator(join(deploymentRoot, 'operator.mjs')),
- rootPath,
- rootId,
- deploymentId,
- },
- }),
- resolveSetupPackage: async () =>
- assert.fail('committed authority must not resolve a package'),
- operator: {
- async runSetup() {
- assert.fail('committed authority must not replay setup');
- },
- async close() {},
- } as unknown as ReturnType<typeof createDesktopRuntimeHostLocalOperator>,
- });
- t.after(() => service.close());
-
- assert.equal(await service.recoverBeforeLocalHostStart(), true);
- assert.deepEqual(
- JSON.parse(await readFile(join(clientDataRoot, 'runtime-host-local-service.json'), 'utf8')),
- {
- schemaVersion: 2,
- state: 'managed',
- serviceId: rootId,
- operator: {
- kind: 'legacy_posix_executable',
- executablePath: join(deploymentRoot, 'operator'),
- },
- rootPath,
- rootId,
- deploymentId,
- },
- );
-});
-
-test('migrates a released managed receipt before exposing it to lifecycle operations', async (t) => {
+test('rejects an obsolete managed receipt without changing it', async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-local-managed-migration-'));
t.after(() => rm(base, { recursive: true, force: true }));
const clientDataRoot = join(base, 'client');
@@ -622,21 +553,9 @@ test('migrates a released managed receipt before exposing it to lifecycle operat
});
t.after(() => service.close());
- const target = await service.inspectManaged(async (managed) => managed);
- const expected = {
- schemaVersion: 2,
- state: 'managed',
- serviceId: rootId,
- operator: {
- kind: 'legacy_posix_executable',
- executablePath: operatorPath,
- },
- rootPath,
- rootId,
- deploymentId: RECOVERY_DEPLOYMENT_ID,
- };
- assert.deepEqual(target, expected);
- assert.deepEqual(JSON.parse(await readFile(lifecyclePath, 'utf8')), expected);
+ const before = await readFile(lifecyclePath, 'utf8');
+ await assert.rejects(service.inspectManaged(async (target) => target), /lifecycle is invalid/u);
+ assert.equal(await readFile(lifecyclePath, 'utf8'), before);
});
test('interrupted Local Host setup converges to its exact managed service', async (t) => {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-managed-services.test.ts b/apps/desktop/src/main/__tests__/runtime-host-managed-services.test.ts
index 4a7be3d104..e815ee952b 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-managed-services.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-managed-services.test.ts
@@ -67,7 +67,7 @@ afterEach(async () => {
);
});
-test("migrates released WSL deployment bindings to the legacy operator route", async () => {
+test("rejects obsolete deployment bindings without rewriting them", async () => {
const root = await mkdtemp(join(tmpdir(), "maka-managed-wsl-migration-"));
roots.push(root);
const path = join(root, "runtime-host-deployments.json");
@@ -94,28 +94,9 @@ test("migrates released WSL deployment bindings to the legacy operator route", a
})}\n`,
);
- const document = await createDesktopRuntimeHostManagedServiceStore(root).read();
- const binding = document.bindings[0];
- assert.equal(binding?.profile.kind, "environment");
- assert.deepEqual(binding?.profile.kind === "environment" ? binding.profile.operator : null, {
- kind: "legacy_posix_executable",
- executablePath: "/home/operator/.local/share/maka/operator",
- });
- const stored = await readFile(path, "utf8");
- assert.match(stored, /"schemaVersion": 2/u);
- assert.doesNotMatch(stored, /operatorPath/u);
-
- const currentProfile = {
- id: "ubuntu",
- name: "Ubuntu",
- kind: "environment" as const,
- provider: { kind: "wsl" as const, distribution: "Ubuntu-24.04" },
- rootId: "a".repeat(64),
- operator,
- };
- const resolved = findDesktopRuntimeHostManagedServiceBinding(document, currentProfile);
- assert.equal(resolved?.profile.kind, "environment");
- assert.equal(resolved?.profile.kind === "environment" ? resolved.profile.operator : null, operator);
+ const before = await readFile(path, 'utf8');
+ await assert.rejects(createDesktopRuntimeHostManagedServiceStore(root).read(), /schema/u);
+ assert.equal(await readFile(path, 'utf8'), before);
});
test("keeps Desktop service bindings outside the shared profile catalog", async () => {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
index bc8bf1eac7..d4f1b1c1a3 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
@@ -24,7 +24,6 @@ import {
RuntimeHostOperationError,
} from '@maka/runtime-host/client';
import {
- RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY,
RUNTIME_HOST_OPERATOR_PEER_WEBRTC_STUN_CAPABILITY,
runtimeHostAccessCredentialFingerprint,
type RuntimeHostServiceManagementFrame,
@@ -1082,13 +1081,7 @@ test('retries acknowledged deployment cleanup without repeating uninstall', asyn
...unusedDirectPeerProfileDependencies(),
resolveManagedService: async () => {
const binding = managedBinding(profile, service, 'cleanup_pending');
- return {
- ...binding,
- deployment: {
- id: binding.deployment.id,
- rootPath: binding.deployment.rootPath,
- },
- };
+ return binding;
},
resolveManagedAccess: async () => undefined,
markManagedServiceUninstalling: async () =>
@@ -1155,13 +1148,7 @@ test('rechecks uninstall intent before retrying the remote service', async () =>
},
'uninstalling',
);
- return {
- ...binding,
- deployment: {
- id: binding.deployment.id,
- rootPath: binding.deployment.rootPath,
- },
- };
+ return binding;
},
resolveManagedAccess: async () => undefined,
markManagedServiceUninstalling: async (binding) => {
@@ -1322,7 +1309,7 @@ test('disables a newly enabled listener when its Desktop profile cannot be commi
directPeerClientAvailable: true,
runServiceManagement: async () => ({
...serviceResult('status'),
- operatorCapabilities: [RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY],
+ operatorCapabilities: [RUNTIME_HOST_OPERATOR_PEER_WEBRTC_STUN_CAPABILITY],
}),
runAccessManagement: async () => assert.fail('access management is not expected'),
runPeerManagement: async (input) => {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-native-capabilities.test.ts b/apps/desktop/src/main/__tests__/runtime-host-native-capabilities.test.ts
index 6bca312df3..1ba9898654 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-native-capabilities.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-native-capabilities.test.ts
@@ -33,6 +33,7 @@ import { z } from 'zod';
import { buildClientSettingsTools } from '../client-settings-tools.js';
import { buildManagedArtifactPreviewTools } from '../managed-artifact-preview-tools.js';
import { browserOriginAdmission } from '../browser/browser-origin-admission.js';
+import type { SessionToolContext } from '@maka/runtime/tool-runtime';
import { buildRiveWorkflowTool } from '../rive-workflow-tool.js';
import { createDesktopNativeCapabilityProvider } from '../runtime-host-native-capabilities.js';
@@ -58,12 +59,12 @@ test('Artifact preview is discoverable and admitted without a pre-existing brows
signal.throwIfAborted();
invoked = true;
return { url: 'http://127.0.0.1:12345/token/index.html', expiresAt: 123456, reachable: true, loaded: false };
- }),
+ }).map(tool => ({ context: 'session' as const, tool })),
}],
}, { nativeSessionId: (sessionId) => `native:${sessionId}` });
assert.doesNotThrow(() => decodeClientCapabilityReplaceInput({ registrationId: 'registration-1', offers: provider.offers() }));
assert.ok(provider.offers().some((offer) => offer.offerId === 'desktop_artifact_preview' && offer.tools.some((tool) => tool.name === 'ArtifactPreview')));
- const result = await call(provider, capabilityFrame({ offerId: 'desktop_artifact_preview', serverId: 'desktop_artifact_preview', toolName: 'ArtifactPreview', arguments: { artifactId: 'artifact-1' } }));
+ const result = await call(provider, capabilityFrame({ offerId: 'desktop_artifact_preview', serverId: 'desktop_artifact_preview', toolName: 'ArtifactPreview', arguments: { artifactId: 'artifact-1' }, source: { kind: 'background', grantId: 'preview-grant', sessionId: 'session-1' } }));
assert.equal(invoked, true);
assert.deepEqual(result.structuredContent, { url: 'http://127.0.0.1:12345/token/index.html', expiresAt: 123456, reachable: true, loaded: false });
});
@@ -615,7 +616,7 @@ test('validates before admission and invokes the exact offered tool with Host co
let received:
| {
args: unknown;
- context: Pick<MakaToolContext, 'sessionId' | 'turnId' | 'cwd' | 'toolCallId'>;
+ context: Pick<SessionToolContext, 'sessionId' | 'cwd' | 'toolCallId'>;
}
| undefined;
const provider = createDesktopNativeCapabilityProvider(
@@ -628,11 +629,11 @@ test('validates before admission and invokes the exact offered tool with Host co
url: 'https://example.com/path',
});
invoked = true;
+ assert.equal('turnId' in context, false);
received = {
args,
context: {
sessionId: context.sessionId,
- turnId: context.turnId,
cwd: context.cwd,
toolCallId: context.toolCallId,
},
@@ -675,7 +676,6 @@ test('validates before admission and invokes the exact offered tool with Host co
args: { url: 'https://example.com/path' },
context: {
sessionId: 'host-a:session-1',
- turnId: 'turn-1',
cwd: '/workspace',
toolCallId: 'tool-call-1',
},
@@ -688,6 +688,23 @@ test('validates before admission and invokes the exact offered tool with Host co
'https://example.com/path',
'https://example.com/path',
]);
+ for (const source of [
+ { kind: 'remote' as const, requestId: 'request-1', sessionId: 'session-1' },
+ { kind: 'background' as const, grantId: 'grant-1', sessionId: 'session-1' },
+ ]) {
+ admitted = false;
+ invoked = false;
+ await call(provider, capabilityFrame({ source, arguments: { url: 'https://example.com/path' } }), () => {
+ admitted = true;
+ });
+ assert.equal(invoked, true);
+ admitted = false;
+ await assert.rejects(call(provider, capabilityFrame({ source: { ...source, sessionId: null } }), () => {
+ admitted = true;
+ }), /requires a Session/u);
+ assert.equal(admitted, false);
+ }
+ await provider.close();
});
test('does not execute Browser work when its Origin changes while admission is pending', async () => {
@@ -750,7 +767,7 @@ test('watches Computer Use turns without widening Browser lifecycle', async () =
await call(
provider,
- computerFrame({ sessionId: 'session-2', turnId: 'turn-2' }),
+ computerFrame({ source: { kind: 'agent', sessionId: 'session-2', turnId: 'turn-2' } }),
);
assert.deepEqual(usedSessions, ['session-1', 'session-2']);
assert.deepEqual(computerUseTurns, [['session-2', 'turn-2']]);
@@ -798,7 +815,7 @@ test('projects Computer Use screenshots and releases all native resources for a
await call(
provider,
capabilityFrame({
- sessionId: 'browser-session',
+ source: { kind: 'agent', sessionId: 'browser-session', turnId: 'turn-1' },
toolName: 'browser_snapshot',
arguments: {},
}),
@@ -807,7 +824,7 @@ test('projects Computer Use screenshots and releases all native resources for a
assert.deepEqual(browserReleased, ['manual-session', 'browser-session']);
assert.deepEqual(computerReleased, ['manual-session', 'browser-session']);
- const completed = await call(provider, computerFrame({ sessionId: 'completed-session', arguments: {} }));
+ const completed = await call(provider, computerFrame({ source: { kind: 'agent', sessionId: 'completed-session', turnId: 'turn-1' }, arguments: {} }));
assert.deepEqual(completed, {
content: [
{ type: 'text', text: 'captured' },
@@ -818,7 +835,7 @@ test('projects Computer Use screenshots and releases all native resources for a
assert.deepEqual(browserReleased, ['manual-session', 'browser-session', 'completed-session']);
assert.deepEqual(computerReleased, ['manual-session', 'browser-session', 'completed-session']);
- const inFlight = call(provider, computerFrame({ sessionId: 'active-session', arguments: { wait: true } }));
+ const inFlight = call(provider, computerFrame({ source: { kind: 'agent', sessionId: 'active-session', turnId: 'turn-1' }, arguments: { wait: true } }));
await started;
await provider.close();
await assert.rejects(inFlight, /provider closed/u);
@@ -1285,11 +1302,11 @@ test('forwards Host cancellation to an admitted Desktop invocation', async () =>
await assert.rejects(inFlight, /Host cancelled invocation/u);
});
-function tool<P, R>(
+function tool<P, R, Context = SessionToolContext>(
name: string,
parameters: z.ZodType<P>,
- impl: (args: P, context: MakaToolContext) => Promise<R>,
-): MakaTool<P, R> {
+ impl: (args: P, context: Context) => Promise<R>,
+): MakaTool<P, R, Context> {
return {
name,
displayName: name,
@@ -1372,8 +1389,7 @@ function capabilityFrame(overrides: Partial<ClientCapabilityCallFrame> = {}): Cl
serverId: 'desktop_browser',
toolName: 'browser_navigate',
arguments: { url: 'https://example.com' },
- sessionId: 'session-1',
- turnId: 'turn-1',
+ source: { kind: 'agent', sessionId: 'session-1', turnId: 'turn-1' },
toolCallId: 'tool-call-1',
cwd: '/workspace',
...overrides,
@@ -1420,7 +1436,7 @@ test('WorkHub groups receive their target epoch and join Desktop interaction tur
});
const frame = capabilityFrame({ offerId: 'desktop_workhub', serverId: 'desktop_workhub', toolName: 'control', arguments: {} });
const result = await call(provider, frame);
- assert.deepEqual(watched, [[frame.sessionId, frame.turnId]]);
- assert.deepEqual(result.content, [{ type: 'text', text: frame.sessionId }], 'WorkHub authority sees the real Host Session id, not a native resource alias');
+ assert.deepEqual(watched, [['session-1', 'turn-1']]);
+ assert.deepEqual(result.content, [{ type: 'text', text: frame.source.sessionId }], 'WorkHub authority sees the real Host Session id, not a native resource alias');
await provider.close();
});
diff --git a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
index 42cd3d157d..40d30bf0cc 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
@@ -19,22 +19,22 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
-import type { EnvironmentRuntimeHostProfile } from '@maka/runtime-host/client';
-import type { DesktopRuntimeHostProfileAddInput } from '../../preload/bridge-contract.js';
-import type {
- DesktopRuntimeHostManagedSshServiceTarget,
- DesktopRuntimeHostManagedWslServiceTarget,
-} from '../runtime-host-managed-services.js';
import { createDesktopRuntimeHostOnboarding } from '../runtime-host-onboarding.js';
-const OPERATOR = {
- kind: 'node' as const,
- platform: 'posix' as const,
- nodePath: '/usr/bin/node',
- modulePath: '/home/operator/.local/share/maka/operator.mjs',
+import type { NativeSetupResult } from '../native-runtime-host-installer.js';
+
+const installed: NativeSetupResult = {
+ operator: { kind: 'native', platform: 'posix', executablePath: '/host/maka' },
+ receipt: {
+ deployment: { deploymentId: '00000000-0000-4000-8000-000000000001', configRevision: 1,
+ rootId: 'a'.repeat(64), rootPath: '/host/state', executable: '/host/maka',
+ sha256: 'b'.repeat(64), mode: 'on_demand', websocket: '127.0.0.1:0' },
+ host: { hostEpoch: 'epoch', pid: 123, port: 4567 },
+ pairing: { rootId: 'a'.repeat(64), credentialId: 'credential', credential: 'pairing-secret' },
+ },
};
-test('native SSH and WSL onboarding save persistent operators without legacy service authority', async () => {
+test('SSH and WSL onboarding use the verified native package without exposing credentials', async () => {
for (const kind of ['ssh', 'wsl'] as const) {
const operator = { kind: 'native' as const, platform: 'posix' as const,
executablePath: '/home/operator/.local/share/Maka/native-cli/package/bin/maka' };
@@ -57,7 +57,6 @@ test('native SSH and WSL onboarding save persistent operators without legacy ser
ssh: kind === 'ssh' ? install : async () => assert.fail('wrong transport'),
wsl: kind === 'wsl' ? install : async () => assert.fail('wrong transport'),
},
- resolveSetupPackage: () => assert.fail('native onboarding must not resolve a Node package'),
profiles: {
addAndEnableVerified: async (input) => { saved = input; return { profileId: input.profile.id }; },
addEnvironmentAndEnable: async (input) => { saved = input; return { profileId: input.profile.id }; },
@@ -76,134 +75,6 @@ test('native SSH and WSL onboarding save persistent operators without legacy ser
}
});
-test('persists a verified on-demand SSH profile without endpoint or credential projection', async () => {
- let setupInput: unknown;
- let saved:
- | (DesktopRuntimeHostProfileAddInput & {
- readonly managedService?: DesktopRuntimeHostManagedSshServiceTarget;
- })
- | undefined;
- const harness = createHarness({
- profiles: {
- addAndEnableVerified: async (input) => {
- saved = input;
- return { profileId: input.profile.id };
- },
- },
- runSetup: async (input, onProgress) => {
- setupInput = input;
- onProgress({ phase: 'installing_service' });
- return {
- serviceId: 'b'.repeat(64),
- deploymentId: '00000000-0000-4000-8000-000000000001',
- rootPath: '/home/operator/.config/Maka/workspaces/default',
- operator: OPERATOR,
- rootId: 'a'.repeat(64),
- endpoint: 'ws://127.0.0.1:7443/runtime-host',
- credential: 'secret-access-token',
- };
- },
- });
-
- const result = await harness.invoke('runtime-host-onboarding:start', {
- kind: 'ssh',
- name: 'Lab',
- destination: 'operator@example.com',
- projectDirectoryRoots: [{ label: 'Work', path: '/srv/work' }],
- });
-
- assert.equal((result as { kind?: string }).kind, 'complete');
- assert.equal(saved?.profile.name, 'Lab');
- if (saved?.profile.kind !== 'remote') assert.fail('expected remote profile');
- assert.deepEqual(saved.profile.transport, {
- kind: 'ssh',
- destination: 'operator@example.com',
- activation: {
- kind: 'ssh_operator',
- operator: OPERATOR,
- },
- });
- assert.deepEqual(saved?.managedService, {
- deployment: {
- id: 'b'.repeat(64),
- rootPath: '/home/operator/.config/Maka/workspaces/default',
- deploymentId: '00000000-0000-4000-8000-000000000001',
- },
- control: {
- kind: 'ssh_operator',
- operator: OPERATOR,
- },
- });
- assert.equal(saved?.credential, 'secret-access-token');
- assert.deepEqual(
- (setupInput as { projectDirectoryRoots?: unknown }).projectDirectoryRoots,
- [{ label: 'Work', path: '/srv/work' }],
- );
- assert.equal((setupInput as { lifecycle?: unknown }).lifecycle, 'on_demand');
- assert.equal((setupInput as { remotePlatform?: unknown }).remotePlatform, 'posix');
- assert.doesNotMatch(JSON.stringify(harness.events), /secret-access-token/u);
- await harness.onboarding.close();
- assert.equal(harness.handlers.size, 0);
-});
-
-test('onboards WSL as a credential-free environment profile', async () => {
- let saved:
- | {
- readonly profile: EnvironmentRuntimeHostProfile;
- readonly managedService?: DesktopRuntimeHostManagedWslServiceTarget;
- }
- | undefined;
- const peerTargets: string[] = [];
- const harness = createHarness({
- profiles: {
- addEnvironmentAndEnable: async (input) => {
- saved = input;
- return {
- profileId: input.profile.id,
- };
- },
- },
- runWslSetup: async (_input, _onProgress, onComplete) => {
- onComplete();
- return {
- serviceId: 'a'.repeat(64),
- deploymentId: '00000000-0000-4000-8000-000000000001',
- rootPath: '/home/operator/.config/Maka/workspaces/default',
- rootId: 'a'.repeat(64),
- operator: OPERATOR,
- };
- },
- resolveSetupPackage: (peerTarget) => {
- peerTargets.push(peerTarget);
- return { kind: 'npm', specifier: 'maka-agent@0.2.0' };
- },
- });
-
- const result = await harness.invoke('runtime-host-onboarding:start', {
- kind: 'wsl',
- distribution: 'Ubuntu-24.04',
- });
-
- assert.equal((result as { kind?: string }).kind, 'complete');
- assert.deepEqual(saved?.profile, {
- id: saved?.profile.id,
- name: 'Ubuntu-24.04',
- kind: 'environment',
- provider: { kind: 'wsl', distribution: 'Ubuntu-24.04' },
- rootId: 'a'.repeat(64),
- operator: OPERATOR,
- });
- assert.deepEqual(saved?.managedService, {
- deployment: {
- id: 'a'.repeat(64),
- rootPath: '/home/operator/.config/Maka/workspaces/default',
- deploymentId: '00000000-0000-4000-8000-000000000001',
- },
- });
- assert.deepEqual(peerTargets, ['none']);
- await harness.onboarding.close();
-});
-
test('projects WSL setup failures as recoverable onboarding state', async () => {
const harness = createHarness({
resolveWslTargetIdentity: async () => {
@@ -268,24 +139,8 @@ test('finishes Host pairing after the cancellable SSH phase has completed', asyn
});
let pairingStarted = false;
let completeReceived = false;
- let finishSetup!: (value: {
- serviceId: string;
- deploymentId: string;
- rootPath: string;
- operator: typeof OPERATOR;
- rootId: string;
- endpoint: string;
- credential: string;
- }) => void;
- const setupDrain = new Promise<{
- serviceId: string;
- deploymentId: string;
- rootPath: string;
- operator: typeof OPERATOR;
- rootId: string;
- endpoint: string;
- credential: string;
- }>((resolve) => {
+ let finishSetup!: (value: NativeSetupResult) => void;
+ const setupDrain = new Promise<NativeSetupResult>((resolve) => {
finishSetup = resolve;
});
const harness = createHarness({
@@ -295,10 +150,12 @@ test('finishes Host pairing after the cancellable SSH phase has completed', asyn
return pairing;
},
},
- runSetup: async (_input, _onProgress, onComplete) => {
- onComplete();
- completeReceived = true;
- return setupDrain;
+ nativeSetup: {
+ ssh: async (_input, onCommit) => {
+ onCommit();
+ completeReceived = true;
+ return setupDrain;
+ },
},
});
@@ -309,15 +166,7 @@ test('finishes Host pairing after the cancellable SSH phase has completed', asyn
while (!completeReceived) await Promise.resolve();
assert.equal(await harness.invoke('runtime-host-onboarding:cancel'), false);
- finishSetup({
- serviceId: 'b'.repeat(64),
- deploymentId: '00000000-0000-4000-8000-000000000001',
- rootPath: '/home/operator/.config/Maka/workspaces/default',
- operator: OPERATOR,
- rootId: 'a'.repeat(64),
- endpoint: 'ws://127.0.0.1:7443/runtime-host',
- credential: 'candidate-token',
- });
+ finishSetup(installed);
while (!pairingStarted) await Promise.resolve();
finishPairing({ profileId: 'office' });
@@ -328,9 +177,11 @@ test('finishes Host pairing after the cancellable SSH phase has completed', asyn
test('resolves the setup package only when onboarding starts', async () => {
let resolutions = 0;
const harness = createHarness({
- resolveSetupPackage: () => {
- resolutions += 1;
- throw new Error('Desktop does not declare an exact Runtime Host setup package');
+ nativeSetup: {
+ resolvePackage: async () => {
+ resolutions += 1;
+ throw new Error('Desktop does not declare an exact Runtime Host setup package');
+ },
},
});
@@ -355,14 +206,15 @@ test('resolves the setup package only when onboarding starts', async () => {
});
type OnboardingInput = Parameters<typeof createDesktopRuntimeHostOnboarding>[0];
-type HarnessOverrides = Partial<Omit<OnboardingInput, 'ipcMain' | 'send' | 'profiles'>> & {
+type HarnessOverrides = Partial<Omit<OnboardingInput, 'ipcMain' | 'send' | 'profiles' | 'nativeSetup'>> & {
readonly profiles?: Partial<OnboardingInput['profiles']>;
+ readonly nativeSetup?: Partial<OnboardingInput['nativeSetup']>;
};
function createHarness(overrides: HarnessOverrides = {}) {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const events: unknown[] = [];
- const { profiles, ...rest } = overrides;
+ const { profiles, nativeSetup, ...rest } = overrides;
const onboarding = createDesktopRuntimeHostOnboarding({
clientInstanceId: 'stable-client',
profiles: {
@@ -370,12 +222,18 @@ function createHarness(overrides: HarnessOverrides = {}) {
addAndEnableVerified: async () => assert.fail('profile must not be saved'),
...profiles,
},
- setupPackageMode: 'published',
resolveSshTargetIdentity: async () => ({ platform: 'darwin', architecture: 'x64' }),
resolveWslTargetIdentity: async () => ({ platform: 'linux', architecture: 'x64', glibcVersion: '2.39' }),
- resolveSetupPackage: () => ({ kind: 'npm', specifier: 'maka-agent@0.2.0' }),
- runSetup: async () => assert.fail('SSH must not start'),
- runWslSetup: async () => assert.fail('WSL must not start'),
+ nativeSetup: {
+ resolvePackage: async () => ({
+ artifact: { target: 'darwin-x64', version: '0.2.0', directory: '/verified/package',
+ executable: '/verified/package/bin/maka', integrity: 'sha512-' + 'A'.repeat(86) + '==' },
+ receiptSha256: 'c'.repeat(64),
+ }),
+ ssh: async () => assert.fail('SSH must not start'),
+ wsl: async () => assert.fail('WSL must not start'),
+ ...nativeSetup,
+ },
listWslDistributions: async () => [],
...rest,
ipcMain: {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-profile-service.test.ts b/apps/desktop/src/main/__tests__/runtime-host-profile-service.test.ts
index e4ea152485..30b3791849 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-profile-service.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-profile-service.test.ts
@@ -18,7 +18,7 @@
*/
import assert from "node:assert/strict";
-import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, test } from "node:test";
@@ -26,7 +26,6 @@ import { deferred } from "@maka/core/test-only/async-primitives";
import {
createClientRuntimeHostCredentialStore,
createClientRuntimeHostProfileCatalog,
- createRuntimeHostProfileCredentialStore,
encodeRuntimeHostOwnerConnectionCode,
LOCAL_RUNTIME_HOST_PROFILE,
RuntimeHostPermanentReconnectError,
@@ -110,49 +109,7 @@ afterEach(async () => {
);
});
-for (const hasDeployment of [false, true]) {
- test(`recovers an abandoned deployment lock before loading Host choices (saved=${hasDeployment})`, async () => {
- const root = await clientRoot();
- const catalog = createClientRuntimeHostProfileCatalog(root);
- const managedServices = createDesktopRuntimeHostManagedServiceStore(root);
- if (hasDeployment) {
- await catalog.create(MANAGED_PROFILE, "token");
- await managedServices.save(MANAGED_PROFILE, MANAGED_SERVICE);
- }
- const before = await managedServices.read();
- await mkdir(join(root, "runtime-host-deployments.json.lock"));
-
- const startup = await resolveDesktopRuntimeHostStartup(root, { catalog });
- const service = createDesktopRuntimeHostProfileService({
- clientDataRoot: root,
- startup,
- catalog,
- managedServices,
- states: () => [ready({ profile: LOCAL_RUNTIME_HOST_PROFILE })],
- enable: async () => undefined,
- disable: async () => undefined,
- setDefault: () => undefined,
- finalizePairing: async () => undefined,
- });
- const snapshot = await service.getSnapshot();
- assert.equal(snapshot.defaultProfileId, LOCAL_RUNTIME_HOST_PROFILE.id);
- assert.equal(snapshot.entries[0]?.readiness, "ready");
- assert.equal(snapshot.entries.length, hasDeployment ? 2 : 1);
- assert.deepEqual(await managedServices.read(), before);
- if (hasDeployment) assert.equal(snapshot.entries[1]?.managedService, true);
- });
-}
-
-test("does not discard unexpected contents in an abandoned deployment lock", async () => {
- const root = await clientRoot();
- const lock = join(root, "runtime-host-deployments.json.lock");
- await mkdir(lock);
- await writeFile(join(lock, "unexpected"), "retain me");
- await assert.rejects(resolveDesktopRuntimeHostStartup(root), { code: "ENOTEMPTY" });
- assert.equal(await readFile(join(lock, "unexpected"), "utf8"), "retain me");
-});
-
-test("migrates the former selected Host into enabled and default preferences", async () => {
+test("preserves unsupported preferences and reports their read failure", async () => {
const root = await clientRoot();
await createClientRuntimeHostProfileCatalog(root).create(PROFILE, "token");
await writeFile(
@@ -162,56 +119,9 @@ test("migrates the former selected Host into enabled and default preferences", a
const startup = await resolveDesktopRuntimeHostStartup(root);
- assert.equal(startup.preferences.defaultProfileId, PROFILE.id);
- assert.deepEqual(startup.preferences.enabledRemoteProfileIds, [PROFILE.id]);
- assert.equal(startup.remotes[0]?.profile.id, PROFILE.id);
- assert.equal(
- JSON.parse(await readFile(join(root, "runtime-host-profile-selection.json"), "utf8"))
- .schemaVersion,
- 2,
- );
-});
-
-test('removes obsolete experimental Guest profiles and pairing intents at startup', async () => {
- const root = await clientRoot();
- const credentials = createClientRuntimeHostCredentialStore(root);
- const catalog = createClientRuntimeHostProfileCatalog(root, credentials);
- const guest = { ...PROFILE, id: 'shared-obsolete', access: 'session_guest' as const };
- await createRuntimeHostProfileCredentialStore(credentials).set(guest, {
- credential: 'guest-token',
- profileIncarnationId: 'guest-incarnation',
- });
- await writeFile(
- join(root, 'runtime-host-profiles.json'),
- `${JSON.stringify({ schemaVersion: 3, profiles: [guest] })}\n`,
- );
- await writeFile(
- join(root, 'runtime-host-profile-selection.json'),
- `${JSON.stringify({
- schemaVersion: 2,
- defaultProfileId: guest.id,
- enabledRemoteProfileIds: [guest.id],
- })}\n`,
- );
- await writeDesktopRuntimeHostPairingIntents(credentials, [
- createDesktopRuntimeHostPairingIntent({
- target: { profile: guest, credential: 'guest-token' },
- wasEnabled: true,
- }),
- ]);
-
- const startup = await resolveDesktopRuntimeHostStartup(root, {
- catalog,
- credentialStore: credentials,
- });
-
- assert.deepEqual((await catalog.read()).profiles, []);
- assert.deepEqual(startup.pairingIntents, []);
- assert.deepEqual(startup.preferences, {
- schemaVersion: 2,
- defaultProfileId: 'local',
- enabledRemoteProfileIds: [],
- });
+ assert.ok(startup.preferencesReadFailure);
+ assert.equal(startup.preferences.defaultProfileId, 'local');
+ assert.equal(JSON.parse(await readFile(join(root, 'runtime-host-profile-selection.json'), 'utf8')).schemaVersion, 1);
});
test("starts Local and preserves remote preferences when the profile catalog is unreadable", async () => {
@@ -343,7 +253,8 @@ test("reuses the existing WSL profile when the same managed Host is added again"
provider: { kind: "wsl" as const, distribution: "Ubuntu-24.04" },
rootId: ROOT_ID,
operator: {
- kind: "legacy_posix_executable" as const,
+ kind: "native" as const,
+ platform: "posix" as const,
executablePath: "/home/operator/.local/share/Maka/runtime-host-services/operator",
},
};
@@ -1124,6 +1035,7 @@ test("does not rotate a managed credential after its profile target changes", as
deployment: {
id: "e".repeat(64),
rootPath: "/srv/other-maka",
+ deploymentId: "22222222-2222-4222-8222-222222222222",
},
};
await catalog.remove(MANAGED_PROFILE.id);
@@ -1357,8 +1269,6 @@ test('discarding a committed rotation unlocks the restored local profile', async
);
restoringOldCredential = true;
- const abandonedLock = join(root, 'runtime-host-profiles.json.lock');
- await mkdir(abandonedLock);
const discarded = await service.discardPairing(MANAGED_PROFILE.id);
assert.equal(discarded.pairingRecoveryPending, undefined);
assert.equal((await catalog.resolve(MANAGED_PROFILE.id)).credential, 'old-token');
@@ -1575,7 +1485,7 @@ test("keeps existing Hosts available while corrupt pairing recovery awaits resol
assert.equal((await service.setEnabled(PROFILE.id, false)).entries[1]?.enabled, false);
});
-test("migrates an interrupted SSH pairing from the released operator path", async () => {
+test("rejects an interrupted pairing with an obsolete operator path", async () => {
const root = await clientRoot();
const credentialStore = createClientRuntimeHostCredentialStore(root);
await credentialStore.setSecret(
@@ -1607,18 +1517,8 @@ test("migrates an interrupted SSH pairing from the released operator path", asyn
const startup = await resolveDesktopRuntimeHostStartup(root, { credentialStore });
- assert.equal(startup.pairingReadFailure, undefined);
- assert.deepEqual(startup.pairingIntents[0]?.target.profile.transport, {
- kind: "ssh",
- destination: "operator@example.com",
- activation: {
- kind: "ssh_operator",
- operator: {
- kind: "legacy_posix_executable",
- executablePath: "/home/operator/.local/share/maka/operator",
- },
- },
- });
+ assert.ok(startup.pairingReadFailure);
+ assert.deepEqual(startup.pairingIntents, []);
});
test("retries pairing recovery before discarding it", async () => {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts
index 1e5ac98107..57aec57911 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-session-execution-ipc-main.test.ts
@@ -783,7 +783,7 @@ test("sends canonical content and uploads owned Attachment bytes through the Hos
return {
disposition: "turn_started",
turnId: "turn-1",
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
},
});
@@ -848,7 +848,7 @@ test("sends canonical content and uploads owned Attachment bytes through the Hos
label: "notes.txt",
},
],
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
});
assert.deepEqual(changes, [
{ reason: "status-change", sessionId: "session-1", turnId: "turn-1" },
@@ -892,7 +892,7 @@ test("uploads a selected workspace file as a Host-owned Session Artifact", async
return {
disposition: "turn_started",
turnId: "turn-1",
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
},
}),
@@ -936,11 +936,11 @@ test("forwards explicit Skill invocation to the Host-owned Turn admission", asyn
return {
disposition: "turn_started",
turnId: "turn-skill",
- skillInvocation: {
+ preparation: fixturePreparation({
loaded: [{ id: "review", name: "Review" }],
failed: [],
receipts: [],
- },
+ }),
};
},
}),
@@ -959,7 +959,7 @@ test("forwards explicit Skill invocation to the Host-owned Turn admission", asyn
type: "send",
text: "",
displayText: "/skill:review",
- skillIds: ["review"],
+ inputSelections: { "maka.skills": ["review"] },
});
assert.deepEqual(starts, [
@@ -968,7 +968,7 @@ test("forwards explicit Skill invocation to the Host-owned Turn admission", asyn
messageId: "turn-skill",
placement: "current_turn",
content: { text: "", displayText: "/skill:review", inlineReferences: [] },
- skillIds: ["review"],
+ inputSelections: { "maka.skills": ["review"] },
},
]);
assert.deepEqual(result, {
@@ -976,11 +976,11 @@ test("forwards explicit Skill invocation to the Host-owned Turn admission", asyn
turnId: "turn-skill",
attachments: [],
inlineReferences: [],
- skillInvocation: {
+ preparation: fixturePreparation({
loaded: [{ id: "review", name: "Review" }],
failed: [],
receipts: [],
- },
+ }),
});
});
@@ -996,7 +996,7 @@ test("submits an ordinary composer message once under its stable message identit
return {
disposition: "turn_started",
turnId: "host-turn",
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
},
}),
@@ -1033,7 +1033,7 @@ test("submits an ordinary composer message once under its stable message identit
turnId: "host-turn",
attachments: [],
inlineReferences: [],
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
});
});
@@ -1329,11 +1329,12 @@ test('submits a slash Skill message and reports the Host Skill outcome', async (
submits.push(input);
return {
disposition: 'blocked',
- skillInvocation: {
+ message: 'Input unavailable',
+ preparation: fixturePreparation({
loaded: [],
failed: [{ request: 'missing', reason: 'not_found' }],
receipts: [],
- },
+ }),
};
},
}),
@@ -1355,12 +1356,13 @@ test('submits a slash Skill message and reports the Host Skill outcome', async (
}]);
assert.deepEqual(result, {
ok: false,
- reason: 'skill_invocation_failed',
- skillInvocation: {
+ reason: 'input_preparation_failed',
+ message: 'Input unavailable',
+ preparation: fixturePreparation({
loaded: [],
failed: [{ request: 'missing', reason: 'not_found' }],
receipts: [],
- },
+ }),
});
});
@@ -1379,7 +1381,7 @@ test("queues a mid-turn send as steering when the Host reports the session busy"
getSession: async () => session(),
submitMessage: async (input) => {
submits.push(input);
- return { disposition: "steering", queueRevision: 1, skillInvocation };
+ return { disposition: "steering", queueRevision: 1, preparation: fixturePreparation(skillInvocation) };
},
}),
observer: unusedObserver(),
@@ -1414,7 +1416,7 @@ test("queues a mid-turn send as steering when the Host reports the session busy"
turnId: "turn-1",
attachments: [],
inlineReferences: [],
- skillInvocation,
+ preparation: fixturePreparation(skillInvocation),
});
assert.deepEqual(changes, [
{ reason: "status-change", sessionId: "session-1" },
@@ -1458,7 +1460,7 @@ test("resolves a twice-interrupted send as an unknown outcome", async () => {
ok: false,
reason: "outcome_unknown",
messageId: "turn-1",
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
);
assert.equal(submits, 2);
@@ -1501,7 +1503,7 @@ test("retries a dispatched send with its original message identity", async () =>
return {
disposition: "steering",
queueRevision: 1,
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
},
}),
@@ -1538,7 +1540,7 @@ test("retries a dispatched send with its original message identity", async () =>
messageId: "turn-1",
attachments: [],
inlineReferences: [],
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
});
assert.deepEqual(
await ipc.invoke("sessions:send", "session-1", {
@@ -1550,7 +1552,7 @@ test("retries a dispatched send with its original message identity", async () =>
ok: false,
reason: "outcome_unknown",
messageId: "turn-unknown",
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
);
assert.deepEqual(
@@ -1563,7 +1565,7 @@ test("retries a dispatched send with its original message identity", async () =>
ok: false,
reason: "outcome_unknown",
messageId: "turn-unknown",
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
);
});
@@ -1581,7 +1583,7 @@ test("answers a send with the Turn the Host started for it", async () => {
return {
disposition: "turn_started",
turnId: "turn-9",
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
},
}),
@@ -1608,7 +1610,7 @@ test("answers a send with the Turn the Host started for it", async () => {
turnId: "turn-9",
attachments: [],
inlineReferences: [],
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
});
assert.deepEqual(submits, [{
sessionId: "session-1",
@@ -1650,7 +1652,7 @@ test("propagates a busy explicit Skill send instead of degrading it to steering"
turnId: "turn-1",
text: "",
displayText: "/skill:review",
- skillIds: ["review"],
+ inputSelections: { "maka.skills": ["review"] },
}),
(error: unknown) =>
error instanceof RuntimeHostOperationError && error.code === "session_busy",
@@ -1665,7 +1667,7 @@ test("propagates a busy explicit Skill send instead of degrading it to steering"
displayText: "/skill:review",
inlineReferences: [],
},
- skillIds: ["review"],
+ inputSelections: { "maka.skills": ["review"] },
},
]);
});
@@ -1682,7 +1684,7 @@ test("lets the Host queue a textual Skill token as steering", async () => {
return {
disposition: "steering",
queueRevision: 1,
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
},
}),
@@ -1707,7 +1709,7 @@ test("lets the Host queue a textual Skill token as steering", async () => {
turnId: "turn-1",
attachments: [],
inlineReferences: [],
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
);
assert.equal(submits.length, 1);
@@ -1721,11 +1723,12 @@ test("reports a Host-blocked Skill send as a Skill failure", async () => {
getSession: async () => session(),
submitMessage: async () => ({
disposition: "blocked",
- skillInvocation: {
+ message: 'Input unavailable',
+ preparation: fixturePreparation({
loaded: [],
failed: [{ request: "missing", reason: "not_found" }],
receipts: [],
- },
+ }),
}),
}),
newId: () => "id-1",
@@ -1741,12 +1744,13 @@ test("reports a Host-blocked Skill send as a Skill failure", async () => {
}),
{
ok: false,
- reason: "skill_invocation_failed",
- skillInvocation: {
+ reason: "input_preparation_failed",
+ message: 'Input unavailable',
+ preparation: fixturePreparation({
loaded: [],
failed: [{ request: "missing", reason: "not_found" }],
receipts: [],
- },
+ }),
},
);
});
@@ -1766,7 +1770,7 @@ test("queues explicit Desktop follow-ups", async () => {
getSession: async () => session(),
submitMessage: async (input) => {
submits.push(input);
- return { disposition: "followup", queueRevision: 4, skillInvocation };
+ return { disposition: "followup", queueRevision: 4, preparation: fixturePreparation(skillInvocation) };
},
}),
observer: unusedObserver(),
@@ -1816,7 +1820,7 @@ test("queues explicit Desktop follow-ups", async () => {
},
],
inlineReferences: [],
- skillInvocation,
+ preparation: fixturePreparation(skillInvocation),
},
);
assert.deepEqual(submits, [
@@ -1988,7 +1992,7 @@ test("binds steer and stop to Host-owned queue and active Turn identities", asyn
return {
disposition: "steering",
queueRevision: 2,
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
},
interruptTurn: async (input) => {
@@ -2072,7 +2076,7 @@ test("binds steer and stop to Host-owned queue and active Turn identities", asyn
disposition: "steering",
attachments: [],
inlineReferences: [],
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
);
assert.deepEqual(
@@ -2448,7 +2452,7 @@ test('steers WorkHub through Host admission even though the ordinary Session cat
getSession: async () => null,
submitMessage: async (input) => {
submits.push(input);
- return { disposition: 'steering', queueRevision: 1, skillInvocation: { loaded: [], failed: [], receipts: [] } };
+ return { disposition: 'steering', queueRevision: 1, preparation: [] };
},
}),
observer: unusedObserver(), attachmentApprovals: createAttachmentApprovalRegistry(),
@@ -2460,3 +2464,7 @@ test('steers WorkHub through Host admission even though the ordinary Session cat
assert.deepEqual(submits, [{ sessionId: WORKHUB_COORDINATION_SESSION_ID, messageId: 'workhub-steering', placement: 'current_turn', content: { text: 'Change direction immediately', inlineReferences: [] } }]);
assert.equal((result as { disposition: string }).disposition, 'steering');
});
+
+function fixturePreparation(receipt: unknown): import('@maka/runtime-host/protocol').InputReceipt[] {
+ return [{ source: { kind: 'input', name: 'maka.skills', packageId: 'maka.skills', entryId: 'skills', activation: "fixture", revision: "1" }, receipt: JSON.parse(JSON.stringify(receipt)) }];
+}
diff --git a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts
index 482fb51d86..8914f86bc1 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts
@@ -18,10 +18,6 @@
*/
import assert from 'node:assert/strict';
-import { createHash } from 'node:crypto';
-import { mkdtemp, rm, writeFile } from 'node:fs/promises';
-import { tmpdir } from 'node:os';
-import { join } from 'node:path';
import { test } from 'node:test';
import type { IPty } from 'node-pty';
import { type RuntimeHostSshProcessFactory } from '@maka/runtime-host/client';
@@ -30,16 +26,15 @@ import {
encodeRuntimeHostAccessManagementFrame,
encodeRuntimeHostPeerManagementFrame,
encodeRuntimeHostServiceManagementFrame,
- encodeRuntimeHostSetupFrame,
encodeRuntimeHostPeerMeshManagementFrame,
runtimeHostAccessCredentialFingerprint,
RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY,
- RUNTIME_HOST_SETUP_FRAME_PREFIX,
} from '@maka/runtime-host/operator';
import {
createDesktopRuntimeHostSshTerminal,
runtimeHostPeerTargetFromPlatform,
} from '../runtime-host-ssh-terminal.js';
+import { NATIVE_ARTIFACT_PREFIX, NATIVE_SETUP_PREFIX } from '../native-runtime-host-setup.js';
import { waitFor as pollFor } from '@maka/core/test-only/async-primitives';
const OPERATOR = {
@@ -196,215 +191,87 @@ test('does not reopen a cancelled SSH prompt for late process output', async ()
await harness.terminal.close();
});
-test('keeps setup credentials out of the interactive terminal projection', async () => {
- const harness = createHarness('pending');
- const controller = new AbortController();
- const progress: string[] = [];
- const setup = harness.terminal.runSetup(
- {
- destination: 'operator@example.com',
- setupPackage: { kind: 'npm', specifier: 'maka-agent@1.2.3+desktop.1' },
- remotePlatform: 'posix',
- principalId: 'desktop:stable-client',
- signal: controller.signal,
- },
- (frame) => progress.push(frame.phase),
- );
- await waitFor(() => harness.pty.hasDataListener());
- harness.pty.emitData('Password: ');
- const progressFrame = encodeRuntimeHostSetupFrame({
- schemaVersion: 1,
- sequence: 0,
- kind: 'progress',
- phase: 'installing_service',
- });
- harness.pty.emitData(progressFrame.slice(0, 12));
- harness.pty.emitData(progressFrame.slice(12));
- const completeFrame = encodeRuntimeHostSetupFrame({
- schemaVersion: 1,
- sequence: 1,
- kind: 'complete',
- version: '0.1.0-beta.1',
- serviceId: 'b'.repeat(64),
- deploymentId: '00000000-0000-4000-8000-000000000001',
- operator: OPERATOR,
- rootPath: '/home/operator/.config/Maka/workspaces/default',
- rootId: 'a'.repeat(64),
- endpoint: 'ws://127.0.0.1:7443/runtime-host',
- credentialId: 'credential-1',
- credential: 'secret-access-token',
- });
- harness.pty.emitData(completeFrame);
- controller.abort();
- await Promise.resolve();
- assert.deepEqual(harness.pty.killSignals, []);
- harness.pty.exit(0);
-
- const result = await setup;
- assert.equal(result.credential, 'secret-access-token');
- assert.deepEqual(progress, ['installing_service']);
- assert.doesNotMatch(JSON.stringify(harness.events), /secret-access-token|MAKA_RUNTIME/u);
- assert.match(JSON.stringify(harness.events), /Password/u);
- const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
- assert.match(remoteCommand, /mktemp -d/u);
- assert.match(remoteCommand, /--prefix/u);
- assert.match(remoteCommand, /trap.*HUP.*trap.*INT.*trap.*TERM/u);
- assert.doesNotMatch(remoteCommand, /--update-existing/u);
- await harness.terminal.close();
-});
-
-test('runs Windows setup through one encoded PowerShell command', async () => {
- const harness = createHarness('pending');
- const setup = harness.terminal.runSetup(
- {
- destination: 'operator@example.com',
- setupPackage: { kind: 'npm', specifier: 'maka-agent@1.2.3' },
- remotePlatform: 'win32',
- principalId: 'desktop:stable-client',
- lifecycle: 'on_demand',
- },
- () => undefined,
- );
- await waitFor(() => harness.pty.hasDataListener());
- const remoteCommand = harness.launchArgs.at(-1)?.at(-1) ?? '';
- assert.match(remoteCommand, /^powershell\.exe .* -EncodedCommand [A-Za-z0-9+/=]+$/u);
- assert.doesNotMatch(remoteCommand, /\/bin\/sh|maka-agent/u);
- const encoded = remoteCommand.split(' ').at(-1);
- assert.ok(encoded);
- const script = Buffer.from(encoded, 'base64').toString('utf16le');
- assert.match(script, /npx\.cmd/u);
- assert.match(script, /Remove-Item/u);
-
- harness.pty.emitData(encodeRuntimeHostSetupFrame({
- schemaVersion: 1,
- sequence: 0,
- kind: 'complete',
- version: '1.2.3',
- serviceId: 'b'.repeat(64),
- deploymentId: '00000000-0000-4000-8000-000000000001',
- operator: WINDOWS_OPERATOR,
- rootPath: 'C:\\Users\\operator\\AppData\\Local\\Maka\\workspaces\\default',
- rootId: 'a'.repeat(64),
- endpoint: 'ws://127.0.0.1:7443/runtime-host',
- credentialId: 'credential-1',
- credential: 'secret-access-token',
- }));
- harness.pty.exit(0);
- assert.deepEqual((await setup).operator, WINDOWS_OPERATOR);
- await harness.terminal.close();
-});
-
-test('discards an oversized reserved setup line instead of projecting its tail', async () => {
- const harness = createHarness('pending');
- const setup = harness.terminal.runSetup(
- {
- destination: 'operator@example.com',
- setupPackage: { kind: 'npm', specifier: 'maka-agent@1.2.3' },
- remotePlatform: 'posix',
- principalId: 'desktop:stable-client',
- },
- () => undefined,
- );
- await waitFor(() => harness.pty.hasDataListener());
- harness.pty.emitData(`${RUNTIME_HOST_SETUP_FRAME_PREFIX}${'x'.repeat(21 * 1024)}`);
- harness.pty.emitData('"credential":"must-not-reach-renderer"}\nvisible output\n');
- harness.pty.exit(1);
-
- await assert.rejects(setup, /oversized result/u);
- assert.doesNotMatch(
- JSON.stringify([harness.events, await harness.getSnapshot()]),
- /must-not-reach-renderer/u,
- );
- assert.match(JSON.stringify(harness.events), /visible output/u);
- await harness.terminal.close();
-});
-
-test('keeps a completed setup process owned until it exits', async () => {
- const harness = createHarness('pending');
- const setup = harness.terminal.runSetup(
- {
- destination: 'operator@example.com',
- setupPackage: { kind: 'npm', specifier: 'maka-agent@1.2.3' },
- remotePlatform: 'posix',
- principalId: 'desktop:stable-client',
- },
- () => undefined,
- );
- await waitFor(() => harness.pty.hasDataListener());
- harness.pty.emitData(encodeRuntimeHostSetupFrame({
- schemaVersion: 1,
- sequence: 0,
- kind: 'complete',
- version: '1.2.3',
- serviceId: 'b'.repeat(64),
- deploymentId: '00000000-0000-4000-8000-000000000001',
- operator: OPERATOR,
- rootPath: '/home/operator/.config/Maka/workspaces/default',
- rootId: 'a'.repeat(64),
- endpoint: 'ws://127.0.0.1:7443/runtime-host',
- credentialId: 'credential-1',
- credential: 'secret-access-token',
- }));
-
- await harness.terminal.close();
-
- assert.deepEqual(harness.pty.killSignals, ['SIGTERM']);
- assert.equal((await setup).credentialId, 'credential-1');
-});
-
-test('force-stops a cancelled setup when SSH ignores graceful termination', async () => {
- const harness = createHarness('pending');
- harness.pty.deferKill = true;
- harness.pty.exitOnForceKill = true;
- const controller = new AbortController();
- const setup = harness.terminal.runSetup(
- {
- destination: 'operator@example.com',
- setupPackage: { kind: 'npm', specifier: 'maka-agent@1.2.3' },
- remotePlatform: 'posix',
- principalId: 'desktop:stable-client',
- signal: controller.signal,
- },
- () => undefined,
- );
- await waitFor(() => harness.pty.hasDataListener());
- harness.pty.emitData('Password: ');
-
- controller.abort();
-
- await assert.rejects(setup, /aborted/u);
- assert.deepEqual(harness.pty.killSignals, ['SIGTERM', 'SIGKILL']);
- assert.deepEqual(harness.terminatedProcesses, [
- { pid: 42, signal: 'SIGTERM' },
- { pid: 42, signal: 'SIGKILL' },
- ]);
- assert.deepEqual(harness.eventKinds(), ['opened', 'data', 'dismissed']);
- assert.deepEqual(await harness.getSnapshot(), { kind: 'idle', revision: 3 });
- await harness.terminal.close();
-});
-
-test('does not signal a reused process identity when cancellation races SSH exit', async () => {
- const harness = createHarness('pending');
- const controller = new AbortController();
- const setup = harness.terminal.runSetup(
- {
- destination: 'operator@example.com',
- setupPackage: { kind: 'npm', specifier: 'maka-agent@1.2.3' },
- remotePlatform: 'posix',
- principalId: 'desktop:stable-client',
- signal: controller.signal,
- },
- () => undefined,
- );
- await waitFor(() => harness.pty.hasDataListener());
-
- controller.abort();
- harness.pty.exit(0);
-
- await assert.rejects(setup, /aborted/u);
- await Promise.resolve();
- assert.deepEqual(harness.pty.killSignals, []);
- await harness.terminal.close();
+test('native SSH setup verifies the target package and keeps its receipt out of terminal output', async () => {
+ for (const windows of [false, true]) {
+ const directory = windows ? 'C:\\Maka\\package' : '/opt/maka/package';
+ const executable = windows ? `${directory}\\bin\\maka.exe` : `${directory}/bin/maka`;
+ const artifact = {
+ target: windows ? 'win32-x64' as const : 'linux-x64-gnu' as const,
+ version: '0.2.0', directory, executable,
+ ...(windows ? { serviceExecutable: `${directory}\\bin\\maka-service.exe` } : {}),
+ integrity: `sha512-${'A'.repeat(86)}==`,
+ };
+ const receipt = {
+ deployment: {
+ deploymentId: '00000000-0000-4000-8000-000000000001', configRevision: 1,
+ rootId: 'a'.repeat(64), rootPath: windows ? 'C:\\Maka\\state' : '/opt/maka/state',
+ executable, sha256: 'b'.repeat(64), mode: 'on_demand', websocket: '127.0.0.1:0',
+ },
+ host: { hostEpoch: 'host-epoch', pid: 123, port: 4567 },
+ pairing: { rootId: 'a'.repeat(64), credentialId: 'credential', credential: 'setup-private-token' },
+ };
+ const events: unknown[] = [];
+ const commands: string[] = [];
+ let committed = false;
+ let launch = 0;
+ const terminal = createDesktopRuntimeHostSshTerminal({
+ ipcMain: { handle: () => undefined, removeHandler: () => undefined },
+ send: (_channel, event) => events.push(event),
+ revealDelayMs: 0,
+ spawnPty: ((file: string, args: string[]) => {
+ const pty = new FakePty();
+ const step = launch++;
+ const encoded = args.at(-1) ?? '';
+ const script = windows && file === 'ssh'
+ ? Buffer.from(encoded.split(' ').at(-1)!, 'base64').toString('utf16le')
+ : encoded;
+ const payload = windows ? script.match(/FromBase64String\('([^']+)'\)/u)?.[1] : undefined;
+ const command = payload ? Buffer.from(payload, 'base64').toString('utf8') : script;
+ commands.push(command);
+ setImmediate(() => {
+ let frame: string;
+ if (step === 0) {
+ const name = command.match(/maka-native-[a-f0-9]{32}/u)?.[0];
+ assert.ok(name);
+ frame = `__MAKA_NATIVE_HOST_STAGE__${windows ? 'C:\\Temp\\' : '/tmp/'}${name}\n`;
+ } else if (step === 1) {
+ assert.equal(file, 'scp');
+ pty.exit(0);
+ return;
+ } else if (step === 2) {
+ assert.match(command, /fetch/u);
+ assert.ok(command.includes('c'.repeat(64)));
+ frame = `${NATIVE_ARTIFACT_PREFIX}${JSON.stringify(artifact)}\n`;
+ } else if (step === 3) {
+ frame = '__MAKA_NATIVE_HOST_CLEAN__ok\n';
+ } else {
+ assert.equal(step, 4);
+ assert.equal(committed, true);
+ assert.match(command, /desktop:client/u);
+ assert.doesNotMatch(command, /allow-interrupt|update-existing/u);
+ frame = `${NATIVE_SETUP_PREFIX}${JSON.stringify(receipt)}\n`;
+ }
+ // PTY chunks need not coincide with reserved frame boundaries.
+ pty.emitData(frame.slice(0, 11));
+ pty.emitData(frame.slice(11));
+ pty.exit(0);
+ });
+ return pty as unknown as IPty;
+ }) as typeof import('node-pty').spawn,
+ });
+ try {
+ const result = await terminal.runNativeSetup({
+ destination: 'operator@example.com', principalId: 'desktop:client',
+ package: { artifact, receiptSha256: 'c'.repeat(64) },
+ }, () => { committed = true; });
+ assert.deepEqual(result.receipt, receipt);
+ assert.equal(result.operator.executablePath, executable);
+ assert.equal(commands.length, 5);
+ assert.doesNotMatch(JSON.stringify(events), /setup-private-token|__MAKA_NATIVE_HOST_/u);
+ } finally {
+ await terminal.close();
+ }
+ }
});
test('reads a framed service result without projecting it into the SSH terminal', async () => {
@@ -1033,63 +900,6 @@ test('runs interactive operator activation as one strict framed SSH command', as
await harness.terminal.close();
});
-test('uploads a development release archive before running the same remote setup', async (t) => {
- const directory = await mkdtemp(join(tmpdir(), 'maka-runtime-host-development-package-'));
- t.after(() => rm(directory, { recursive: true, force: true }));
- const archive = join(directory, 'maka-agent-development.tgz');
- await writeFile(archive, 'development package');
- const integrity = `sha512-${createHash('sha512').update('development package').digest('base64')}`;
- const handlers = new Map<string, (...args: unknown[]) => unknown>();
- const launches: Array<{ file: string; args: string[]; pty: FakePty }> = [];
- const terminal = createDesktopRuntimeHostSshTerminal({
- ipcMain: {
- handle: (channel, handler) => handlers.set(channel, handler as (...args: unknown[]) => unknown),
- removeHandler: (channel) => handlers.delete(channel),
- },
- send: () => undefined,
- spawnPty: ((file: string, args: string[]) => {
- const pty = new FakePty();
- launches.push({ file, args, pty });
- return pty as unknown as IPty;
- }) as typeof import('node-pty').spawn,
- });
- t.after(() => terminal.close());
-
- const setupInput = {
- destination: 'operator@example.com',
- setupPackage: {
- kind: 'development_archive',
- path: archive,
- integrity,
- } as const,
- remotePlatform: 'posix' as const,
- principalId: 'desktop:stable-client',
- };
- const setup = terminal.runSetup(setupInput, () => undefined);
- await waitFor(() => launches.length === 1);
- assert.equal(launches[0]?.file, 'scp');
- assert.match(launches[0]?.args.at(-2) ?? '', /maka-agent-development\.tgz$/u);
- assert.match(
- launches[0]?.args.at(-1) ?? '',
- /^operator@example\.com:\.\/\.maka-runtime-host-setup-.+\.tgz$/u,
- );
- launches[0]?.pty.exit(0);
-
- await waitFor(() => launches.length === 2);
- assert.equal(launches[1]?.file, 'ssh');
- const remoteCommand = launches[1]?.args.at(-1) ?? '';
- assert.match(remoteCommand, /--package.*maka-runtime-host-setup-.+\.tgz/u);
- assert.match(remoteCommand, /MAKA_RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY=/u);
- assert.ok(remoteCommand.includes(integrity));
- assert.match(remoteCommand, /--defer-pairing-commit/u);
- assert.match(remoteCommand, /--update-existing/u);
- assert.match(remoteCommand, /cd.*\$HOME/u);
- assert.match(remoteCommand, /rm -f/u);
- assert.match(remoteCommand, /exec \/bin\/sh -c/u);
- launches[1]?.pty.exit(255);
- await assert.rejects(setup, /exited with code 255/u);
-});
-
function createHarness(
mode: 'pending' | 'exit',
options: { readonly managementTimeoutMs?: number } = {},
diff --git a/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts b/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts
index c73bf7344c..f0df9dd8f6 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts
@@ -19,21 +19,18 @@
import assert from 'node:assert/strict';
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
-import { createHash } from 'node:crypto';
import { EventEmitter } from 'node:events';
import { PassThrough } from 'node:stream';
import test from 'node:test';
import type { RuntimeHostWslProcessFactory } from '@maka/runtime-host/client';
import {
encodeRuntimeHostServiceManagementFrame,
- encodeRuntimeHostSetupFrame,
RUNTIME_HOST_OPERATOR_PROJECT_DIRECTORY_CONFIGURATION_REQUEST_ENV,
RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV,
} from '@maka/runtime-host/operator';
import {
resolveDesktopRuntimeHostWslTarget,
runDesktopRuntimeHostWslManagement,
- runDesktopRuntimeHostWslSetup,
runDesktopRuntimeHostWslUpdate,
} from '../runtime-host-wsl-controller.js';
@@ -166,102 +163,6 @@ test('WSL management invokes the stable operator directly with the exact deploym
]);
});
-test('WSL setup forwards the development archive and its exact evidence', async () => {
- const launches: string[][] = [];
- const processFactory: RuntimeHostWslProcessFactory = (_executable, args) => {
- launches.push([...args]);
- const child = new EventEmitter() as ChildProcessWithoutNullStreams;
- const stdin = new PassThrough();
- const stdout = new PassThrough();
- const stderr = new PassThrough();
- Object.assign(child, { stdin, stdout, stderr, kill: () => true });
- process.nextTick(() => {
- if (launches.length === 1) stdout.end('/mnt/c/maka-development.tgz\n');
- else {
- stdout.end(encodeRuntimeHostSetupFrame({
- schemaVersion: 1,
- sequence: 0,
- kind: 'complete',
- version: '0.2.0-development',
- serviceId: 'b'.repeat(64),
- deploymentId: '00000000-0000-4000-8000-000000000001',
- operator: { kind: 'native', platform: 'posix', executablePath: '/tmp/maka/maka' },
- rootPath: '/tmp/maka/root',
- rootId: 'a'.repeat(64),
- endpoint: 'ws://127.0.0.1:7443/runtime-host',
- credentialId: 'credential-1',
- credential: 'secret-access-token',
- }));
- }
- stderr.end();
- child.emit('close', 0, null);
- });
- return child;
- };
- const integrity = `sha512-${createHash('sha512').update('archive evidence').digest('base64')}`;
-
- const installed = await runDesktopRuntimeHostWslSetup({
- distribution: 'Ubuntu',
- setupPackage: {
- kind: 'development_archive',
- path: 'C:\\maka-development.tgz',
- integrity,
- },
- principalId: 'desktop-owner:pairing',
- }, () => undefined, undefined, { processFactory, wslExecutable: 'wsl.exe' });
-
- assert.deepEqual(installed.operator, {
- kind: 'native', platform: 'posix', executablePath: '/tmp/maka/maka',
- });
- assert.deepEqual(launches[0], [
- '--distribution',
- 'Ubuntu',
- '--exec',
- 'wslpath',
- '-a',
- '-u',
- 'C:\\maka-development.tgz',
- ]);
- const setupCommand = launches[1]?.at(-1) ?? '';
- assert.match(setupCommand, /\$\{SHELL:-\/bin\/sh\}.*-lic/u);
- assert.match(setupCommand, new RegExp(`${RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV}=`, 'u'));
- assert.ok(setupCommand.includes(integrity));
- assert.match(setupCommand, /--update-existing/u);
- assert.doesNotMatch(setupCommand, /--allow-interrupt-active-tasks/u);
- assert.match(setupCommand, /--package.*\/mnt\/c\/maka-development\.tgz/u);
-});
-
-
-test('released WSL onboarding cannot authorize replacement or interruption', async () => {
- let command = '';
- await assert.rejects(runDesktopRuntimeHostWslSetup({
- distribution: 'Ubuntu',
- setupPackage: { kind: 'npm', specifier: 'maka-agent@0.2.0' },
- principalId: 'desktop:client',
- }, () => undefined, undefined, {
- wslExecutable: 'wsl.exe',
- processFactory: (_executable, args) => {
- command = args.at(-1) ?? '';
- const child = new EventEmitter() as ChildProcessWithoutNullStreams;
- const stdout = new PassThrough();
- const stderr = new PassThrough();
- Object.assign(child, { stdin: new PassThrough(), stdout, stderr, kill: () => true });
- process.nextTick(() => {
- stdout.end(encodeRuntimeHostSetupFrame({
- schemaVersion: 1, sequence: 0, kind: 'error',
- error: { code: 'version_change_requires_update', message: 'Use the update workflow' },
- }));
- stderr.end();
- child.emit('close', 1, null);
- });
- return child;
- },
- }), /Use the update workflow/u);
- assert.doesNotMatch(command, /--update-existing|--allow-interrupt-active-tasks/u);
- assert.match(command, /--reuse-existing-environment/u);
-});
-
-
test('WSL update cancellation closes retirement input without killing the transaction', async () => {
const abort = new AbortController();
let finish!: () => void;
@@ -308,25 +209,3 @@ test('WSL update cancellation closes retirement input without killing the transa
finish();
await rejected;
});
-
-
-test('released WSL discovery accepts an existing binding without a credential or package mutation', async () => {
- const existing = {
- schemaVersion: 1 as const, sequence: 0, kind: 'existing_environment' as const,
- version: '0.3.0', serviceId: 'a'.repeat(64), rootId: 'a'.repeat(64), rootPath: '/state',
- deploymentId: '00000000-0000-4000-8000-000000000001', operator: OPERATOR,
- };
- const result = await runDesktopRuntimeHostWslSetup({
- distribution: 'Ubuntu', setupPackage: { kind: 'npm', specifier: 'maka-agent@0.2.0' }, principalId: 'desktop:client',
- }, () => {}, undefined, {
- wslExecutable: 'wsl.exe',
- processFactory: () => {
- const child = new EventEmitter() as ChildProcessWithoutNullStreams;
- const stdout = new PassThrough(); const stderr = new PassThrough();
- Object.assign(child, { stdin: new PassThrough(), stdout, stderr, kill: () => true });
- process.nextTick(() => { stdout.end(encodeRuntimeHostSetupFrame(existing)); stderr.end(); child.emit('close', 0, null); });
- return child;
- },
- });
- assert.deepEqual(result, existing);
-});
diff --git a/apps/desktop/src/main/__tests__/runtime-host-wsl-handoff.test.ts b/apps/desktop/src/main/__tests__/runtime-host-wsl-handoff.test.ts
index 4d82b92e7a..a1ed9279ca 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-wsl-handoff.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-wsl-handoff.test.ts
@@ -92,29 +92,16 @@ test('WSL handoff carries exact source identity and separately forwards interrup
});
-test('legacy operator without a configuration fingerprint remains fenced by source version and Host identity', async () => {
- const { configurationFingerprint: _fingerprint, ...legacyService } = service;
+test('WSL handoff rejects a source without its configuration fingerprint', async () => {
+ const { configurationFingerprint: _fingerprint, ...unverifiedService } = service;
const blocker = await resolveDesktopWslHostHandoff(profile, incompatible(), new AbortController().signal, {
resolveBinding: async () => binding,
- resolvePackage: async () => ({
- kind: 'development_archive', path: '/selected.tgz', integrity: 'sha512-selected',
- displayVersion: '0.3.0-dev-abcdef012345',
- }),
- status: async () => ({ schemaVersion: 1, kind: 'result', action: 'status', service: legacyService }),
- update: async (input) => {
- assert.equal(input.expectedConfigFingerprint, undefined);
- assert.equal(input.expectedSourceVersion, legacyService.installedVersion);
- assert.deepEqual(input.expectedHost, { hostEpoch: 'old-host', pid: 42 });
- assert.equal(input.expectedTarget.deploymentId, binding.deployment.deploymentId);
- return { schemaVersion: 1, kind: 'error', action: 'update', error: { code: 'target_mismatch', message: 'Source changed under deployment lock' } };
- },
+ resolvePackage: async () => assert.fail('unverified source must not stage a package'),
+ status: async () => ({ schemaVersion: 1, kind: 'result', action: 'status', service: unverifiedService }),
+ update: async () => assert.fail('unverified source must not be updated'),
});
- assert.ok(blocker.replacement);
- assert.deepEqual(blocker.packageChange, {
- current: legacyService.installedVersion,
- target: '0.3.0-dev-abcdef012345',
- });
- assert.deepEqual(await blocker.replacement.execute('refuse_active_work', () => {}, 'explicit'), { kind: 'changed' });
+ assert.equal(blocker.replacement, undefined);
+ assert.match(blocker.operatorStep!, /could not verify/u);
});
test('development handoff never presents package integrity as a version', async () => {
diff --git a/apps/desktop/src/main/__tests__/session-local.test.ts b/apps/desktop/src/main/__tests__/session-local.test.ts
index 75b5745017..c895e63285 100644
--- a/apps/desktop/src/main/__tests__/session-local.test.ts
+++ b/apps/desktop/src/main/__tests__/session-local.test.ts
@@ -45,7 +45,7 @@ import { createAttachmentApprovalRegistry } from '../attachment-approval.js';
const accepted: TurnMessageSubmitResult = {
disposition: 'turn_started',
turnId: 'turn-1',
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
const intent = (messageId = 'message-1', sessionId = 'session-1'): LocalMessageIntent => ({
command: { sessionId, messageId, placement: 'current_turn', content: { text: 'hello' } },
@@ -319,7 +319,7 @@ for (const refusal of ['operation-error', 'blocked-skill'] as const) {
calls.push(input.messageId);
if (input.messageId === 'message-1') {
if (refusal === 'blocked-skill')
- return { disposition: 'blocked', skillInvocation: accepted.skillInvocation };
+ return { disposition: 'blocked', message: 'Input unavailable', preparation: accepted.preparation };
throw new RuntimeHostOperationError('turn.message.submit', 'session_busy', 'busy');
}
return accepted;
diff --git a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts
index f9f724a310..df74a2470f 100644
--- a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts
+++ b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts
@@ -49,7 +49,7 @@ function createBridgeRecorder(): {
disposition: 'steering',
attachments: [],
inlineReferences: [],
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
},
],
]);
diff --git a/apps/desktop/src/main/browser-message-box.ts b/apps/desktop/src/main/browser-message-box.ts
index d3f1af108b..9ca5e481a4 100644
--- a/apps/desktop/src/main/browser-message-box.ts
+++ b/apps/desktop/src/main/browser-message-box.ts
@@ -107,6 +107,8 @@ export async function showBrowserMessageBoxWithRuntime(
): Promise<MessageBoxReturnValue> {
activeBrowserMessageBoxPresentations += 1;
try {
+ const cancelled = (): MessageBoxReturnValue => ({response: normalizeBrowserMessageBoxPresentation(options, {...appearance, dark: appearance.dark ?? runtime.shouldUseDarkColors}).cancelId, checkboxChecked: false});
+ if (options.signal?.aborted) return cancelled();
const visibleParent = (): BrowserWindow | undefined =>
parent && !parent.isDestroyed() && parent.isVisible() && !parent.isMinimized()
? parent
@@ -114,6 +116,9 @@ export async function showBrowserMessageBoxWithRuntime(
try {
return await presentBrowserMessageBox(runtime, options, visibleParent(), appearance);
} catch (error) {
+ if (options.signal?.aborted) return cancelled();
+ // macOS parentless native dialogs ignore AbortSignal and block synchronously.
+ if (options.signal && !visibleParent()) throw error;
runtime.onBrowserError(error);
return await runtime.showNative(options, visibleParent());
}
@@ -188,14 +193,19 @@ async function presentBrowserMessageBox(
if (settled) return;
settled = true;
clearPresentationTimeout();
+ options.signal?.removeEventListener('abort', abort);
resolve({ response, checkboxChecked: false });
};
const fail = (error: unknown): void => {
if (settled) return;
settled = true;
clearPresentationTimeout();
+ options.signal?.removeEventListener('abort', abort);
reject(error instanceof Error ? error : new Error(String(error)));
};
+ const abort = () => finish(presentation.cancelId);
+ options.signal?.addEventListener('abort', abort, {once:true});
+ if (options.signal?.aborted) { abort(); return; }
presentationTimeout = setTimeout(
() => fail(new Error('Dialog renderer did not become interactive in time')),
runtime.presentationTimeoutMs ?? DIALOG_PRESENTATION_TIMEOUT_MS,
diff --git a/apps/desktop/src/main/browser/browser-tools.ts b/apps/desktop/src/main/browser/browser-tools.ts
index 1997ba8bdc..6ab12ae98d 100644
--- a/apps/desktop/src/main/browser/browser-tools.ts
+++ b/apps/desktop/src/main/browser/browser-tools.ts
@@ -20,7 +20,7 @@
import { z } from 'zod';
import { htmlToMarkdown } from '@jackwener/opencli/utils';
import type { IPage } from '@jackwener/opencli/types';
-import type { MakaTool } from '@maka/runtime/tool-runtime';
+import type { SessionTool } from '@maka/runtime/tool-runtime';
import { browserOriginAdmission } from './browser-origin-admission.js';
import type { BrowserOriginLease } from './browser-host.js';
import {
@@ -162,7 +162,7 @@ function guardBrowserPage(page: IPage, lease: BrowserOriginLease): IPage {
}) as IPage;
}
-export function buildBrowserNavigateTool(): MakaTool<{ url: string }, string> {
+export function buildBrowserNavigateTool(): SessionTool<{ url: string }, string> {
return {
name: 'browser_navigate',
displayName: '浏览器导航',
@@ -203,7 +203,7 @@ export function buildBrowserNavigateTool(): MakaTool<{ url: string }, string> {
};
}
-export function buildBrowserSnapshotTool(): MakaTool<Record<string, never>, string> {
+export function buildBrowserSnapshotTool(): SessionTool<Record<string, never>, string> {
return {
name: 'browser_snapshot',
displayName: '浏览器快照',
@@ -233,7 +233,7 @@ export function buildBrowserSnapshotTool(): MakaTool<Record<string, never>, stri
};
}
-export function buildBrowserClickTool(): MakaTool<{ ref: string }, string> {
+export function buildBrowserClickTool(): SessionTool<{ ref: string }, string> {
return {
name: 'browser_click',
displayName: '浏览器点击',
@@ -299,7 +299,7 @@ function navigationResult(url: string, requiresApproval: boolean): string {
);
}
-export function buildBrowserTypeTool(): MakaTool<{ ref: string; text: string; submit?: boolean }, string> {
+export function buildBrowserTypeTool(): SessionTool<{ ref: string; text: string; submit?: boolean }, string> {
return {
name: 'browser_type',
displayName: '浏览器输入',
@@ -341,7 +341,7 @@ export function buildBrowserTypeTool(): MakaTool<{ ref: string; text: string; su
};
}
-export function buildBrowserWaitTool(): MakaTool<
+export function buildBrowserWaitTool(): SessionTool<
{ text?: string; selector?: string; time?: number; timeout?: number },
string
> {
@@ -424,7 +424,7 @@ export function buildBrowserWaitTool(): MakaTool<
};
}
-export function buildBrowserExtractTool(): MakaTool<{ selector?: string; start?: number }, string> {
+export function buildBrowserExtractTool(): SessionTool<{ selector?: string; start?: number }, string> {
return {
name: 'browser_extract',
displayName: '浏览器提取',
@@ -503,7 +503,7 @@ export function readHtmlJs(selectorJson: string): string {
}
/** The six generic observe→act browser tools, in observe-before-act order. */
-export function buildBrowserTools(): MakaTool[] {
+export function buildBrowserTools(): SessionTool[] {
return [
buildBrowserNavigateTool(),
buildBrowserSnapshotTool(),
@@ -511,5 +511,5 @@ export function buildBrowserTools(): MakaTool[] {
buildBrowserTypeTool(),
buildBrowserWaitTool(),
buildBrowserExtractTool(),
- ] as MakaTool[];
+ ] as SessionTool[];
}
diff --git a/apps/desktop/src/main/client-plugin-remote-ipc.ts b/apps/desktop/src/main/client-plugin-remote-ipc.ts
index 08dd0d0c9e..838fb55f2c 100644
--- a/apps/desktop/src/main/client-plugin-remote-ipc.ts
+++ b/apps/desktop/src/main/client-plugin-remote-ipc.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { HOST_OPERATION_SPECS, type PluginRemoteInput, type PluginRemoteResult } from '@maka/runtime-host/protocol';
+import { HOST_OPERATION_SPECS, type PluginRemoteInput, type PluginRemoteResult, type PluginAuthorizationInput, type PluginAuthorizationResult } from '@maka/runtime-host/protocol';
import type { IpcMain, IpcMainInvokeEvent, WebContents } from 'electron';
import { isAbsolute } from 'node:path';
import { randomUUID } from 'node:crypto';
@@ -29,6 +29,8 @@ interface DocumentOwner {
readonly pending: Set<Promise<unknown>>;
pendingOpens: number;
closed: boolean;
+ readonly cancellation: AbortController;
+ readonly authorizations: Map<string, AbortController>;
dispose(): void;
}
@@ -41,6 +43,11 @@ export function registerClientPluginRemoteIpc(input: {
};
readonly ownsRenderer: (contents: WebContents) => boolean;
readonly report: (error: unknown) => void;
+ readonly authorization?: {
+ validate(client: PluginAuthorizationInput['client']): Promise<void>;
+ confirm(input: PluginAuthorizationInput, signal: AbortSignal): Promise<boolean>;
+ request(input: PluginAuthorizationInput): Promise<PluginAuthorizationResult>;
+ };
readonly files?: {
validate(input: PluginClientQueryInput): Promise<void>;
pick(): Promise<string | null>;
@@ -71,10 +78,11 @@ export function registerClientPluginRemoteIpc(input: {
if (previous?.nonce === nonce) return previous;
previous?.dispose();
const state: DocumentOwner = {
- nonce, documents: new Set(), pending: new Set(), pendingOpens: 0, closed: false,
+ nonce, documents: new Set(), pending: new Set(), pendingOpens: 0, closed: false, cancellation: new AbortController(), authorizations: new Map(),
dispose() {
if (state.closed) return;
state.closed = true;
+ state.cancellation.abort();
if (owners.get(sender) === state) owners.delete(sender);
sender.removeListener('did-start-navigation', navigation);
sender.removeListener('render-process-gone', dispose);
@@ -151,6 +159,43 @@ export function registerClientPluginRemoteIpc(input: {
return path;
});
+ input.ipcMain.handle('plugins:authorization-cancel', (event, nonce: unknown, expectedEpoch: unknown, requestId: unknown) => {
+ const owner = owners.get(event.sender);
+ if (closed || !owner || owner.closed || owner.nonce !== nonce || expectedEpoch !== epoch ||
+ !input.ownsRenderer(event.sender) || event.senderFrame?.frameToken !== event.sender.mainFrame.frameToken) return;
+ if (typeof requestId === 'string') owner.authorizations.get(requestId)?.abort();
+ });
+
+ input.ipcMain.handle('plugins:authorization', async (event, nonce: unknown, expectedEpoch: unknown, raw: unknown, requestId: unknown): Promise<PluginAuthorizationResult> => {
+ const owner = ownerFor(event, nonce);
+ if (expectedEpoch !== epoch) throw new Error('Client connection has retired');
+ const authorization = input.authorization;
+ if (!authorization) throw new Error('Plugin authorization is unavailable');
+ const value = HOST_OPERATION_SPECS['plugin.authorization'].decodeInput(raw);
+ if (typeof requestId !== 'string' || !/^[0-9a-f-]{36}$/i.test(requestId) || owner.authorizations.has(requestId))
+ throw new Error('Invalid authorization request identity');
+ if (owner.authorizations.size >= 32) throw new Error('Authorization request limit exceeded');
+ const cancellation = new AbortController();
+ owner.authorizations.set(requestId, cancellation);
+ const signal = AbortSignal.any([cancellation.signal, owner.cancellation.signal, AbortSignal.timeout(90_000)]);
+ return track(owner.pending, (async () => {
+ try {
+ await authorization.validate(value.client);
+ signal.throwIfAborted();
+ if (value.command.kind === 'approve') {
+ if (!await authorization.confirm(value, signal) || signal.aborted) return {kind:'grant',grant:null};
+ }
+ signal.throwIfAborted();
+ // Revalidate publication after consent; a pending dialog never follows a replacement.
+ await authorization.validate(value.client);
+ signal.throwIfAborted();
+ // Beyond this cut Host owns the accepted command. Cancellation never
+ // retracts a durable grant; its operation identity recovers the receipt.
+ return await authorization.request(value);
+ } finally { owner.authorizations.delete(requestId); }
+ })());
+ });
+
return async () => {
closed = true;
for (const owner of owners.values()) owner.dispose();
diff --git a/apps/desktop/src/main/main-process-diagnostics.ts b/apps/desktop/src/main/main-process-diagnostics.ts
index 7adc9c9463..8858fd398e 100644
--- a/apps/desktop/src/main/main-process-diagnostics.ts
+++ b/apps/desktop/src/main/main-process-diagnostics.ts
@@ -493,7 +493,7 @@ export function formatDesktopDiagnosticReport(
`Protocol: v${host.protocolVersion} · compatibility ${host.compatibilityEpoch}`,
`State: ${host.state}`,
`Process: ${host.pid} · uptime ${host.processUptimeSeconds}s`,
- `Runtime: Node ${host.nodeVersion} · ${host.platform} ${host.osRelease} (${host.arch})`,
+ `Platform: ${host.platform} ${host.osRelease} (${host.arch})`,
`Activity: ${host.connections} connections · ${host.activeOperations} operations · ${host.activeResidencies} residencies`,
`Recent Runtime Host logs (${host.logs.length})`,
...(host.logs.length > 0 ? host.logs : ['<none captured>']),
diff --git a/apps/desktop/src/main/managed-artifact-preview-tools.ts b/apps/desktop/src/main/managed-artifact-preview-tools.ts
index 939a78c2f5..50db0639d5 100644
--- a/apps/desktop/src/main/managed-artifact-preview-tools.ts
+++ b/apps/desktop/src/main/managed-artifact-preview-tools.ts
@@ -17,14 +17,14 @@
* under the License.
*/
-import type { MakaTool } from '@maka/runtime/tool-runtime';
+import type { SessionTool } from '@maka/runtime/tool-runtime';
import { z } from 'zod';
import type { ArtifactPreviewEndpoint } from './managed-artifact-preview.js';
export function buildManagedArtifactPreviewTools(
prepare: (sessionId: string, artifactId: string, signal: AbortSignal) => Promise<ArtifactPreviewEndpoint>,
-): readonly MakaTool[] {
- const tool: MakaTool<{ artifactId: string }, ArtifactPreviewEndpoint> = {
+): readonly SessionTool[] {
+ const tool: SessionTool<{ artifactId: string }, ArtifactPreviewEndpoint> = {
name: 'ArtifactPreview',
displayName: 'Prepare HTML preview',
description: 'Create a Desktop-managed, temporary HTTP URL for an HTML Artifact in the current session. No shell server or file:// navigation is needed. The URL is a bearer capability: do not share it. It expires after 30 minutes or when the client disconnects. Only self-contained HTML is supported: inline scripts/styles and embedded images; remote subresources, fetch requests, forms and local file access are blocked. This is not OS network isolation: an external browser can navigate [...]
diff --git a/apps/desktop/src/main/permission-response-guard.ts b/apps/desktop/src/main/permission-response-guard.ts
index b2390fd984..6d389c4554 100644
--- a/apps/desktop/src/main/permission-response-guard.ts
+++ b/apps/desktop/src/main/permission-response-guard.ts
@@ -36,6 +36,7 @@ import { MAX_ATTACHMENT_COUNT } from '@maka/core/attachments';
import { isAttachmentRef, isCanonicalStorageRef, type AttachmentRef } from '@maka/core/events';
import { isOrchestrationMode, isTurnOrchestrationSource } from '@maka/core/orchestration';
+import { decodeInputSelections, type InputSelections } from '@maka/runtime-host/protocol';
const MAX_PERMISSION_REQUEST_ID_LENGTH = 128;
const MAX_TURN_ID_LENGTH = 128;
@@ -62,7 +63,7 @@ interface NormalizedSendSessionCommand {
turnId?: string;
text: string;
displayText?: string;
- skillIds?: string[];
+ inputSelections?: InputSelections;
attachmentItems?: unknown;
retainedAttachments?: AttachmentRef[];
turnOrchestration?: TurnOrchestration;
@@ -208,7 +209,7 @@ export function normalizeSessionSendCommand(input: unknown): NormalizedSendSessi
const text = normalizeSendText(value.text);
const displayText =
value.displayText === undefined ? undefined : normalizeSendText(value.displayText);
- const skillIds = normalizeSessionSkillIds(value.skillIds);
+ const inputSelections = decodeInputSelections(value.inputSelections);
// A send may carry structured content instead of text (a pure quote or a
// pure attachment, #4804). Only the presence is decided here: attachment
// state, ownership, and size limits stay with the ingestion checks, and
@@ -227,7 +228,7 @@ export function normalizeSessionSendCommand(input: unknown): NormalizedSendSessi
const hasAttachmentItems = (attachmentItems.attachmentItems?.length ?? 0) > 0;
if (
!text.trim() &&
- skillIds.length === 0 &&
+ Object.keys(inputSelections).length === 0 &&
(quotes?.length ?? 0) === 0 &&
!hasAttachmentItems &&
(retainedAttachments.retainedAttachments?.length ?? 0) === 0
@@ -240,7 +241,7 @@ export function normalizeSessionSendCommand(input: unknown): NormalizedSendSessi
...normalizeOptionalSendTurnId(value.turnId),
text,
...(displayText !== undefined ? { displayText } : {}),
- ...(skillIds.length > 0 ? { skillIds } : {}),
+ ...(Object.keys(inputSelections).length > 0 ? { inputSelections } : {}),
...attachmentItems,
...retainedAttachments,
...(value.turnOrchestration !== undefined
@@ -385,26 +386,6 @@ function normalizeSendText(input: unknown): string {
return input;
}
-export function normalizeSessionSkillIds(input: unknown): string[] {
- if (input === undefined) return [];
- if (
- !Array.isArray(input) ||
- input.length > 50 ||
- input.some(
- (id) =>
- typeof id !== 'string' ||
- id.length === 0 ||
- id.length > 512 ||
- // The field name is retained for wire compatibility. Values may be a
- // legacy id or a stable scope-aware ref such as project:maka:writer.
- !/^[A-Za-z0-9][A-Za-z0-9._-]*(?::[A-Za-z0-9][A-Za-z0-9._-]*)*$/.test(id),
- )
- ) {
- throw new Error('Invalid send skillIds');
- }
- return [...input];
-}
-
export function normalizeStopSessionInput(input: unknown): NormalizedStopSessionInput {
if (input === undefined) return {};
const value = requireObject(input, 'Invalid stop session input');
diff --git a/apps/desktop/src/main/plugin-authorization-dialog.ts b/apps/desktop/src/main/plugin-authorization-dialog.ts
new file mode 100644
index 0000000000..578a59daa8
--- /dev/null
+++ b/apps/desktop/src/main/plugin-authorization-dialog.ts
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { AuthorizationCapability } from '@maka-agent/plugin-sdk/client';
+import type { PluginAuthorizationInput } from '@maka/runtime-host/protocol';
+import type { MessageBoxOptions } from 'electron';
+
+/** Application-owned copy; plugins supply a proposal, never the confirmation UI. */
+export function pluginAuthorizationDialog(input: PluginAuthorizationInput, locale: string, signal: AbortSignal): MessageBoxOptions {
+ if (input.command.kind !== 'approve') throw new Error('Consent requires a proposal');
+ const zh = locale.startsWith('zh');
+ const labels: Record<AuthorizationCapability, readonly [string,string]> = {
+ read_files: ['Read workspace files', '读取工作区文件'],
+ write_files: ['Modify workspace files', '修改工作区文件'],
+ network: ['Make network requests', '发送网络请求'],
+ models: ['Call models (may incur usage costs)', '调用模型(可能产生费用)'],
+ processes: ['Run programs and terminals', '运行程序与终端'],
+ client_capabilities: ['Invoke connected client capabilities', '调用已连接客户端的能力'],
+ executions: ['Create, submit, inspect and control executions', '创建、提交、查询与控制执行'],
+ notifications: ['Send notifications', '发送通知'],
+ };
+ const {request} = input.command;
+ const target = request.target.kind === 'profile' ? 'profile' : request.target.kind === 'session'
+ ? `${request.target.sessionId} (${zh ? '当前工作区与权限' : 'current workspace and permissions'})`
+ : `${request.target.workspace.kind === 'project' ? request.target.workspace.projectId : request.target.workspace.path} (${request.target.permissionMode})`;
+ return {
+ type: 'warning', title: zh ? '插件后台授权' : 'Plugin background authorization',
+ message: `${input.client.extensionId}\n${zh ? '允许此插件在后台工作?' : 'Allow this plugin to work in the background?'}`,
+ detail: [request.title, `${zh ? '范围' : 'Scope'}: ${input.scope}`, `${zh ? '目标' : 'Target'}: ${target}`,
+ request.capabilities.map((capability) => labels[capability][zh ? 1 : 0]).join('\n'),
+ zh ? '授权在重启后仍有效,直到撤销。目标权限变化会使授权失效。' : 'Authorization survives restart until revoked. Changes to the target permissions invalidate it.',
+ ].join('\n\n'),
+ buttons: zh ? ['取消','允许'] : ['Cancel','Allow'], cancelId:0, defaultId:0, signal,
+ };
+}
diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts
index 4cbac05bcb..590fc60441 100644
--- a/apps/desktop/src/main/runtime-host-boot.ts
+++ b/apps/desktop/src/main/runtime-host-boot.ts
@@ -231,7 +231,6 @@ import {
} from "./runtime-host-ssh-terminal.js";
import {
runDesktopRuntimeHostWslManagement,
- runDesktopRuntimeHostWslSetup,
resolveDesktopRuntimeHostWslTarget,
runNativeRuntimeHostWslSetup,
prepareNativeRuntimeHostWslPackage,
@@ -261,6 +260,7 @@ import { RuntimeHostOAuthPresentation } from "./runtime-host-oauth-presentation.
import { registerRuntimeHostPermissionsIpc } from "./runtime-host-permissions-ipc-main.js";
import { registerRuntimeHostRendererIpc } from "./runtime-host-renderer-ipc-main.js";
import { registerClientPluginRemoteIpc } from './client-plugin-remote-ipc.js';
+import { pluginAuthorizationDialog } from './plugin-authorization-dialog.js';
import { registerRuntimeHostSearchIpc } from "./runtime-host-search-ipc-main.js";
import { createRuntimeHostProjectCatalog } from "./runtime-host-project-catalog.js";
import { createRuntimeHostDefaultRecovery } from "./runtime-host-default-recovery.js";
@@ -793,18 +793,14 @@ const runtimeHostOnboarding = createDesktopRuntimeHostOnboarding({
ipcMain,
clientInstanceId: runtimeHostClientInstanceId,
profiles: runtimeHostProfileService,
- ...(!isE2e ? { nativeSetup: {
+ nativeSetup: {
resolvePackage: resolveNativePackage,
ssh: runtimeHostSshTerminal.runNativeSetup,
wsl: runNativeRuntimeHostWslSetup,
- } } : {}),
- runSetup: runtimeHostSshTerminal.runSetup,
- runWslSetup: runDesktopRuntimeHostWslSetup,
+ },
resolveWslTargetIdentity: resolveDesktopRuntimeHostWslTarget,
listWslDistributions: listRuntimeHostWslDistributions,
- setupPackageMode: runtimeHostSetupPackage.mode,
resolveSshTargetIdentity: runtimeHostSshTerminal.resolveTargetIdentity,
- resolveSetupPackage: runtimeHostSetupPackage.resolve,
send: (snapshot) =>
mainWindowController.send("runtime-host-onboarding:changed", snapshot),
});
@@ -1309,7 +1305,7 @@ const startLocalRuntimeHostManager = () => startRuntimeHostDesktopManager(
const target = runtimePolicyTargetsByEpoch.get(scope.targetEpoch);
if (!target?.isActive()) throw new Error('Preview target is no longer active');
return managedArtifactPreview.prepare(scope.targetEpoch, target.client, sessionId, artifactId, signal);
- }),
+ }).map(tool => ({ context: 'session' as const, tool })),
},
{
offerId: "desktop_settings",
@@ -1818,6 +1814,15 @@ function registerHostClientIpc(
ipcMain: scopedIpc, client,
ownsRenderer: (contents) => mainWindowController.ownsRenderer(contents),
report: (error) => console.error('[plugins] Remote cleanup failed:', error),
+ authorization: {
+ validate: async (identity) => { await client.request('plugin.client.query', {kind:'bundle',entryId:identity.entryId,activation:identity.activation,clientDigest:identity.clientDigest,offset:0}); },
+ confirm: async (input, signal) => {
+ const locale = desktopLocale.current();
+ const result = await showDesktopMessageBox(pluginAuthorizationDialog(input, locale, signal), {locale});
+ return result.response === 1;
+ },
+ request: (input) => client.request('plugin.authorization', input),
+ },
files: usesHostWorkspace ? undefined : {
validate: async (input) => { await client.request('plugin.client.query', input); },
pick: async () => {
diff --git a/apps/desktop/src/main/runtime-host-bot-session-adapter.ts b/apps/desktop/src/main/runtime-host-bot-session-adapter.ts
index e9beb307a4..3902583d9f 100644
--- a/apps/desktop/src/main/runtime-host-bot-session-adapter.ts
+++ b/apps/desktop/src/main/runtime-host-bot-session-adapter.ts
@@ -151,13 +151,7 @@ export function createRuntimeHostBotSessionAdapter(
if (started.kind === 'blocked') {
return {
kind: 'errored' as const,
- reason: started.skillInvocation.failed
- .map((failure) =>
- failure.reason === 'too_many_requests'
- ? `Skill request limit exceeded: ${failure.requestLimit}`
- : `${failure.request}: ${failure.reason}`,
- )
- .join(', '),
+ reason: started.message,
};
}
} catch (error) {
diff --git a/apps/desktop/src/main/runtime-host-client.ts b/apps/desktop/src/main/runtime-host-client.ts
index 9f85c1e48d..3b7f1be08f 100644
--- a/apps/desktop/src/main/runtime-host-client.ts
+++ b/apps/desktop/src/main/runtime-host-client.ts
@@ -106,7 +106,6 @@ import {
SESSION_TRANSCRIPT_BOOTSTRAP_MAX_BYTES,
type SessionCatalogChangedFrame,
type ScheduledTaskChangedFrame,
- type SessionCatalogItem,
type SessionCatalogProjection,
type SharedSessionCatalogProjection,
type CollaborationAccessQueryResult,
@@ -653,7 +652,7 @@ export class DesktopRuntimeHostClient {
async listSessions(): Promise<SessionCatalogProjection[]> {
this.#assertOpen();
try {
- return (await readRuntimeHostSessions(this.connection)).map(requireSessionProjection);
+ return await readRuntimeHostSessions(this.connection);
} catch (error) {
if (error instanceof DesktopRuntimeHostClientError) throw error;
if (!(error instanceof RuntimeHostCatalogReadError)) throw error;
@@ -914,17 +913,13 @@ export class DesktopRuntimeHostClient {
"Runtime Host returned an invalid Session catalog lookup",
);
}
- return result.session === null
- ? null
- : requireSessionProjection(result.session);
+ return result.session;
}
async createSession(
input: SessionCreateInput,
): Promise<SessionCatalogProjection> {
- return requireSessionProjection(
- await this.request("session.create", input),
- );
+ return this.request("session.create", input);
}
listExternalSessionSources(): Promise<ExternalSessionSourceQueryResult> {
@@ -942,9 +937,7 @@ export class DesktopRuntimeHostClient {
readonly sourceSessionId: string;
}): Promise<ExternalSessionImportResult<SessionCatalogProjection>> {
const result = await this.request("external-session.import", input);
- return result.kind === 'imported'
- ? { kind: 'imported', session: requireSessionProjection(result.session) }
- : result;
+ return result;
}
exportSessionBundle(input: {
@@ -996,12 +989,7 @@ export class DesktopRuntimeHostClient {
sessionId: string,
readThroughMessageId: string,
): Promise<SessionCatalogProjection> {
- return requireSessionProjection(
- await this.request("session.read_marker.set", {
- sessionId,
- readThroughMessageId,
- }),
- );
+ return this.request("session.read_marker.set", { sessionId, readThroughMessageId });
}
readExecutionBoundary(sessionId: string): Promise<ExecutionBoundarySummary> {
@@ -1012,9 +1000,7 @@ export class DesktopRuntimeHostClient {
sessionId: string,
state: SessionLifecycleState,
): Promise<SessionCatalogProjection> {
- return requireSessionProjection(
- await this.request("session.lifecycle.set", { sessionId, state }),
- );
+ return this.request("session.lifecycle.set", { sessionId, state });
}
/**
@@ -1095,7 +1081,7 @@ export class DesktopRuntimeHostClient {
? await this.request("session.branch.create", request)
: await this.request("session.revision.create", request);
if (result.kind === "committed")
- return requireSessionProjection(result.session);
+ return result.session;
}
throw revisionConflict(`${kind} copy`, input.sourceSessionId);
}
@@ -1708,7 +1694,7 @@ export class DesktopRuntimeHostClient {
const current = await this.#requireSession(sessionId);
const result = await update(current);
if (result.kind === "committed")
- return requireSessionProjection(result.session);
+ return result.session;
}
throw revisionConflict("update", sessionId);
}
@@ -1800,16 +1786,6 @@ class DesktopSessionHandle implements DesktopRuntimeHostSession {
}
}
-function requireSessionProjection(
- item: SessionCatalogItem,
-): SessionCatalogProjection {
- if (!("kind" in item)) return item;
- throw new DesktopRuntimeHostClientError(
- "unsupported_session",
- `Runtime Host Session is not representable by this Desktop Client: ${item.id}`,
- );
-}
-
function clientClosed(): DesktopRuntimeHostClientError {
return new DesktopRuntimeHostClientError(
"client_closed",
diff --git a/apps/desktop/src/main/runtime-host-local-remote-access.ts b/apps/desktop/src/main/runtime-host-local-remote-access.ts
index 4f9a3fd861..f554779dd0 100644
--- a/apps/desktop/src/main/runtime-host-local-remote-access.ts
+++ b/apps/desktop/src/main/runtime-host-local-remote-access.ts
@@ -29,7 +29,6 @@ import {
type HostHandoffBlocker,
} from '@maka/runtime-host/client';
import {
- createRuntimeHostLegacyPosixOperatorCommand,
runtimeHostManagedOperatorCommand,
decodeRuntimeHostOperatorCommand,
resolveRuntimeHostManagedDeploymentAuthority,
@@ -77,16 +76,6 @@ interface LocalServiceSetupPending {
readonly allowInterruptActiveTasks: boolean;
}
-/** Released schema-v1 setup intent written before managed ownership was established. */
-interface LocalServiceLegacyHandoff {
- readonly schemaVersion: 1;
- readonly state: 'handoff';
- readonly rootPath: string;
- readonly rootId: string;
- readonly coordinationRelays: readonly string[];
- readonly allowInterruptActiveTasks: boolean;
-}
-
interface LocalServiceManaged extends LocalServiceTarget {
readonly state: 'managed';
}
@@ -163,7 +152,6 @@ interface LocalServiceUninstalling extends LocalServiceTarget {
}
type LocalServiceLifecycle =
- | LocalServiceLegacyHandoff
| LocalServiceSetupPending
| LocalServiceManaged
| LocalServicePeerChanging
@@ -237,7 +225,7 @@ export function createDesktopLocalRuntimeHostRemoteAccess(input: {
});
const adoptCommittedSetup = async (
- setup: LocalServiceSetupPending | LocalServiceLegacyHandoff,
+ setup: LocalServiceSetupPending,
): Promise<
| { readonly kind: 'absent' | 'transition' }
| { readonly kind: 'managed'; readonly managed: LocalServiceManaged }
@@ -245,16 +233,7 @@ export function createDesktopLocalRuntimeHostRemoteAccess(input: {
const authority = await resolveManagedDeploymentAuthority(setup.rootId);
if (!authority) return { kind: 'absent' };
if (authority.kind === 'transition') return authority;
- const target =
- setup.schemaVersion === 1
- ? {
- ...authority.target,
- operator: createRuntimeHostLegacyPosixOperatorCommand(
- join(authority.deploymentRoot, 'operator'),
- ),
- }
- : authority.target;
- const managed = managedLifecycle(target);
+ const managed = managedLifecycle(authority.target);
await writeDocument(lifecyclePath, managed);
return { kind: 'managed', managed };
};
@@ -276,7 +255,7 @@ export function createDesktopLocalRuntimeHostRemoteAccess(input: {
return {
state: 'unavailable',
message:
- lifecycle.state === 'setupPending' || lifecycle.state === 'handoff'
+ lifecycle.state === 'setupPending'
? 'Local Runtime Host setup is being recovered'
: lifecycle.state === 'peerChanging'
? 'Local Runtime Host remote access is being recovered'
@@ -317,14 +296,6 @@ export function createDesktopLocalRuntimeHostRemoteAccess(input: {
if (recovered.kind === 'active_tasks') return recovered;
lifecycle = managedLifecycle(lifecycle);
}
- if (lifecycle?.state === 'handoff') {
- const recovered = await recoverLegacyHandoff(lifecycle);
- if (recovered.kind === 'active_tasks') return recovered;
- if (recovered.kind === 'external') {
- throw new Error('The Local Runtime Host is already managed outside this Desktop');
- }
- lifecycle = recovered.managed;
- }
if (lifecycle?.state === 'setupPending') {
const committed = await adoptCommittedSetup(lifecycle);
if (committed.kind === 'managed') {
@@ -487,51 +458,6 @@ export function createDesktopLocalRuntimeHostRemoteAccess(input: {
}
};
- const recoverLegacyHandoff = async (
- legacy: LocalServiceLegacyHandoff,
- ): Promise<
- | { readonly kind: 'active_tasks' }
- | { readonly kind: 'external' }
- | { readonly kind: 'complete'; readonly managed: LocalServiceManaged }
- > => {
- const pending = pendingSetup(legacy);
- const authority = await adoptCommittedSetup(legacy);
- if (authority.kind === 'managed') {
- return { kind: 'complete', managed: authority.managed };
- }
- if (authority.kind === 'transition') {
- await writeDocument(lifecyclePath, pending);
- return finishSetup(pending, 'recovery');
- }
-
- const manager = requireManager(input.manager);
- const retirement = await manager.retireOwnedLocalHost(
- legacy.allowInterruptActiveTasks ? 'interrupt_active_work' : 'refuse_active_work',
- );
- if (retirement.kind === 'active_tasks') return { kind: 'active_tasks' };
- if (retirement.kind === 'not_owned') {
- const raced = await adoptCommittedSetup(legacy);
- if (raced.kind === 'managed') {
- return { kind: 'complete', managed: raced.managed };
- }
- if (raced.kind === 'absent') {
- await removeDocument(lifecyclePath);
- return { kind: 'external' };
- }
- }
-
- try {
- await writeDocument(lifecyclePath, pending);
- const setupPackage = await input.resolveSetupPackage(closing.signal);
- const reconcile = () => reconcileSetup(pending, setupPackage);
- return retirement.kind === 'not_owned'
- ? await manager.runManagedLocalHostChange(reconcile)
- : await reconcile();
- } finally {
- if (retirement.kind === 'retired') retirement.resume();
- }
- };
-
const createConnectionCode = (): Promise<string> =>
serialize(async () => {
const managed = requireManaged(
@@ -823,9 +749,10 @@ export function createDesktopLocalRuntimeHostRemoteAccess(input: {
if (facts.hostEpoch === observed.connection.hostEpoch && facts.pid === observed.registration.pid &&
facts.state === 'ready' && facts.connections >= 1 && facts.activeOperations >= 1) {
// The accepted diagnostic connection and query are our own, not work
- // requiring consent. Legacy residency evidence stays conservative.
+ // requiring consent. The Host supplies the drain count directly.
activity = { connections: facts.connections - 1, activeOperations: facts.activeOperations - 1,
processUptimeSeconds: facts.processUptimeSeconds, residencies: facts.residencies,
+ drainResidencies: facts.drainResidencies,
...(observed.connection.cooperativeHandoff ? { cooperativeHandoff: true } : {}) };
}
} catch { /* Missing activity evidence never authorizes automatic interruption. */ }
@@ -958,18 +885,16 @@ export function createDesktopLocalRuntimeHostRemoteAccess(input: {
return true;
});
}
- if (observed?.state !== 'setupPending' && observed?.state !== 'handoff') return false;
+ if (observed?.state !== 'setupPending') return false;
return serialize(async () => {
operationSignal.throwIfAborted();
const pending = await readLifecycle(lifecyclePath, input.rootPath, input.rootId);
- if (pending?.state !== 'setupPending' && pending?.state !== 'handoff') return false;
- const current = pendingSetup(pending);
+ if (pending?.state !== 'setupPending') return false;
const authority = await adoptCommittedSetup(pending);
if (authority.kind === 'absent') return false;
if (authority.kind === 'managed') return true;
- if (pending.state === 'handoff') await writeDocument(lifecyclePath, current);
const setupPackage = await input.resolveSetupPackage(operationSignal);
- await reconcileSetup(current, setupPackage, operationSignal);
+ await reconcileSetup(pending, setupPackage, operationSignal);
return true;
});
},
@@ -988,12 +913,11 @@ export function createDesktopLocalRuntimeHostRemoteAccess(input: {
}
return;
}
- if (lifecycle.state === 'handoff' || lifecycle.state === 'setupPending') {
+ if (lifecycle.state === 'setupPending') {
const committed = await adoptCommittedSetup(lifecycle);
if (committed.kind === 'managed') return;
if (!input.directPeerAvailable) return;
- if (lifecycle.state === 'handoff') await recoverLegacyHandoff(lifecycle);
- else await finishSetup(lifecycle, 'recovery');
+ await finishSetup(lifecycle, 'recovery');
return;
}
}),
@@ -1186,19 +1110,6 @@ function requireManagementTarget(
return lifecycle;
}
-function pendingSetup(
- intent: LocalServiceSetupPending | LocalServiceLegacyHandoff,
-): LocalServiceSetupPending {
- return {
- schemaVersion: 2,
- state: 'setupPending',
- rootPath: intent.rootPath,
- rootId: intent.rootId,
- coordinationRelays: intent.coordinationRelays,
- allowInterruptActiveTasks: intent.allowInterruptActiveTasks,
- };
-}
-
function managedLifecycle(intent: LocalServiceTarget): LocalServiceManaged {
return {
schemaVersion: 2,
@@ -1215,7 +1126,7 @@ function hasManagedServiceTarget(lifecycle: LocalServiceLifecycle): lifecycle is
| LocalServiceManaged
| LocalServicePeerChanging
| LocalServiceUninstalling {
- return lifecycle.state !== 'handoff' && lifecycle.state !== 'setupPending';
+ return lifecycle.state !== 'setupPending';
}
async function readLifecycle(
@@ -1232,55 +1143,13 @@ async function readLifecycle(
}
if (
!isRecord(value) ||
- (value.schemaVersion !== 1 && value.schemaVersion !== 2) ||
+ value.schemaVersion !== 2 ||
value.rootPath !== rootPath ||
value.rootId !== rootId
) {
throw new Error('Local Runtime Host service lifecycle is invalid');
}
- let record: Record<string, unknown> = value;
- if (record.schemaVersion === 1 && record.state === 'handoff') {
- assertExactKeys(record, [
- 'schemaVersion',
- 'state',
- 'rootPath',
- 'rootId',
- 'coordinationRelays',
- 'allowInterruptActiveTasks',
- ]);
- if (typeof record.allowInterruptActiveTasks !== 'boolean') {
- throw new Error('Local Runtime Host setup intent is invalid');
- }
- return {
- schemaVersion: 1,
- state: 'handoff',
- rootPath,
- rootId,
- coordinationRelays: requireAddresses(record.coordinationRelays),
- allowInterruptActiveTasks: record.allowInterruptActiveTasks,
- };
- }
- let migrated = false;
- if (record.schemaVersion === 1) {
- if (record.state === 'setupPending') {
- record = { ...record, schemaVersion: 2 };
- } else {
- if (typeof record.operatorPath !== 'string') {
- throw new Error('Local Runtime Host service receipt is invalid');
- }
- const { operatorPath, ...legacy } = record;
- record = {
- ...legacy,
- schemaVersion: 2,
- operator: createRuntimeHostLegacyPosixOperatorCommand(operatorPath),
- };
- }
- migrated = true;
- }
- const finish = async <T extends LocalServiceLifecycle>(lifecycle: T): Promise<T> => {
- if (migrated) await writeDocument(path, lifecycle);
- return lifecycle;
- };
+ const record = value;
if (record.state === 'setupPending') {
assertExactKeys(record, [
'schemaVersion',
@@ -1293,14 +1162,14 @@ async function readLifecycle(
if (typeof record.allowInterruptActiveTasks !== 'boolean') {
throw new Error('Local Runtime Host setup intent is invalid');
}
- return finish({
+ return {
schemaVersion: 2,
state: record.state,
rootPath,
rootId,
coordinationRelays: requireAddresses(record.coordinationRelays),
allowInterruptActiveTasks: record.allowInterruptActiveTasks,
- });
+ };
}
const target = requireServiceTarget(record, rootPath);
const targetKeys = [
@@ -1336,7 +1205,7 @@ async function readLifecycle(
) {
throw new Error('Local Runtime Host service lifecycle is invalid');
}
- if (record.state === 'managed') return finish({ ...target, state: 'managed' });
+ if (record.state === 'managed') return { ...target, state: 'managed' };
if (typeof record.allowInterruptActiveTasks !== 'boolean') {
throw new Error('Local Runtime Host service intent is invalid');
}
@@ -1344,19 +1213,19 @@ async function readLifecycle(
if (typeof record.peerEnabled !== 'boolean') {
throw new Error('Local Runtime Host peer intent is invalid');
}
- return finish({
+ return {
...target,
state: 'peerChanging',
peerEnabled: record.peerEnabled,
coordinationRelays: requireAddresses(record.coordinationRelays),
allowInterruptActiveTasks: record.allowInterruptActiveTasks,
- });
+ };
}
- return finish({
+ return {
...target,
state: record.state,
allowInterruptActiveTasks: record.allowInterruptActiveTasks,
- });
+ };
}
function assertExactKeys(value: Record<string, unknown>, keys: readonly string[]): void {
diff --git a/apps/desktop/src/main/runtime-host-managed-services.ts b/apps/desktop/src/main/runtime-host-managed-services.ts
index 1c3547d058..28ea264e30 100644
--- a/apps/desktop/src/main/runtime-host-managed-services.ts
+++ b/apps/desktop/src/main/runtime-host-managed-services.ts
@@ -22,7 +22,6 @@ import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import {
decodePersistedRuntimeHostProfile,
- migrateRuntimeHostProfileOperatorCommand,
sameEnvironmentRuntimeHostDeployment,
sameResolvedRuntimeHostProfileTarget,
type EnvironmentRuntimeHostProfile,
@@ -32,7 +31,6 @@ import {
} from "@maka/runtime-host/client";
import { requireHostRootId } from "@maka/runtime-host/protocol";
import {
- createRuntimeHostLegacyPosixOperatorCommand,
decodeRuntimeHostOperatorCommand,
type RuntimeHostOperatorCommand,
} from "@maka/runtime-host/operator";
@@ -47,7 +45,7 @@ const PATH_MAX_BYTES = 4 * 1024;
export interface DesktopRuntimeHostDeploymentBinding {
readonly id: string;
readonly rootPath: string;
- readonly deploymentId?: string;
+ readonly deploymentId: string;
}
type ManagedSshRuntimeHostProfile = RemoteRuntimeHostProfile & {
@@ -153,7 +151,6 @@ export function createDesktopRuntimeHostManagedServiceStore(
): DesktopRuntimeHostManagedServiceStore {
return new FileDesktopRuntimeHostManagedServiceStore(
join(clientDataRoot, "runtime-host-deployments.json"),
- join(clientDataRoot, "runtime-host-managed-services.json"),
);
}
@@ -187,11 +184,9 @@ export function sameDesktopRuntimeHostManagedServiceBinding(
class FileDesktopRuntimeHostManagedServiceStore implements DesktopRuntimeHostManagedServiceStore {
readonly #path: string;
- readonly #legacyPath: string;
- constructor(path: string, legacyPath: string) {
+ constructor(path: string) {
this.#path = path;
- this.#legacyPath = legacyPath;
}
async read(): Promise<DesktopRuntimeHostManagedServiceDocument> {
@@ -203,28 +198,13 @@ class FileDesktopRuntimeHostManagedServiceStore implements DesktopRuntimeHostMan
try {
contents = await readFile(this.#path, "utf8");
} catch (error) {
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
- try {
- contents = await readFile(this.#legacyPath, "utf8");
- } catch (legacyError) {
- if ((legacyError as NodeJS.ErrnoException).code === "ENOENT") return emptyDocument();
- throw legacyError;
- }
- const migrated = decodeLegacyDocument(JSON.parse(contents));
- await writeDocument(this.#path, migrated);
- await removeLegacyDocument(this.#legacyPath);
- return migrated;
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return emptyDocument();
+ throw error;
}
if (Buffer.byteLength(contents, "utf8") > DOCUMENT_MAX_BYTES) {
throw new Error("Runtime Host managed service document is too large");
}
- const value: unknown = JSON.parse(contents);
- const migrated = decodeDocument(value);
- if ((value as { readonly schemaVersion?: unknown }).schemaVersion === 1) {
- await writeDocument(this.#path, migrated);
- }
- await removeLegacyDocument(this.#legacyPath);
- return migrated;
+ return decodeDocument(JSON.parse(contents));
}
save(
@@ -390,9 +370,8 @@ function decodeDocument(
if (!Array.isArray(record.bindings)) {
throw new Error("Runtime Host managed service document is invalid");
}
- if (record.schemaVersion === 1) return migrateVersionOneDocument(record.bindings);
if (record.schemaVersion !== SCHEMA_VERSION) {
- throw new Error("Runtime Host managed service document is invalid");
+ throw new Error("Runtime Host managed service document schema is unsupported");
}
if (record.bindings.length > BINDING_COUNT_MAX) {
throw new Error(
@@ -470,98 +449,16 @@ function decodeDocument(
});
}
-function migrateVersionOneDocument(bindings: unknown[]): DesktopRuntimeHostManagedServiceDocument {
- return decodeDocument({
- schemaVersion: SCHEMA_VERSION,
- bindings: bindings.map((candidate) => {
- const profile = migrateRuntimeHostProfileOperatorCommand(
- (candidate as { readonly profile?: unknown } | null)?.profile,
- );
- const decodedProfile = decodePersistedRuntimeHostProfile(profile);
- const binding = requireExactRecord(
- candidate,
- "Runtime Host managed service binding",
- decodedProfile.kind === "environment"
- ? ["deployment", "profile", "state"]
- : ["control", "deployment", "profile", "state"],
- );
- if (decodedProfile.kind === "environment") return { ...binding, profile };
- const control = requireExactRecord(binding.control, "Managed Runtime Host control route", [
- "kind",
- "operatorPath",
- ]);
- if (control.kind !== "ssh_operator") {
- throw new Error("Managed Runtime Host control route is invalid");
- }
- return {
- ...binding,
- profile,
- control: {
- kind: "ssh_operator",
- operator: createRuntimeHostLegacyPosixOperatorCommand(
- requirePosixOperatorPath(control.operatorPath),
- ),
- },
- };
- }),
- });
-}
-
-function decodeLegacyDocument(value: unknown): DesktopRuntimeHostManagedServiceDocument {
- const record = requireExactRecord(value, "Legacy Runtime Host managed service document", [
- "schemaVersion",
- "bindings",
- ]);
- if (record.schemaVersion !== 1 || !Array.isArray(record.bindings)) {
- throw new Error("Legacy Runtime Host managed service document is invalid");
- }
- return decodeDocument({
- schemaVersion: SCHEMA_VERSION,
- bindings: record.bindings.map((candidate) => {
- const binding = requireExactRecord(candidate, "Legacy Runtime Host service binding", [
- "profile",
- "service",
- "state",
- ]);
- const service = requireExactRecord(binding.service, "Managed Runtime Host service", [
- "id",
- "operatorPath",
- "rootPath",
- ]);
- const operatorPath = requirePosixOperatorPath(service.operatorPath);
- return {
- profile: migrateRuntimeHostProfileOperatorCommand(binding.profile),
- deployment: {
- id: requireHostRootId(service.id),
- rootPath: requirePath(service.rootPath, "Managed Runtime Host State Root"),
- },
- control: {
- kind: "ssh_operator",
- operator: createRuntimeHostLegacyPosixOperatorCommand(operatorPath),
- },
- state: binding.state,
- };
- }),
- });
-}
-
function decodeDeployment(value: unknown): DesktopRuntimeHostDeploymentBinding {
- const hasDeploymentId =
- value !== null &&
- typeof value === "object" &&
- !Array.isArray(value) &&
- Object.hasOwn(value, "deploymentId");
const record = requireExactRecord(
value,
"Managed Runtime Host deployment",
- hasDeploymentId ? ["deploymentId", "id", "rootPath"] : ["id", "rootPath"],
+ ["deploymentId", "id", "rootPath"],
);
return Object.freeze({
id: requireHostRootId(record.id),
rootPath: requirePath(record.rootPath, "Managed Runtime Host State Root"),
- ...(record.deploymentId === undefined
- ? {}
- : { deploymentId: requireDeploymentId(record.deploymentId) }),
+ deploymentId: requireDeploymentId(record.deploymentId),
});
}
@@ -624,14 +521,6 @@ function requirePath(value: unknown, label: string): string {
return value;
}
-function requirePosixOperatorPath(value: unknown): string {
- const path = requirePath(value, "Managed Runtime Host operator path");
- if (!path.startsWith("/")) {
- throw new Error("Managed Runtime Host operator path must be absolute");
- }
- return path;
-}
-
function requireExactRecord(
value: unknown,
label: string,
@@ -714,13 +603,3 @@ async function writeDocument(
await rm(temporaryPath, { force: true });
}
}
-
-async function removeLegacyDocument(path: string): Promise<void> {
- try {
- await rm(path);
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
- throw error;
- }
- await syncDirectory(dirname(path));
-}
diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts
index 7579613490..aac49c8b09 100644
--- a/apps/desktop/src/main/runtime-host-management.ts
+++ b/apps/desktop/src/main/runtime-host-management.ts
@@ -24,7 +24,6 @@ import {
} from '@maka/runtime-host/client';
import {
RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY,
- RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY,
RUNTIME_HOST_OPERATOR_PEER_WEBRTC_STUN_CAPABILITY,
decodeRuntimeHostWebRtcStunPolicy,
isProductReleaseVersion,
@@ -197,21 +196,11 @@ export function createDesktopRuntimeHostManagement(input: {
if (managed.state !== 'active' && managementAction !== 'uninstall') {
throw new Error('Finish uninstalling this Runtime Host service before managing it');
}
- if (
- managementAction !== 'status' &&
- managementAction !== 'logs' &&
- !deployment.deploymentId &&
- !(managementAction === 'uninstall' && managed.state !== 'active')
- ) {
- throw new Error(
- 'Re-onboard this Runtime Host before changing it; its legacy binding has no deployment generation',
- );
- }
const expectedTarget = {
serviceId: deployment.id,
rootPath: deployment.rootPath,
rootId: managed.profile.rootId,
- ...(deployment.deploymentId ? { deploymentId: deployment.deploymentId } : {}),
+ deploymentId: deployment.deploymentId,
};
if (!isDesktopRuntimeHostManagedSshServiceBinding(managed)) {
if (managementAction !== 'status') {
@@ -375,11 +364,6 @@ export function createDesktopRuntimeHostManagement(input: {
if (managed.state !== 'active') {
throw new Error('This Runtime Host profile is not available for managed service changes');
}
- if (!managed.deployment.deploymentId) {
- throw new Error(
- 'Re-onboard this Runtime Host before changing it; its legacy binding has no deployment generation',
- );
- }
return {
profileId,
managed,
@@ -461,29 +445,7 @@ export function createDesktopRuntimeHostManagement(input: {
const webRtcStunAvailable = adaptiveCapability.operatorCapabilities?.includes(
RUNTIME_HOST_OPERATOR_PEER_WEBRTC_STUN_CAPABILITY,
) === true;
- const legacyCapability = webRtcStunAvailable
- ? adaptiveCapability
- : await input.runServiceManagement({
- destination: target.transport.destination,
- ...(target.transport.sshPort === undefined
- ? {}
- : { sshPort: target.transport.sshPort }),
- operator: target.managed.control.operator,
- action: 'status',
- expectedTarget: target.expectedTarget,
- capabilityRequest: RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY,
- });
- if (legacyCapability.kind === 'error') throw new Error(legacyCapability.error.message);
- if (legacyCapability.action !== 'status') {
- throw new Error('Runtime Host returned an unrelated capability result');
- }
- return {
- ...target,
- available: webRtcStunAvailable || legacyCapability.operatorCapabilities?.includes(
- RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY,
- ) === true,
- webRtcStunAvailable,
- };
+ return { ...target, available: webRtcStunAvailable };
};
const unavailablePeerSnapshot = async (
@@ -505,7 +467,7 @@ export function createDesktopRuntimeHostManagement(input: {
const getDirectPeer = async (
profileIdValue: unknown,
): Promise<DesktopRuntimeHostDirectPeerSnapshot> => {
- const { profileId, managed, transport, expectedTarget, available, webRtcStunAvailable } =
+ const { profileId, managed, transport, expectedTarget, available } =
await peerManagementTarget(profileIdValue);
if (!available) return unavailablePeerSnapshot(profileId);
const response = await input.runPeerManagement({
@@ -513,7 +475,7 @@ export function createDesktopRuntimeHostManagement(input: {
...(transport.sshPort === undefined ? {} : { sshPort: transport.sshPort }),
operator: managed.control.operator,
action: 'status',
- ...(webRtcStunAvailable ? { webRtcStunStatus: true } : {}),
+ webRtcStunStatus: true,
expectedTarget,
});
if (response.kind !== 'result') {
@@ -549,15 +511,11 @@ export function createDesktopRuntimeHostManagement(input: {
transport,
expectedTarget,
available,
- webRtcStunAvailable,
} =
await peerManagementTarget(profileIdValue);
if (!available) {
throw new Error('Update this Runtime Host before managing Direct peer access');
}
- if (webRtcStunPolicy && !webRtcStunAvailable) {
- throw new Error('Update this Runtime Host before changing its STUN policy');
- }
const peerProfile = await input.profiles.resolveManagedDirectPeerProfile(profileId);
if (peerProfile.enabled) {
throw new Error('Disable the Direct peer profile before changing its listener');
@@ -571,7 +529,7 @@ export function createDesktopRuntimeHostManagement(input: {
...(enabledValue ? { coordinationRelays } : {}),
...(enabledValue ? { automaticRelayDiscovery: automaticRelayDiscoveryValue } : {}),
...(enabledValue && webRtcStunPolicy ? { webRtcStunPolicy } : {}),
- ...(webRtcStunAvailable ? { webRtcStunStatus: true } : {}),
+ webRtcStunStatus: true,
expectedTarget,
});
if (response.kind !== 'result') {
diff --git a/apps/desktop/src/main/runtime-host-native-capabilities.ts b/apps/desktop/src/main/runtime-host-native-capabilities.ts
index fdaf0fe129..c93e945788 100644
--- a/apps/desktop/src/main/runtime-host-native-capabilities.ts
+++ b/apps/desktop/src/main/runtime-host-native-capabilities.ts
@@ -19,7 +19,7 @@
import { Buffer } from "node:buffer";
import type { ComputerUseToolSet } from '@maka/runtime/computer-use-tools';
-import type { MakaTool } from '@maka/runtime/tool-runtime';
+import type { MakaTool, SessionTool, SessionToolContext } from '@maka/runtime/tool-runtime';
import {
createOAuthPresentationClientProvider,
type ClientCapabilityProvider,
@@ -60,11 +60,17 @@ export interface DesktopIdentifiedCapabilityTool {
readonly toolName: string;
}
+/** Declares that this implementation needs a Session, not an Agent Turn. */
+export interface DesktopSessionCapabilityTool {
+ readonly context: 'session';
+ readonly tool: SessionTool;
+}
+
export interface DesktopCapabilityGroup {
readonly offerId: string;
readonly label: string;
readonly description: string;
- readonly tools: readonly (MakaTool | DesktopIdentifiedCapabilityTool)[];
+ readonly tools: readonly (MakaTool | DesktopIdentifiedCapabilityTool | DesktopSessionCapabilityTool)[];
/**
* Marks a dynamically sourced group: tools the decoder rejects are omitted
* with a diagnostic, the group may be chunked past the single-offer tool
@@ -74,10 +80,9 @@ export interface DesktopCapabilityGroup {
readonly dynamic?: boolean;
}
-interface PreparedDesktopCapabilityTool {
- readonly tool: MakaTool;
+type PreparedDesktopCapabilityTool = NativeToolBinding & {
readonly descriptor: ClientCapabilityToolDescriptor;
-}
+};
interface PreparedDesktopCapabilityGroup {
readonly offerId: string;
@@ -87,7 +92,9 @@ interface PreparedDesktopCapabilityGroup {
readonly dynamic?: boolean;
}
-type NativeToolBinding = Pick<PreparedDesktopCapabilityTool, "tool">;
+type NativeToolBinding =
+ | { readonly kind: 'session'; readonly tool: SessionTool }
+ | { readonly kind: 'agent'; readonly tool: MakaTool };
type DesktopToolModelOutput = Awaited<
ReturnType<NonNullable<MakaTool["toModelOutput"]>>
@@ -98,7 +105,7 @@ type DesktopToolContentPart = Extract<
>["value"][number];
export interface DesktopNativeCapabilityProviderInput {
- readonly browserTools: readonly MakaTool[];
+ readonly browserTools: readonly SessionTool[];
readonly resolveBrowserUrl: (input: {
readonly sessionId: string;
readonly toolName: string;
@@ -211,6 +218,7 @@ export function createDesktopNativeCapabilityProvider(
offers: () => offers,
services: () => [...serviceOffers],
call: (frame, options) => {
+ if (frame.source.sessionId === null) throw new Error('This native tool requires a Session');
if (closed)
throw new Error("Desktop native capability provider is closed");
if (providerOptions.isTargetValid?.() === false) {
@@ -234,7 +242,7 @@ export function createDesktopNativeCapabilityProvider(
() => undefined,
);
activeInvocations.set(invocation, {
- sessionId: frame.sessionId,
+ sessionId: frame.source.sessionId,
settled,
});
void settled.finally(() => activeInvocations.delete(invocation));
@@ -356,7 +364,7 @@ function capabilityGroups(
label: "Browser",
description:
"Operate the embedded browser owned by this Desktop client.",
- tools: input.browserTools,
+ tools: input.browserTools.map(tool => ({ context: 'session' as const, tool })),
},
]
: []),
@@ -384,6 +392,11 @@ async function invokeNativeTool(
invocation: AbortController,
usedSessionIds: Set<string>,
): Promise<ClientCapabilityCallResult> {
+ const source = frame.source;
+ if (source.sessionId === null) throw new Error('This native tool requires a Session');
+ if (binding.kind === 'agent' && source.kind !== 'agent') {
+ throw new Error('This tool requires an Agent invocation');
+ }
const hostPathAccess = providerOptions.hostPathAccess ?? "cwd";
if (hostPathAccess === "none" && frame.cwd !== undefined) {
throw new Error("Desktop native capability does not accept a Host path");
@@ -400,8 +413,8 @@ async function invokeNativeTool(
signal.throwIfAborted();
const sessionId =
frame.offerId === BROWSER_OFFER_ID || frame.offerId === COMPUTER_USE_OFFER_ID
- ? providerOptions.nativeSessionId?.(frame.sessionId) ?? frame.sessionId
- : frame.sessionId;
+ ? providerOptions.nativeSessionId?.(source.sessionId) ?? source.sessionId
+ : source.sessionId;
const admissionEvidence =
frame.offerId === BROWSER_OFFER_ID
? {
@@ -429,22 +442,25 @@ async function invokeNativeTool(
}
}
signal.throwIfAborted();
- usedSessionIds.add(frame.sessionId);
- providerOptions.onSessionUsed?.(frame.sessionId);
- if (frame.offerId === COMPUTER_USE_OFFER_ID || frame.offerId === "desktop_workhub") {
- providerOptions.onDesktopInteractionTurnUsed?.(frame.sessionId, frame.turnId);
+ usedSessionIds.add(source.sessionId);
+ providerOptions.onSessionUsed?.(source.sessionId);
+ if (source.kind === 'agent' && (frame.offerId === COMPUTER_USE_OFFER_ID || frame.offerId === "desktop_workhub")) {
+ providerOptions.onDesktopInteractionTurnUsed?.(source.sessionId, source.turnId);
}
- const execute = () =>
- binding.tool.impl(args, {
+ const context: SessionToolContext = {
sessionId,
- turnId: frame.turnId,
cwd,
toolCallId: frame.toolCallId,
abortSignal: signal,
emitOutput() {},
requestUserForm: options.requestInteraction,
...(options.progress ? { emitProgress: options.progress } : {}),
- });
+ };
+ const execute = () => {
+ if (binding.kind === 'session') return binding.tool.impl(args, context);
+ if (source.kind !== 'agent') throw new Error('This tool requires an Agent invocation');
+ return binding.tool.impl(args, { ...context, turnId: source.turnId });
+ };
const output = await (admissionEvidence.kind === "browser_url"
? withBrowserOriginAdmission(
{ sessionId, url: admissionEvidence.url },
@@ -501,7 +517,10 @@ function prepareCapabilityGroups(
): PreparedDesktopCapabilityGroup[] {
return groups.flatMap((group) => {
const tools = group.tools.flatMap((entry): PreparedDesktopCapabilityTool[] => {
- const tool = isIdentifiedEntry(entry) ? entry.tool : entry;
+ const tool = 'tool' in entry ? entry.tool : entry;
+ const binding: NativeToolBinding = 'context' in entry
+ ? { kind: 'session', tool: entry.tool }
+ : { kind: 'agent', tool };
const identity = isIdentifiedEntry(entry)
? { serverId: entry.serverId, toolName: entry.toolName }
: undefined;
@@ -521,7 +540,7 @@ function prepareCapabilityGroups(
);
return [];
}
- return [{ tool, descriptor }];
+ return [{ ...binding, descriptor }];
});
if (group.dynamic && tools.length === 0) return [];
return chunkPreparedGroup({ ...group, tools });
@@ -628,9 +647,9 @@ function manifestFitsBudget(
}
function isIdentifiedEntry(
- entry: MakaTool | DesktopIdentifiedCapabilityTool,
+ entry: MakaTool | DesktopIdentifiedCapabilityTool | DesktopSessionCapabilityTool,
): entry is DesktopIdentifiedCapabilityTool {
- return 'tool' in entry;
+ return 'serverId' in entry && 'toolName' in entry;
}
function capabilityToolDescriptor(
@@ -701,7 +720,8 @@ function indexBindings(
): Map<string, NativeToolBinding> {
const bindings = new Map<string, NativeToolBinding>();
for (const group of groups) {
- for (const { tool, descriptor } of group.tools) {
+ for (const binding of group.tools) {
+ const { tool, descriptor } = binding;
const key = bindingKey({
offerId: group.offerId,
serverId: descriptor.serverId,
@@ -712,7 +732,7 @@ function indexBindings(
`Duplicate Desktop native capability tool: ${group.offerId}/${tool.name}`,
);
}
- bindings.set(key, { tool });
+ bindings.set(key, binding);
}
}
return bindings;
diff --git a/apps/desktop/src/main/runtime-host-onboarding.ts b/apps/desktop/src/main/runtime-host-onboarding.ts
index 27ed4b0787..68177125bc 100644
--- a/apps/desktop/src/main/runtime-host-onboarding.ts
+++ b/apps/desktop/src/main/runtime-host-onboarding.ts
@@ -19,28 +19,12 @@
import { randomUUID } from 'node:crypto';
import type { IpcMain } from 'electron';
-import {
- parseRuntimeHostSetupEndpoint,
- type RuntimeHostNodeOperatorCommand,
- type RuntimeHostNativeOperatorCommand,
- type RuntimeHostOperatorCommand,
- type RuntimeHostSetupPhase,
-} from '@maka/runtime-host/operator';
import type {
DesktopRuntimeHostOnboardingInput,
DesktopRuntimeHostOnboardingSnapshot,
} from '../preload/bridge-contract.js';
import type { DesktopRuntimeHostProfileService } from './runtime-host-profile-service.js';
-import {
- runtimeHostPeerTargetFromPlatform,
- type RuntimeHostTargetIdentity,
- type DesktopRuntimeHostSshSetupInput,
-} from './runtime-host-ssh-terminal.js';
-import type { DesktopRuntimeHostWslSetupInput } from './runtime-host-wsl-controller.js';
-import type {
- DesktopRuntimeHostDevelopmentPeerTarget,
- DesktopRuntimeHostSetupPackage,
-} from './runtime-host-setup-package.js';
+import type { RuntimeHostTargetIdentity } from './runtime-host-target.js';
import { requireProjectDirectoryRoots } from '../shared/runtime-host-project-directory-policy.js';
import type { NativeRuntimeHostPackage } from './native-runtime-host-setup.js';
import type { NativeSetupInput, NativeSetupResult } from './native-runtime-host-installer.js';
@@ -53,8 +37,7 @@ type OnboardingState = DesktopRuntimeHostOnboardingSnapshot extends infer Snapsh
: never;
export function createDesktopRuntimeHostOnboarding(input: {
- /** Production uses native packages; the legacy route remains for TS fixtures. */
- readonly nativeSetup?: {
+ readonly nativeSetup: {
resolvePackage(identity: RuntimeHostTargetIdentity, signal?: AbortSignal, budget?: NativeHostBudget, onProgress?: (progress: import('../shared/native-runtime-host-management.js').NativeHostProgress) => void): Promise<NativeRuntimeHostPackage>;
ssh(input: NativeSetupInput & { readonly destination: string; readonly sshPort?: number }, onCommit: () => void): Promise<NativeSetupResult>;
wsl(input: NativeSetupInput & { readonly distribution: string }, onCommit: () => void): Promise<NativeSetupResult>;
@@ -65,46 +48,17 @@ export function createDesktopRuntimeHostOnboarding(input: {
DesktopRuntimeHostProfileService,
'addEnvironmentAndEnable' | 'addAndEnableVerified'
>;
- readonly runSetup: (
- input: DesktopRuntimeHostSshSetupInput,
- onProgress: (frame: { readonly phase: RuntimeHostSetupPhase }) => void,
- onComplete: () => void,
- ) => Promise<{
- readonly rootId: string;
- readonly rootPath: string;
- readonly serviceId: string;
- readonly deploymentId: string;
- readonly operator: RuntimeHostOperatorCommand;
- readonly endpoint: string;
- readonly credential: string;
- }>;
- readonly runWslSetup: (
- input: DesktopRuntimeHostWslSetupInput,
- onProgress: (frame: { readonly phase: RuntimeHostSetupPhase }) => void,
- onComplete: () => void,
- ) => Promise<{
- readonly rootId: string;
- readonly rootPath: string;
- readonly serviceId: string;
- readonly deploymentId: string;
- readonly operator: RuntimeHostNodeOperatorCommand<'posix'> | RuntimeHostNativeOperatorCommand<'posix'>;
- }>;
readonly listWslDistributions: () => Promise<readonly string[]>;
readonly resolveWslTargetIdentity: (input: {
readonly distribution: string;
readonly signal?: AbortSignal;
}) => Promise<Extract<RuntimeHostTargetIdentity, { platform: 'linux' }>>;
readonly send: (snapshot: DesktopRuntimeHostOnboardingSnapshot) => void;
- readonly setupPackageMode: 'published' | 'development';
readonly resolveSshTargetIdentity: (input: {
readonly destination: string;
readonly sshPort?: number;
readonly signal?: AbortSignal;
}) => Promise<RuntimeHostTargetIdentity>;
- readonly resolveSetupPackage: (
- peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
- signal?: AbortSignal,
- ) => DesktopRuntimeHostSetupPackage | Promise<DesktopRuntimeHostSetupPackage>;
}): { close(): Promise<void> } {
let revision = 0;
let snapshot: DesktopRuntimeHostOnboardingSnapshot = { kind: 'idle', revision };
@@ -150,107 +104,8 @@ export function createDesktopRuntimeHostOnboarding(input: {
signal: AbortSignal,
): Promise<DesktopRuntimeHostOnboardingSnapshot> => {
try {
- if (input.nativeSetup) {
- const budget = new NativeHostBudget(180_000, signal);
- return await budget.wait(runNative(request, signal, input.nativeSetup, budget), 'Host setup');
- }
- if (request.kind === 'wsl') {
- publish({ kind: 'running', phase: 'connecting_wsl' });
- const target = await input.resolveWslTargetIdentity({ distribution: request.distribution, signal });
- publish({ kind: 'running', phase: 'preparing_cli' });
- const peerTarget = input.setupPackageMode === 'development'
- ? runtimeHostPeerTargetFromPlatform(target.platform, target.architecture) : 'none';
- const setupPackage = await input.resolveSetupPackage(peerTarget, signal);
- return await runWsl(request, setupPackage, signal);
- }
- const targetIdentity = await resolveSshTargetIdentity(request, signal);
- const peerTarget = input.setupPackageMode === 'development'
- ? runtimeHostPeerTargetFromPlatform(targetIdentity.platform, targetIdentity.architecture)
- : 'none';
- const setupPackage = await input.resolveSetupPackage(
- peerTarget,
- signal,
- );
- const lifecycle = setupPackage.kind === 'npm' ? 'on_demand' : 'supervised';
- signal.throwIfAborted();
- publish({ kind: 'running', phase: 'connecting_ssh' });
- let commitStarted = false;
- const beginCommit = () => {
- if (commitStarted) return;
- commitStarted = true;
- if (active) active.cancellable = false;
- publish({
- kind: 'running',
- phase: 'connecting_host',
- });
- };
- const complete = await input.runSetup(
- {
- destination: request.destination,
- ...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
- setupPackage,
- remotePlatform: targetIdentity.platform === 'win32' ? 'win32' : 'posix',
- lifecycle,
- principalId: `desktop:${input.clientInstanceId}`,
- ...(request.projectDirectoryRoots
- ? { projectDirectoryRoots: request.projectDirectoryRoots }
- : {}),
- signal,
- },
- (progress) => {
- if (commitStarted) return;
- publish({
- kind: 'running',
- phase: progress.phase,
- });
- },
- beginCommit,
- );
- beginCommit();
- const endpoint = parseRuntimeHostSetupEndpoint(complete.endpoint);
- if (!endpoint) throw new Error('Remote Maka setup returned an invalid endpoint');
- const profileId = `remote-${randomUUID()}`;
- const profileName = request.name?.trim() || request.destination;
- const connected = await input.profiles.addAndEnableVerified({
- profile: {
- id: profileId,
- name: profileName,
- kind: 'remote',
- rootId: complete.rootId,
- transport: {
- kind: 'ssh',
- destination: request.destination,
- ...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
- ...(lifecycle === 'on_demand'
- ? {
- activation: {
- kind: 'ssh_operator' as const,
- operator: complete.operator,
- },
- }
- : {
- remotePort: endpoint.port,
- websocketPath: endpoint.websocketPath,
- }),
- },
- },
- credential: complete.credential,
- managedService: {
- deployment: {
- id: complete.serviceId,
- rootPath: complete.rootPath,
- deploymentId: complete.deploymentId,
- },
- control: {
- kind: 'ssh_operator',
- operator: complete.operator,
- },
- },
- });
- return publish({
- kind: 'complete',
- profileId: connected.profileId,
- });
+ const budget = new NativeHostBudget(180_000, signal);
+ return await budget.wait(runNative(request, signal, input.nativeSetup, budget), 'Host setup');
} catch (error) {
if (signal.aborted) return publish({ kind: 'idle' });
return publish({
@@ -263,7 +118,7 @@ export function createDesktopRuntimeHostOnboarding(input: {
const runNative = async (
request: DesktopRuntimeHostOnboardingInput,
signal: AbortSignal,
- native: NonNullable<typeof input.nativeSetup>,
+ native: typeof input.nativeSetup,
budget: NativeHostBudget,
): Promise<DesktopRuntimeHostOnboardingSnapshot> => {
publish({ kind: 'running', phase: request.kind === 'wsl' ? 'connecting_wsl' : 'connecting_ssh' });
@@ -316,71 +171,6 @@ export function createDesktopRuntimeHostOnboarding(input: {
return publish({ kind: 'complete', profileId: result.profileId });
};
- const resolveSshTargetIdentity = async (
- request: Extract<DesktopRuntimeHostOnboardingInput, { readonly kind: 'ssh' }>,
- signal: AbortSignal,
- ): Promise<RuntimeHostTargetIdentity> => {
- publish({ kind: 'running', phase: 'connecting_ssh' });
- const identity = await input.resolveSshTargetIdentity({
- destination: request.destination,
- ...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
- signal,
- });
- publish({ kind: 'running', phase: 'preparing_cli' });
- return identity;
- };
-
- const runWsl = async (
- request: Extract<DesktopRuntimeHostOnboardingInput, { readonly kind: 'wsl' }>,
- setupPackage: DesktopRuntimeHostSetupPackage,
- signal: AbortSignal,
- ): Promise<DesktopRuntimeHostOnboardingSnapshot> => {
- publish({ kind: 'running', phase: 'connecting_wsl' });
- let commitStarted = false;
- const beginCommit = () => {
- if (commitStarted) return;
- commitStarted = true;
- if (active) active.cancellable = false;
- publish({ kind: 'running', phase: 'connecting_host' });
- };
- const complete = await input.runWslSetup(
- {
- distribution: request.distribution,
- setupPackage,
- principalId: `desktop:${input.clientInstanceId}`,
- ...(request.projectDirectoryRoots
- ? { projectDirectoryRoots: request.projectDirectoryRoots }
- : {}),
- signal,
- },
- (progress) => {
- if (!commitStarted) publish({ kind: 'running', phase: progress.phase });
- },
- beginCommit,
- );
- beginCommit();
- const profileId = `environment-${randomUUID()}`;
- const profile = {
- id: profileId,
- name: request.name?.trim() || request.distribution,
- kind: 'environment' as const,
- provider: { kind: 'wsl' as const, distribution: request.distribution },
- rootId: complete.rootId,
- operator: complete.operator,
- };
- const connected = await input.profiles.addEnvironmentAndEnable({
- profile,
- managedService: {
- deployment: {
- id: complete.serviceId,
- rootPath: complete.rootPath,
- deploymentId: complete.deploymentId,
- },
- },
- });
- return publish({ kind: 'complete', profileId: connected.profileId });
- };
-
const channels = [
'runtime-host-onboarding:getSnapshot',
'runtime-host-onboarding:start',
diff --git a/apps/desktop/src/main/runtime-host-pairing-journal.ts b/apps/desktop/src/main/runtime-host-pairing-journal.ts
index d5868e4eda..38169122e3 100644
--- a/apps/desktop/src/main/runtime-host-pairing-journal.ts
+++ b/apps/desktop/src/main/runtime-host-pairing-journal.ts
@@ -19,7 +19,6 @@
import {
decodeRemoteRuntimeHostProfile,
- migrateRuntimeHostProfileOperatorCommand,
RUNTIME_HOST_ACCESS_CREDENTIAL_MAX_BYTES,
sameResolvedRuntimeHostProfileTarget,
type RemoteRuntimeHostProfile,
@@ -187,9 +186,7 @@ function decodePairingTarget(
): DesktopRuntimeHostPairingIntent['target'] {
const record = requireExactRecord(value, ['profile', 'credential']);
return {
- profile: decodeRemoteRuntimeHostProfile(
- migrateRuntimeHostProfileOperatorCommand(record.profile),
- ),
+ profile: decodeRemoteRuntimeHostProfile(record.profile),
credential: requireCredential(record.credential),
};
}
diff --git a/apps/desktop/src/main/runtime-host-profile-service.ts b/apps/desktop/src/main/runtime-host-profile-service.ts
index 7926c2112f..9d3a62ea72 100644
--- a/apps/desktop/src/main/runtime-host-profile-service.ts
+++ b/apps/desktop/src/main/runtime-host-profile-service.ts
@@ -18,7 +18,7 @@
*/
import { createHash, randomUUID } from "node:crypto";
-import { lstat, open, readFile, rename, rm, rmdir } from "node:fs/promises";
+import { open, readFile, rename, rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import { NativeHostBudget } from './native-runtime-host-operation.js';
import {
@@ -44,7 +44,7 @@ import {
import { runtimeHostAccessCredentialFingerprint } from "@maka/runtime-host/operator";
import type { HostPeerEndpoint } from '@maka/runtime-host/protocol';
import type { CredentialStore } from "@maka/storage/credential-store";
-import { withFileUpdateLock } from "@maka/storage/file-update-lock";
+import { withProcessLifetimeFileUpdateLock } from "@maka/storage/process-lifetime-file-update-lock";
import type {
DesktopRuntimeHostProfileAddInput,
DesktopRuntimeHostProfileAddResult,
@@ -170,12 +170,6 @@ export async function resolveDesktopRuntimeHostStartup(
readPreferences?: () => Promise<DesktopRuntimeHostPreferences>;
} = {},
): Promise<DesktopRuntimeHostStartup> {
- // The Desktop single-instance lock is held before startup opens any stores.
- // Reclaim only legacy directory markers here, before concurrent readers can
- // start; current deployment writers use a process-lifetime OS lease.
- await recoverAbandonedDesktopFileUpdateLock(
- join(clientDataRoot, "runtime-host-deployments.json"),
- );
const preferencesPath = join(clientDataRoot, PREFERENCES_FILE);
let preferences: DesktopRuntimeHostPreferences;
let preferencesReadFailure: Error | undefined;
@@ -227,25 +221,6 @@ export async function resolveDesktopRuntimeHostStartup(
unavailable,
};
}
- const obsoleteProfileIds = new Set(
- document.profiles.flatMap((profile) =>
- profile.kind === 'remote' && profile.access === 'session_guest' ? [profile.id] : [],
- ),
- );
- if (obsoleteProfileIds.size > 0) {
- await recoverAbandonedDesktopFileUpdateLock(join(clientDataRoot, PROFILE_FILE));
- }
- for (const profileId of obsoleteProfileIds) await catalog.remove(profileId);
- if (obsoleteProfileIds.size > 0) document = await catalog.read();
- if (!pairingReadFailure) {
- const retained = pairingIntents.filter(
- (intent) => intent.target.profile.access !== 'session_guest',
- );
- if (retained.length !== pairingIntents.length) {
- await writeDesktopRuntimeHostPairingIntents(credentialStore, retained);
- pairingIntents = retained;
- }
- }
const profileIds = new Set(document.profiles.map((profile) => profile.id));
const defaultProfile = document.profiles.find(
(profile) => profile.id === preferences.defaultProfileId,
@@ -354,7 +329,6 @@ export function createDesktopRuntimeHostProfileService(input: {
const mutateProfiles = <T>(operation: () => Promise<T>): Promise<T> =>
mutate(async () => {
- await recoverAbandonedDesktopFileUpdateLock(profilePath);
assertPreferencesWritable();
if (pairingReadFailure) {
throw new Error(
@@ -429,7 +403,7 @@ export function createDesktopRuntimeHostProfileService(input: {
};
const persist = async (next: DesktopRuntimeHostPreferences): Promise<void> => {
- await withFileUpdateLock(profilePath, () =>
+ await withProcessLifetimeFileUpdateLock(profilePath, () =>
writeRuntimeHostPreferences(preferencesPath, next),
);
preferences = next;
@@ -1128,14 +1102,6 @@ export function createDesktopRuntimeHostProfileService(input: {
},
markManagedServiceUninstalling(expected) {
return mutateProfiles(async () => {
- if (
- !expected.deployment.deploymentId &&
- expected.state !== 'uninstalling'
- ) {
- throw new Error(
- 'Re-onboard this Runtime Host before uninstalling it; its legacy binding has no deployment generation',
- );
- }
requirePairingComplete(expected.profile.id);
const document = await catalog.read();
const current = document.profiles.find(
@@ -1488,27 +1454,6 @@ function assertRootIsNotEnabled(
}
}
-async function recoverAbandonedDesktopFileUpdateLock(targetPath: string): Promise<void> {
- const lockPath = `${targetPath}.lock`;
- const lock = await lstat(lockPath).catch((error: unknown) => {
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
- throw error;
- });
- if (
- !lock ||
- !lock.isDirectory() ||
- lock.isSymbolicLink()
- ) {
- return;
- }
- // Electron's single-instance authority excludes another Desktop writer for
- // this client data root. Legacy directory locks contain no owner identity,
- // so only reclaim an old, empty marker; unexpected contents still fail loud.
- await rmdir(lockPath).catch((error: unknown) => {
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
- });
-}
-
async function persistIfCurrentTarget(
catalog: RuntimeHostProfileCatalog,
profilePath: string,
@@ -1516,7 +1461,7 @@ async function persistIfCurrentTarget(
target: ResolvedRuntimeHostProfile,
preferences: DesktopRuntimeHostPreferences,
): Promise<void> {
- await withFileUpdateLock(profilePath, async () => {
+ await withProcessLifetimeFileUpdateLock(profilePath, async () => {
const current = await catalog.resolve(target.profile.id);
if (!sameResolvedRuntimeHostProfileTarget(current, target)) {
throw new Error("Runtime Host profile changed while it was being enabled");
@@ -1637,37 +1582,14 @@ async function readRuntimeHostPreferences(path: string): Promise<DesktopRuntimeH
value = JSON.parse(await readFile(path, "utf8"));
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return defaultPreferences();
- if (!(error instanceof SyntaxError)) throw error;
- console.error("[runtime-host] preferences are invalid; using Local defaults");
- return defaultPreferences();
- }
- if (isLegacySelection(value)) {
- const migrated = {
- schemaVersion: PREFERENCES_SCHEMA_VERSION,
- defaultProfileId: value.profileId,
- enabledRemoteProfileIds:
- value.profileId === LOCAL_RUNTIME_HOST_PROFILE.id ? [] : [value.profileId],
- } as const;
- await writeRuntimeHostPreferences(path, migrated);
- return migrated;
+ throw error;
}
if (!isRuntimeHostPreferences(value)) {
- console.error("[runtime-host] preferences are invalid; using Local defaults");
- return defaultPreferences();
+ throw new Error("Runtime Host preferences format is invalid or unsupported");
}
return value;
}
-function isLegacySelection(value: unknown): value is { schemaVersion: 1; profileId: string } {
- return Boolean(
- value &&
- typeof value === "object" &&
- !Array.isArray(value) &&
- (value as { schemaVersion?: unknown }).schemaVersion === 1 &&
- typeof (value as { profileId?: unknown }).profileId === "string",
- );
-}
-
function isRuntimeHostPreferences(value: unknown): value is DesktopRuntimeHostPreferences {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const input = value as Partial<DesktopRuntimeHostPreferences>;
diff --git a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
index 8f9ff5ff60..c0a6c0fba3 100644
--- a/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
+++ b/apps/desktop/src/main/runtime-host-session-execution-ipc-main.ts
@@ -129,7 +129,6 @@ type RuntimeHostSessionExecutionClient = Pick<
>;
/** No Skill was named, so the Host resolved none. */
-const EMPTY_SKILL_INVOCATION = { loaded: [], failed: [], receipts: [] } as const;
const DESKTOP_MESSAGE_QUERY_MAX_ENTRIES = 4_096;
async function submitMessageWithReconnect(
@@ -429,7 +428,7 @@ export function registerRuntimeHostSessionExecutionIpc(
command.displayText ??
(command.text.trim().length > 0
? command.text
- : (command.skillIds ?? []).map((id) => `/skill:${id}`).join(" "));
+ : Object.values(command.inputSelections ?? {}).flat().join(" "));
const inlineReferences = mergeWorkspaceFileInlineReferences({
displayText,
workspaceFileReferences: command.workspaceFileReferences,
@@ -455,7 +454,7 @@ export function registerRuntimeHostSessionExecutionIpc(
...(command.quotes ? { quotes: command.quotes } : {}),
inlineReferences,
},
- ...((command.skillIds?.length ?? 0) > 0 ? { skillIds: command.skillIds } : {}),
+ ...(command.inputSelections ? { inputSelections: command.inputSelections } : {}),
...(command.turnOrchestration
? { turnOrchestration: command.turnOrchestration }
: {}),
@@ -465,14 +464,15 @@ export function registerRuntimeHostSessionExecutionIpc(
ok: false as const,
reason: 'outcome_unknown' as const,
messageId,
- skillInvocation: EMPTY_SKILL_INVOCATION,
+ preparation: [],
};
}
if (submitted.disposition === "blocked") {
return {
ok: false as const,
- reason: "skill_invocation_failed" as const,
- skillInvocation: submitted.skillInvocation,
+ reason: "input_preparation_failed" as const,
+ message: submitted.message,
+ preparation: submitted.preparation,
};
}
if (submitted.disposition === "turn_started") {
@@ -484,7 +484,7 @@ export function registerRuntimeHostSessionExecutionIpc(
turnId: submitted.turnId,
attachments,
inlineReferences,
- skillInvocation: submitted.skillInvocation,
+ preparation: submitted.preparation,
};
}
// The sending surface believed this Session idle; nudge it to refresh so
@@ -497,7 +497,7 @@ export function registerRuntimeHostSessionExecutionIpc(
...(sideConversation ? { messageId } : {}),
attachments,
inlineReferences,
- skillInvocation: submitted.skillInvocation,
+ preparation: submitted.preparation,
};
},
);
@@ -532,7 +532,7 @@ export function registerRuntimeHostSessionExecutionIpc(
command.displayText ??
(command.text.trim().length > 0
? command.text
- : (command.skillIds ?? []).map((id) => `/skill:${id}`).join(" "));
+ : Object.values(command.inputSelections ?? {}).flat().join(" "));
const inlineReferences = mergeWorkspaceFileInlineReferences({
displayText,
workspaceFileReferences: command.workspaceFileReferences,
@@ -555,7 +555,7 @@ export function registerRuntimeHostSessionExecutionIpc(
...(command.quotes ? { quotes: command.quotes } : {}),
inlineReferences,
},
- ...((command.skillIds?.length ?? 0) > 0 ? { skillIds: command.skillIds } : {}),
+ ...(command.inputSelections ? { inputSelections: command.inputSelections } : {}),
...(command.turnOrchestration
? { turnOrchestration: command.turnOrchestration }
: {}),
@@ -564,8 +564,9 @@ export function registerRuntimeHostSessionExecutionIpc(
if (result.disposition === 'blocked') {
return {
ok: false as const,
- reason: 'skill_invocation_failed' as const,
- skillInvocation: result.skillInvocation,
+ reason: 'input_preparation_failed' as const,
+ message: result.message,
+ preparation: result.preparation,
};
}
if (result.disposition === "turn_started") {
@@ -578,7 +579,7 @@ export function registerRuntimeHostSessionExecutionIpc(
turnId: result.turnId,
attachments,
inlineReferences,
- skillInvocation: result.skillInvocation,
+ preparation: result.preparation,
};
}
// The submitting surface believed this Session idle when it steered;
@@ -589,7 +590,7 @@ export function registerRuntimeHostSessionExecutionIpc(
disposition: result.disposition,
attachments,
inlineReferences,
- skillInvocation: result.skillInvocation,
+ preparation: result.preparation,
};
},
);
diff --git a/apps/desktop/src/main/runtime-host-ssh-terminal.ts b/apps/desktop/src/main/runtime-host-ssh-terminal.ts
index 14fe5c4347..1a0bda207f 100644
--- a/apps/desktop/src/main/runtime-host-ssh-terminal.ts
+++ b/apps/desktop/src/main/runtime-host-ssh-terminal.ts
@@ -42,7 +42,6 @@ import {
decodeRuntimeHostPeerManagementFrame,
decodeRuntimeHostPeerMeshManagementFrame,
decodeRuntimeHostServiceManagementFrame,
- decodeRuntimeHostSetupFrame,
RUNTIME_HOST_ACTIVATION_FRAME_MAX_BYTES,
RUNTIME_HOST_ACTIVATION_FRAME_PREFIX,
RUNTIME_HOST_ACCESS_MANAGEMENT_FRAME_PREFIX,
@@ -54,7 +53,6 @@ import {
RUNTIME_HOST_PEER_MESH_MANAGEMENT_FRAME_MAX_BYTES,
RUNTIME_HOST_PEER_MESH_MANAGEMENT_FRAME_PREFIX,
RUNTIME_HOST_SERVICE_MANAGEMENT_FRAME_PREFIX,
- RUNTIME_HOST_SETUP_FRAME_PREFIX,
RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV,
type RuntimeHostAccessManagementFrame,
type RuntimeHostActivationResult,
@@ -70,7 +68,6 @@ import {
type RuntimeHostServiceManagementAction,
type RuntimeHostServiceManagementFrame,
type RuntimeHostServiceUpdatePhase,
- type RuntimeHostSetupFrame,
type RuntimeHostWebRtcStunPolicy,
} from '@maka/runtime-host/operator';
import type {
@@ -111,7 +108,6 @@ type DesktopRuntimeHostSshProcess = RuntimeHostSshProcess & {
const TERMINAL_REVEAL_DELAY_MS = 500;
const TERMINAL_OUTPUT_MAX = 64 * 1024;
-const SETUP_FRAME_PENDING_MAX = 20 * 1024;
const MANAGEMENT_FRAME_PENDING_MAX = 128 * 1024;
const ACCESS_MANAGEMENT_FRAME_PENDING_MAX = 768 * 1024;
const PEER_MANAGEMENT_FRAME_PENDING_MAX = 128 * 1024;
@@ -119,17 +115,6 @@ const SETUP_TIMEOUT_MS = 10 * 60_000;
const MANAGEMENT_TIMEOUT_MS = 2 * 60_000;
const PROCESS_STOP_GRACE_MS = 2_000;
-export interface DesktopRuntimeHostSshSetupInput {
- readonly destination: string;
- readonly sshPort?: number;
- readonly setupPackage: DesktopRuntimeHostSetupPackage;
- readonly remotePlatform: RuntimeHostOperatorPlatform;
- readonly principalId: string;
- readonly lifecycle?: 'supervised' | 'on_demand';
- readonly projectDirectoryRoots?: readonly { readonly label: string; readonly path: string }[];
- readonly signal?: AbortSignal;
-}
-
export interface DesktopRuntimeHostSshTargetInput {
readonly destination: string;
readonly sshPort?: number;
@@ -260,7 +245,6 @@ export type RuntimeHostServiceUpdateReconciliationTerminalFrame = Extract<
{ kind: 'result' | 'error'; action: 'reconcile_update' }
>;
-type RuntimeHostSetupCompleteFrame = Extract<RuntimeHostSetupFrame, { kind: 'complete' }>;
export function createDesktopRuntimeHostSshTerminal(input: {
readonly ipcMain: Pick<IpcMain, 'handle' | 'removeHandler'>;
@@ -280,11 +264,6 @@ export function createDesktopRuntimeHostSshTerminal(input: {
resolveTargetIdentity(
input: DesktopRuntimeHostSshTargetInput,
): Promise<RuntimeHostTargetIdentity>;
- runSetup(
- input: DesktopRuntimeHostSshSetupInput,
- onProgress: (frame: Extract<RuntimeHostSetupFrame, { kind: 'progress' }>) => void,
- onComplete?: (frame: RuntimeHostSetupCompleteFrame) => void,
- ): Promise<RuntimeHostSetupCompleteFrame>;
runNativeSetup(input: NativeSetupInput & DesktopRuntimeHostSshTargetInput, onCommit: () => void): Promise<NativeSetupResult>;
prepareNativePackage(input: NativeSetupInput & DesktopRuntimeHostSshTargetInput): Promise<RuntimeHostNativeOperatorCommand>;
runServiceManagement(
@@ -800,80 +779,6 @@ export function createDesktopRuntimeHostSshTerminal(input: {
},
runNativeSetup: (setupInput, onCommit) => installNativeRuntimeHost(nativeTransport(setupInput), setupInput, onCommit),
prepareNativePackage: (setupInput) => prepareNativeRuntimeHost(nativeTransport(setupInput), setupInput),
- runSetup: async (setupInput, onProgress, onComplete) => {
- if (closed) throw new Error('Runtime Host SSH terminal is closed');
- setupInput.signal?.throwIfAborted();
- const cancellation = cancellableUntilComplete(setupInput.signal);
- try {
- const destination = normalizeRuntimeHostSshDestination(setupInput.destination);
- const sshPort = setupInput.sshPort === undefined
- ? undefined
- : requireSetupPort(setupInput.sshPort);
- const setupPackage = await prepareSetupPackage(
- setupInput.setupPackage,
- destination,
- sshPort,
- setupInput.principalId,
- startTerminalProcess,
- cancellation.signal,
- input.processStopGraceMs,
- dismissPresentation,
- input.terminateProcessTree,
- );
- const remoteCommand = runtimeHostSetupRemoteCommand(setupPackage, setupInput);
- let complete: RuntimeHostSetupCompleteFrame | undefined;
- let setupFailure: Error | undefined;
- let setupTerminal: ActiveTerminal | undefined;
- const filter = createRuntimeHostFramedOutputFilter({
- prefix: RUNTIME_HOST_SETUP_FRAME_PREFIX,
- pendingMaxBytes: SETUP_FRAME_PENDING_MAX,
- decode: decodeRuntimeHostSetupFrame,
- label: 'Remote Maka setup',
- onFrame: (frame) => {
- if (frame.kind === 'progress') onProgress(frame);
- else if (frame.kind === 'complete') {
- if (!cancellation.commit()) return;
- complete = frame;
- onComplete?.(frame);
- if (setupTerminal) completePresentation(setupTerminal);
- } else setupFailure = new Error(frame.kind === 'error' ? frame.error.message : 'SSH setup returned a local environment binding');
- },
- onError: (error) => {
- setupFailure = error;
- },
- });
- const { process, terminal } = startTerminalProcess(
- 'ssh',
- sshRemoteCommandArgs(destination, sshPort, remoteCommand),
- filter.push,
- );
- setupTerminal = terminal;
- if (complete) completePresentation(terminal);
- const wait = await waitForTerminalProcess(process, {
- signal: cancellation.signal,
- timeoutMs: SETUP_TIMEOUT_MS,
- stopGraceMs: input.processStopGraceMs,
- onAbort: () => dismissPresentation(terminal),
- }, input.terminateProcessTree);
- filter.finish();
- if (setupFailure) throw setupFailure;
- if (!complete) {
- throw new Error(
- wait.timedOut
- ? 'Remote Maka setup timed out'
- : wait.exit.code === 0
- ? 'Remote Maka setup ended without a completion result'
- : wait.exit.code === 2
- ? 'The released Maka CLI on this channel does not support automated Runtime Host setup'
- : `Remote Maka setup exited with code ${String(wait.exit.code)}`,
- );
- }
- completePresentation(terminal);
- return complete;
- } finally {
- cancellation.close();
- }
- },
runServiceManagement: async (managementInput) => {
const frame = await runFramedManagement({
...managementInput,
@@ -1071,31 +976,6 @@ export function runtimeHostPeerTargetFromPlatform(
throw new Error(`Direct peer is not available on ${target}`);
}
-function cancellableUntilComplete(signal: AbortSignal | undefined): {
- readonly signal: AbortSignal;
- commit(): boolean;
- close(): void;
-} {
- const controller = new AbortController();
- let committed = false;
- const onAbort = () => {
- if (!committed) controller.abort();
- };
- if (signal?.aborted) onAbort();
- else signal?.addEventListener('abort', onAbort, { once: true });
- const close = () => signal?.removeEventListener('abort', onAbort);
- return {
- signal: controller.signal,
- commit() {
- if (controller.signal.aborted) return false;
- committed = true;
- close();
- return true;
- },
- close,
- };
-}
-
type PreparedSetupPackage =
| { readonly kind: 'npm'; readonly specifier: string }
| {
@@ -1260,42 +1140,6 @@ async function settlesWithin(promise: Promise<unknown>, timeoutMs: number): Prom
}
}
-function runtimeHostSetupRemoteCommand(
- setupPackage: PreparedSetupPackage,
- input: Pick<
- DesktopRuntimeHostSshSetupInput,
- 'principalId' | 'projectDirectoryRoots' | 'lifecycle' | 'remotePlatform'
- >,
-): string {
- if (!/^[A-Za-z0-9_.:-]{1,128}$/u.test(input.principalId)) {
- throw new Error('Runtime Host setup principal is invalid');
- }
- return runtimeHostPackageRemoteCommand(setupPackage, [
- 'runtime-host',
- 'setup',
- '--principal',
- input.principalId,
- '--preset',
- 'desktop-client',
- '--lifecycle',
- input.lifecycle === 'on_demand' ? 'on-demand' : 'supervised',
- // Development archives identify every source revision as a distinct exact
- // package. Re-running Add computer is the explicit replacement gesture in
- // that environment; released packages keep using the normal update UI.
- ...(setupPackage.kind === 'development_archive' ? ['--update-existing'] : []),
- '--defer-pairing-commit',
- ...(input.projectDirectoryRoots === undefined
- ? []
- : input.projectDirectoryRoots.length === 0
- ? ['--no-project-roots']
- : input.projectDirectoryRoots.flatMap(({ label, path }) => [
- '--project-root-json',
- JSON.stringify({ label, path }),
- ])),
- '--json',
- ], {}, input.remotePlatform);
-}
-
function runtimeHostActivationRemoteCommand(
input: RuntimeHostSshOperatorActivationInput,
): string {
@@ -1366,7 +1210,7 @@ function runtimeHostUpdateRemoteCommand(
RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY,
[RUNTIME_HOST_OPERATOR_PROJECT_DIRECTORY_CONFIGURATION_REQUEST_ENV]: '1',
},
- input.operator.kind === 'legacy_posix_executable' ? 'posix' : input.operator.platform,
+ input.operator.platform,
);
}
diff --git a/apps/desktop/src/main/runtime-host-wsl-controller.ts b/apps/desktop/src/main/runtime-host-wsl-controller.ts
index cdea0be866..a409105369 100644
--- a/apps/desktop/src/main/runtime-host-wsl-controller.ts
+++ b/apps/desktop/src/main/runtime-host-wsl-controller.ts
@@ -26,20 +26,15 @@ import {
type RuntimeHostWslProcessFactory,
} from '@maka/runtime-host/client';
import {
- decodeRuntimeHostSetupFrame,
decodeRuntimeHostServiceManagementFrame,
RUNTIME_HOST_OPERATOR_PROJECT_DIRECTORY_CONFIGURATION_REQUEST_ENV,
RUNTIME_HOST_OPERATOR_RETIREMENT_CANCELLATION_ENV,
RUNTIME_HOST_SERVICE_MANAGEMENT_FRAME_PREFIX,
- RUNTIME_HOST_SETUP_FRAME_PREFIX,
RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV,
decodeRuntimeHostPosixOperatorCommand,
runtimeHostOperatorInvocation,
- type RuntimeHostNodeOperatorCommand,
type RuntimeHostNativeOperatorCommand,
type RuntimeHostPosixOperatorCommand,
- type RuntimeHostSetupFrame,
- type RuntimeHostSetupPhase,
type RuntimeHostServiceManagementFrame,
} from '@maka/runtime-host/operator';
import { createRuntimeHostFramedOutputFilter } from './runtime-host-framed-output.js';
@@ -124,10 +119,6 @@ export async function resolveDesktopRuntimeHostWslTarget(
});
}
-type RuntimeHostSetupCompleteFrame = Extract<RuntimeHostSetupFrame, { kind: 'complete' | 'existing_environment' }>;
-type RuntimeHostWslSetupCompleteFrame = Omit<RuntimeHostSetupCompleteFrame, 'operator'> & {
- readonly operator: RuntimeHostNodeOperatorCommand<'posix'> | RuntimeHostNativeOperatorCommand<'posix'>;
-};
type RuntimeHostManagementTerminalFrame = Exclude<
RuntimeHostServiceManagementFrame,
{ readonly kind: 'progress' }
@@ -229,65 +220,6 @@ export async function runDesktopRuntimeHostWslManagement(
return terminal;
}
-export interface DesktopRuntimeHostWslSetupInput {
- readonly distribution: string;
- readonly setupPackage: DesktopRuntimeHostSetupPackage;
- readonly principalId: string;
- readonly projectDirectoryRoots?: readonly {
- readonly label: string;
- readonly path: string;
- }[];
- readonly signal?: AbortSignal;
-}
-
-export async function runDesktopRuntimeHostWslSetup(
- input: DesktopRuntimeHostWslSetupInput,
- onProgress: (frame: { readonly phase: RuntimeHostSetupPhase }) => void,
- onComplete?: () => void,
- overrides: {
- readonly processFactory?: RuntimeHostWslProcessFactory;
- readonly wslExecutable?: string;
- } = {},
-): Promise<RuntimeHostWslSetupCompleteFrame> {
- input.signal?.throwIfAborted();
- const distribution = normalizeRuntimeHostWslDistribution(input.distribution);
- const processFactory = overrides.processFactory ?? spawnWsl;
- const executable = overrides.wslExecutable ?? resolveSystemRuntimeHostWslExecutable();
- const setupPackage = await resolveWslPackageSpecifier(
- input.setupPackage,
- distribution,
- executable,
- processFactory,
- );
- const command = runtimeHostWslSetupCommand(setupPackage, input, input.setupPackage.kind === 'development_archive');
- const child = processFactory(executable, ['--distribution', distribution, '--exec', '/bin/sh', '-lc', command]);
- return runWslFramedProcess({
- child,
- signal: input.signal,
- prefix: RUNTIME_HOST_SETUP_FRAME_PREFIX,
- decode: decodeRuntimeHostSetupFrame,
- label: 'WSL Maka setup',
- onFrame: (frame) => {
- if (frame.kind === 'progress') {
- onProgress(frame);
- return undefined;
- }
- if (frame.kind === 'error') throw new Error(frame.error.message);
- if (frame.operator.platform !== 'posix') {
- throw new Error('WSL Runtime Host setup returned a non-POSIX operator');
- }
- return {
- ...frame,
- operator: {
- ...frame.operator,
- platform: 'posix',
- },
- };
- },
- onResult: () => onComplete?.(),
- });
-}
-
/** Runs the explicitly selected successor, which owns the existing durable update transaction. */
export async function runDesktopRuntimeHostWslUpdate(
input: {
@@ -375,48 +307,6 @@ async function resolveWslPackageSpecifier(
return { specifier: path, integrity: setupPackage.integrity };
}
-function runtimeHostWslSetupCommand(
- setupPackage: { readonly specifier: string; readonly integrity?: string },
- input: Pick<DesktopRuntimeHostWslSetupInput, 'principalId' | 'projectDirectoryRoots'>,
- development: boolean,
-): string {
- if (!/^[A-Za-z0-9_.:-]{1,128}$/u.test(input.principalId)) {
- throw new Error('Runtime Host setup principal is invalid');
- }
- const args = [
- 'maka',
- 'runtime-host',
- 'setup',
- '--principal',
- input.principalId,
- '--preset',
- 'desktop-client',
- '--lifecycle',
- 'on-demand',
- '--repair-root-after-remount',
- // Source development explicitly selects a new archive; released onboarding
- // must never replace an existing shared deployment as a side effect of Connect.
- ...(development ? ['--update-existing'] : ['--reuse-existing-environment']),
- ...(input.projectDirectoryRoots === undefined
- ? []
- : input.projectDirectoryRoots.length === 0
- ? ['--no-project-roots']
- : input.projectDirectoryRoots.flatMap(({ label, path }) => [
- '--project-root-json',
- JSON.stringify({ label, path }),
- ])),
- '--json',
- ];
- const invocation = ['npx', '--yes', '--package', setupPackage.specifier, ...args]
- .map(quotePosix)
- .join(' ');
- const environment = setupPackage.integrity
- ? `${RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV}=${quotePosix(setupPackage.integrity)} `
- : '';
- const command = `maka_prefix=$(mktemp -d) || exit 1; trap 'rm -rf -- "$maka_prefix"' EXIT; cd "$maka_prefix" || exit 1; ${environment}${invocation}`;
- return runtimeHostWslLoginCommand(command);
-}
-
function runtimeHostWslLoginCommand(command: string): string {
const loginCommand = `exec /bin/sh -c ${quotePosix(command)}`;
return `exec "\${SHELL:-/bin/sh}" -lic ${quotePosix(loginCommand)}`;
diff --git a/apps/desktop/src/main/runtime-host-wsl-handoff.ts b/apps/desktop/src/main/runtime-host-wsl-handoff.ts
index 67cc4034b4..7fbb759866 100644
--- a/apps/desktop/src/main/runtime-host-wsl-handoff.ts
+++ b/apps/desktop/src/main/runtime-host-wsl-handoff.ts
@@ -63,7 +63,7 @@ export async function resolveDesktopWslHostHandoff(
action: 'status', expectedTarget, signal,
});
if (status.kind !== 'result' || status.action !== 'status' || status.service.lifecycle?.mode !== 'on_demand' ||
- !status.service.pid || !status.service.installedVersion) {
+ !status.service.pid || !status.service.installedVersion || !status.service.configurationFingerprint) {
return { ...base, operatorStep: guidance.source };
}
const setupPackage = await deps.resolvePackage(signal);
@@ -73,8 +73,6 @@ export async function resolveDesktopWslHostHandoff(
return { ...base, operatorStep: guidance.target };
}
const currentVersion = status.service.installedVersion;
- // Legacy operators omit the fingerprint. The transaction still fences the source
- // package version, deployment identity and exact Host generation under its lease.
const expectedConfigFingerprint = status.service.configurationFingerprint;
const expectedHost = { hostEpoch: error.hostEpoch, pid: status.service.pid };
const identity = JSON.stringify([base.identity, binding.deployment, expectedHost, currentVersion, expectedConfigFingerprint, setupPackage]);
diff --git a/apps/desktop/src/main/session-local-service.ts b/apps/desktop/src/main/session-local-service.ts
index 558bd3e84c..f466d015a4 100644
--- a/apps/desktop/src/main/session-local-service.ts
+++ b/apps/desktop/src/main/session-local-service.ts
@@ -650,7 +650,7 @@ export function registerDesktopSessionLocalIpc(deps: {
quotes: command.quotes,
inlineReferences,
},
- ...(command.skillIds?.length ? { skillIds: command.skillIds } : {}),
+ ...(command.inputSelections ? { inputSelections: command.inputSelections } : {}),
...(command.turnOrchestration ? { turnOrchestration: command.turnOrchestration } : {}),
},
}),
@@ -668,7 +668,7 @@ export function registerDesktopSessionLocalIpc(deps: {
disposition: 'locally_saved',
attachments: retained,
inlineReferences,
- skillInvocation: { loaded: [], failed: [], receipts: [] },
+ preparation: [],
};
},
);
diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts
index 2115b23ce0..aaff099e39 100644
--- a/apps/desktop/src/preload/bridge-contract.d.ts
+++ b/apps/desktop/src/preload/bridge-contract.d.ts
@@ -856,6 +856,7 @@ export interface MakaBridge {
};
clientPlugins: {
+ authorization(host: DesktopRuntimeHostRef, connectionEpoch: string, input: OperationInput<'plugin.authorization'>, registerCancellation: (cancel: () => void) => void): Promise<OperationOutput<'plugin.authorization'>>;
file(host: DesktopRuntimeHostRef, connectionEpoch: string, identity: ClientIdentity, input: ClientFileRequest): Promise<string | null>;
connection(host: DesktopRuntimeHostRef): Promise<{ epoch: string; hostEpoch: string; localFiles: boolean }>;
session(host: DesktopRuntimeHostRef, connectionEpoch: string, sessionId: string): Promise<string>;
@@ -1119,7 +1120,7 @@ export interface MakaBridge {
turnId: string;
text: string;
displayText?: string;
- skillIds?: string[];
+ inputSelections?: import("@maka/runtime-host/protocol").InputSelections;
attachmentItems?: RendererIngestInput[];
retainedAttachments?: import('@maka/core/events').AttachmentRef[];
turnOrchestration?: TurnOrchestration;
@@ -1142,7 +1143,7 @@ export interface MakaBridge {
messageId?: never;
attachments: import('@maka/core/events').AttachmentRef[];
inlineReferences: import('@maka/core/events').InlineReference[];
- skillInvocation: import('@maka/runtime/skill-invocation').SkillInvocationResult;
+ preparation: import('@maka/runtime-host/protocol').InputReceipt[];
}
| {
ok: true;
@@ -1156,12 +1157,13 @@ export interface MakaBridge {
messageId: string;
attachments: import('@maka/core/events').AttachmentRef[];
inlineReferences: import('@maka/core/events').InlineReference[];
- skillInvocation: import('@maka/runtime/skill-invocation').SkillInvocationResult;
+ preparation: import('@maka/runtime-host/protocol').InputReceipt[];
}
| {
ok: false;
- reason: 'skill_invocation_failed';
- skillInvocation: import('@maka/runtime/skill-invocation').SkillInvocationResult;
+ reason: 'input_preparation_failed';
+ message: string;
+ preparation: import('@maka/runtime-host/protocol').InputReceipt[];
}
| {
ok: false;
@@ -1172,7 +1174,7 @@ export interface MakaBridge {
ok: false;
reason: 'outcome_unknown';
messageId: string;
- skillInvocation: import('@maka/runtime/skill-invocation').SkillInvocationResult;
+ preparation: import('@maka/runtime-host/protocol').InputReceipt[];
}
>;
stop(
@@ -1195,7 +1197,7 @@ export interface MakaBridge {
messageId: string;
text: string;
displayText?: string;
- skillIds?: string[];
+ inputSelections?: import("@maka/runtime-host/protocol").InputSelections;
turnOrchestration?: TurnOrchestration;
attachmentItems?: RendererIngestInput[];
retainedAttachments?: import('@maka/core/events').AttachmentRef[];
@@ -1214,12 +1216,13 @@ export interface MakaBridge {
turnId?: string;
attachments: import('@maka/core/events').AttachmentRef[];
inlineReferences: import('@maka/core/events').InlineReference[];
- skillInvocation: import('@maka/runtime/skill-invocation').SkillInvocationResult;
+ preparation: import('@maka/runtime-host/protocol').InputReceipt[];
}
| {
ok: false;
- reason: 'skill_invocation_failed';
- skillInvocation: import('@maka/runtime/skill-invocation').SkillInvocationResult;
+ reason: 'input_preparation_failed';
+ message: string;
+ preparation: import('@maka/runtime-host/protocol').InputReceipt[];
}
| {
ok: false;
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 7747615536..382c614c88 100644
--- a/apps/desktop/src/preload/preload.ts
+++ b/apps/desktop/src/preload/preload.ts
@@ -1409,6 +1409,17 @@ const makaBridge = {
workHubControl: workHubControlBridge,
workHubPresentation: workHubPresentationBridge,
clientPlugins: {
+ async authorization(host, connectionEpoch, input, registerCancellation) {
+ const requestId = crypto.randomUUID();
+ let cancelled = false;
+ let cancelPending: (() => void) | undefined;
+ registerCancellation(() => { cancelled = true; cancelPending?.(); });
+ const scope = await runtimeHostScope(host);
+ if (cancelled) throw new Error('Client plugin retired before authorization');
+ const pending = ipcRenderer.invoke('plugins:authorization', scope, browserDocumentId, connectionEpoch, input, requestId);
+ cancelPending = () => { void ipcRenderer.invoke('plugins:authorization-cancel', scope, browserDocumentId, connectionEpoch, requestId).catch(() => {}); };
+ try { return await pending; } finally { cancelPending = undefined; }
+ },
subscribeContext(host, handler) {
const disposers = [
subscribeSelectedRuntimeHostEvent('connections:event', host, handler),
diff --git a/apps/desktop/src/renderer/app-shell-chat-actions.ts b/apps/desktop/src/renderer/app-shell-chat-actions.ts
index 8dff1f4450..e4b29ab40e 100644
--- a/apps/desktop/src/renderer/app-shell-chat-actions.ts
+++ b/apps/desktop/src/renderer/app-shell-chat-actions.ts
@@ -24,7 +24,7 @@ import type * as DesktopBridge from '../preload/bridge-contract.js';
import type { QuoteRef } from '@maka/core/events';
import type { OrchestrationMode } from '@maka/core/orchestration';
import type { SandboxBoundaryResponse } from '@maka/core/sandbox-boundary';
-import type { SkillInvocationResult } from '@maka/runtime/skill-invocation';
+import type { InputReceipt } from '@maka/runtime-host/protocol';
import type { ThinkingLevel } from '@maka/core/model-thinking';
import type { TurnOrchestration } from '@maka/core/runtime-inputs';
import type { UiLocale } from '@maka/core/ui-locale';
@@ -234,7 +234,7 @@ export function createAppShellChatActions(deps: {
/** Only an unreconciled submission keeps its row because Host admission may have succeeded. */
type SubmittedMessage =
- | { kind: 'projected'; skillInvocation: SkillInvocationResult; turnId?: string }
+ | { kind: 'projected'; preparation: InputReceipt[]; turnId?: string }
| { kind: 'unreconciled' }
| { kind: 'refused' };
@@ -277,7 +277,7 @@ export function createAppShellChatActions(deps: {
return { kind: 'refused' };
}
if (result.disposition === 'locally_saved') {
- return { kind: 'projected', skillInvocation: result.skillInvocation };
+ return { kind: 'projected', preparation: result.preparation };
}
if (surfaceVisible) skillFeedback.showSubmissionFeedback(uiLocale, toastApi, result, sessionId);
// The row is updated whether or not the surface is on screen: attachments,
@@ -285,7 +285,7 @@ export function createAppShellChatActions(deps: {
// they come back to it.
publishTransientUserMessage(sessionId, {
id: messageId,
- text: input.displayText ?? skillFeedback.skillInvocationDisplayText(input.command.text, result.skillInvocation),
+ text: input.displayText ?? skillFeedback.skillInvocationDisplayText(input.command.text, result.preparation),
attachments: [...result.attachments],
transientPlacement: placement,
pendingSteering: result.disposition === 'turn_started' ? false : input.pendingSteering,
@@ -296,7 +296,7 @@ export function createAppShellChatActions(deps: {
}, true);
return {
kind: 'projected',
- skillInvocation: result.skillInvocation,
+ preparation: result.preparation,
...(result.turnId ? { turnId: result.turnId } : {}),
};
}
diff --git a/apps/desktop/src/renderer/features/client-plugins/host-runtime.ts b/apps/desktop/src/renderer/features/client-plugins/host-runtime.ts
index 3f86dfe27b..0411656594 100644
--- a/apps/desktop/src/renderer/features/client-plugins/host-runtime.ts
+++ b/apps/desktop/src/renderer/features/client-plugins/host-runtime.ts
@@ -95,7 +95,7 @@ export class ClientHostRuntime {
else this.options.report(diagnostic);
};
const runtime = this.create({ ...this.options, source: transport.source,
- remote: transport.remote, localFiles: transport.localFiles, report });
+ remote: transport.remote, localFiles: transport.localFiles, authorization: transport.authorization, report });
let revision = 0;
let retry = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
diff --git a/apps/desktop/src/renderer/features/client-plugins/ports.ts b/apps/desktop/src/renderer/features/client-plugins/ports.ts
index 479fe6526b..ab11870a18 100644
--- a/apps/desktop/src/renderer/features/client-plugins/ports.ts
+++ b/apps/desktop/src/renderer/features/client-plugins/ports.ts
@@ -18,7 +18,7 @@
*/
import type { ClientDescriptor } from '@maka-agent/plugin-sdk/client';
-import type { ClientSnapshot, ClientRemoteFactory, ClientFilesFactory } from '@maka/ui/client-plugins';
+import type { ClientSnapshot, ClientRemoteFactory, ClientFilesFactory, ClientAuthorizationFactory } from '@maka/ui/client-plugins';
export interface ClientHostRef { readonly profileId: string; readonly hostId: string }
export interface ClientPluginServices {
@@ -27,6 +27,7 @@ export interface ClientPluginServices {
connect(host: ClientHostRef): {
readonly remote: ClientRemoteFactory;
readonly localFiles?: ClientFilesFactory;
+ readonly authorization?: ClientAuthorizationFactory;
session(sessionId: string): Promise<string>;
snapshot(signal: AbortSignal): Promise<ClientSnapshot>;
source(descriptor: ClientDescriptor, signal: AbortSignal): Promise<string>;
diff --git a/apps/desktop/src/renderer/platform/desktop/client-plugin-remote.ts b/apps/desktop/src/renderer/platform/desktop/client-plugin-remote.ts
index 801e6b69c0..0442dfe3c4 100644
--- a/apps/desktop/src/renderer/platform/desktop/client-plugin-remote.ts
+++ b/apps/desktop/src/renderer/platform/desktop/client-plugin-remote.ts
@@ -17,118 +17,17 @@
* under the License.
*/
-import type { ClientIdentity, ClientRemote } from '@maka-agent/plugin-sdk/client';
-import type { Json } from '@maka-agent/plugin-sdk/host';
+import { createPluginRemote } from '@maka/runtime-host/client';
import type { ClientRemoteFactory } from '@maka/ui/client-plugins';
-import type { PluginRemoteBinding, PluginRemoteTarget } from '@maka/runtime-host/protocol';
import type { MakaBridge } from '../../../preload/bridge-contract.js';
import type { ClientHostRef } from '../../features/client-plugins/index.js';
-/** One document per UI instance, lazily allocated and never retargeted. */
export function clientPluginRemote(
transport: MakaBridge['clientPlugins']['remote'],
host: ClientHostRef,
connectionEpoch: string,
): ClientRemoteFactory {
- return (identity: ClientIdentity, signal: AbortSignal) => {
- let document: Promise<string> | undefined;
- let closing: Promise<void> | undefined;
- const request = (input: Parameters<typeof transport>[2]) => transport(host, connectionEpoch, input);
- const assertLive = () => {
- signal.throwIfAborted();
- if (closing) throw new Error('Client Remote is closed');
- };
- const getDocument = () => {
- assertLive();
- document ??= request({ kind: 'open_document' }).then((result) => {
- if (result.kind !== 'document') throw new Error('Unexpected Remote document result');
- return result.document;
- });
- return document;
- };
- const close = (): Promise<void> => {
- if (closing) return closing;
- signal.removeEventListener('abort', retired);
- closing = (async () => {
- if (!document) return;
- let id: string;
- try { id = await document; } catch { return; }
- const result = await request({ kind: 'close_document', document: id });
- // Connection retirement revokes this local lease. Host owns draining
- // the old connection and fences backend instances if cleanup fails.
- if (result.kind !== 'closed' && result.kind !== 'connection_retired')
- throw new Error('Remote cleanup was not confirmed');
- })();
- return closing;
- };
- const retired = () => { void close().catch(() => {}); };
- signal.addEventListener('abort', retired, { once: true });
- if (signal.aborted) retired();
-
- const bind = (name: string, sessionId: string | undefined, handler: 'method' | 'stream') => {
- const binding: PluginRemoteBinding = { client: identity, method: name, sessionId: sessionId ?? null };
- let target: Promise<PluginRemoteTarget> | undefined;
- return async () => {
- assertLive();
- target ??= request({ kind: 'bind', binding }).then((result) => {
- if (result.kind !== 'bound' || result.handler !== handler) throw new Error('Remote handler kind mismatch');
- return result.target;
- });
- const [destination, owner] = await Promise.all([target, getDocument()]);
- assertLive();
- return { binding, target: destination, document: owner };
- };
- };
- const api: ClientRemote = {
- method<I extends Json, O extends Json>(name: string, sessionId?: string) {
- const bound = bind(name, sessionId, 'method');
- return async (input: I): Promise<O> => {
- const result = await request({ kind: 'call', ...await bound(), input });
- if (result.kind !== 'value') throw new Error('Unexpected Remote method result');
- return result.value as O;
- };
- },
- stream<I extends Json, O extends Json>(name: string, sessionId?: string) {
- const bound = bind(name, sessionId, 'stream');
- return async function* (input: I, cancellation?: AbortSignal): AsyncGenerator<O> {
- cancellation?.throwIfAborted();
- const origin = await bound();
- cancellation?.throwIfAborted();
- const opened = await request({ kind: 'open', ...origin, input });
- if (opened.kind !== 'opened') throw new Error('Unexpected Remote stream result');
- const handle = { document: origin.document, stream: opened.stream };
- let ended = false;
- let stopping: Promise<void> | undefined;
- const stop = () => {
- if (ended || closing) return Promise.resolve();
- stopping ??= request({ kind: 'close', ...handle }).then((result) => {
- if (result.kind !== 'closed' && result.kind !== 'connection_retired')
- throw new Error('Remote stream cleanup was not confirmed');
- });
- return stopping;
- };
- // A generator return() cannot preempt its own awaited next(). Signal
- // the Host separately so an idle provider read can actually finish.
- const abort = () => { void stop().catch(() => {}); };
- cancellation?.addEventListener('abort', abort, { once: true });
- try {
- for (;;) {
- assertLive();
- cancellation?.throwIfAborted();
- const result = await request({ kind: 'next', ...handle });
- cancellation?.throwIfAborted();
- if (result.kind === 'pending') continue;
- if (result.kind === 'end') { ended = true; return; }
- if (result.kind !== 'item') throw new Error('Unexpected Remote stream item');
- yield result.item as O;
- }
- } finally {
- cancellation?.removeEventListener('abort', abort);
- await stop();
- }
- };
- },
- };
- return { api, close };
- };
+ return (identity, signal) => createPluginRemote(
+ (input) => transport(host, connectionEpoch, input), identity, signal,
+ );
}
diff --git a/apps/desktop/src/renderer/platform/desktop/create-client-plugin-services.ts b/apps/desktop/src/renderer/platform/desktop/create-client-plugin-services.ts
index 26b5dc1dd8..655689a0f9 100644
--- a/apps/desktop/src/renderer/platform/desktop/create-client-plugin-services.ts
+++ b/apps/desktop/src/renderer/platform/desktop/create-client-plugin-services.ts
@@ -55,6 +55,37 @@ export function createDesktopClientPluginServices(
if (!connectionEpoch) throw new Error('Client catalog has no connection identity');
return clientPluginRemote(bridge.clientPlugins.remote, host, connectionEpoch)(identity, signal);
},
+ authorization(identity, signal) {
+ if (!connectionEpoch) throw new Error('Client catalog has no connection identity');
+ const epoch = connectionEpoch;
+ const invoke = async (scope: import('@maka-agent/plugin-sdk/client').AuthorizationScope, command: Parameters<typeof bridge.clientPlugins.authorization>[2]['command']) => {
+ signal.throwIfAborted();
+ let cancel: (() => void) | undefined;
+ try {
+ return await bounded(bridge.clientPlugins.authorization(host, epoch, {client: identity, scope, command}, (stop) => {
+ cancel = stop;
+ signal.addEventListener('abort', stop, {once:true});
+ if (signal.aborted) stop();
+ }), signal);
+ } finally { if (cancel) signal.removeEventListener('abort', cancel); }
+ };
+ return {
+ async approve(scope, request) {
+ const result = await invoke(scope, {kind:'approve',request});
+ if (result.kind !== 'grant') throw new Error('Invalid authorization result');
+ return result.grant;
+ },
+ async query(scope, id) {
+ const result = await invoke(scope, {kind:'query',id});
+ if (result.kind !== 'grant') throw new Error('Invalid authorization result');
+ return result.grant;
+ },
+ async revoke(scope, id) {
+ const result = await invoke(scope, {kind:'revoke',id});
+ if (result.kind !== 'revoked') throw new Error('Invalid authorization result');
+ },
+ };
+ },
localFiles(identity, signal) {
if (!connectionEpoch) throw new Error('Client catalog has no connection identity');
if (!localFiles) return undefined;
diff --git a/apps/desktop/src/renderer/skill-invocation-feedback.ts b/apps/desktop/src/renderer/skill-invocation-feedback.ts
index 4e71b0817c..d4f51fedec 100644
--- a/apps/desktop/src/renderer/skill-invocation-feedback.ts
+++ b/apps/desktop/src/renderer/skill-invocation-feedback.ts
@@ -20,6 +20,8 @@
import type { AttachmentIngestBlockedCode } from '@maka/core/attachments';
import type { UiLocale } from '@maka/core/ui-locale';
import type { SkillInvocationResult } from '@maka/runtime/skill-invocation';
+import { preparedSkillInvocation } from '@maka/core/skill-invocation';
+import type { InputReceipt } from '@maka/runtime-host/protocol';
import { getShellCopy } from './locales/shell-copy.js';
type FeedbackToastApi = {
@@ -33,7 +35,7 @@ type FeedbackToastApi = {
};
type SubmissionFeedback =
- | { skillInvocation: SkillInvocationResult }
+ | { preparation: InputReceipt[]; message?: string }
| { reason: 'attachment_blocked'; code: AttachmentIngestBlockedCode };
export function showSubmissionFeedback(
@@ -46,7 +48,11 @@ export function showSubmissionFeedback(
showAttachmentIngestBlockedFeedback(uiLocale, toastApi, outcome.code, sessionId);
return;
}
- showSkillInvocationFeedback(uiLocale, toastApi, outcome.skillInvocation, sessionId);
+ if (outcome.message) {
+ toastApi.error(getShellCopy(uiLocale).chatActions.sendFailedTitle, outcome.message, undefined, { sessionId });
+ return;
+ }
+ showSkillInvocationFeedback(uiLocale, toastApi, preparedSkillInvocation(outcome.preparation), sessionId);
}
function showAttachmentIngestBlockedFeedback(
@@ -67,10 +73,10 @@ function showAttachmentIngestBlockedFeedback(
/** Match main-process persistence for a chip-only optimistic user message. */
export function skillInvocationDisplayText(
text: string,
- skillInvocation: SkillInvocationResult,
+ preparation: InputReceipt[],
): string {
if (text.trim().length > 0) return text;
- return skillInvocation.loaded.map((skill) => `/skill:${skill.id}`).join(' ');
+ return preparedSkillInvocation(preparation).loaded.map((skill) => `/skill:${skill.id}`).join(' ');
}
/** The Composer is the only Desktop surface that invokes Skills (#1433). */
diff --git a/crates/agent/src/executor.rs b/crates/agent/src/executor.rs
index d8beb617bc..16386b87c9 100644
--- a/crates/agent/src/executor.rs
+++ b/crates/agent/src/executor.rs
@@ -48,12 +48,8 @@ impl Engine {
"executor cannot carry a model binding or mismatched workspace".into(),
));
}
- maka_runtime::message::validate_opening(
- &input.request.content,
- &input.source_messages,
- None,
- )
- .map_err(|reason| RunError::InvalidInput(reason.into()))?;
+ maka_runtime::message::validate_sources(&input.request.content, &input.source_messages)
+ .map_err(|reason| RunError::InvalidInput(reason.into()))?;
let identity = input
.binding
.identity()
@@ -82,7 +78,6 @@ impl Engine {
content: input.request.content,
request_fingerprint: input.request_fingerprint,
source_messages: input.source_messages,
- skill_invocation: None,
},
},
Fact::ExecutorStarted { binding: identity },
diff --git a/crates/agent/src/history.rs b/crates/agent/src/history.rs
index 84d16cafb0..4cfe885ada 100644
--- a/crates/agent/src/history.rs
+++ b/crates/agent/src/history.rs
@@ -111,7 +111,6 @@ mod tests {
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
content: "hello".into(),
request_fingerprint: None,
@@ -119,7 +118,7 @@ mod tests {
},
Fact::ModelRequested {
effective_source_digest: None,
- purpose: None,
+ purpose: maka_runtime::context::ModelPurpose::Main,
context: None,
checkpoint_event_id: None,
step_id: "step".into(),
diff --git a/crates/agent/src/history/compatible.rs b/crates/agent/src/history/compatible.rs
index 6412df71ed..cb1c172cea 100644
--- a/crates/agent/src/history/compatible.rs
+++ b/crates/agent/src/history/compatible.rs
@@ -65,13 +65,7 @@ pub(crate) fn project(mut messages: Vec<Message>, provider: &ProviderKind) -> Ve
}
fn reasoning_field(options: Option<&Value>) -> Option<&'static str> {
- let maka = &options?["maka"];
- let current = &maka["openAiChatReasoningField"];
- let field = if current.is_null() {
- &maka["kimiReasoningField"]
- } else {
- current
- };
+ let field = &options?["maka"]["openAiChatReasoningField"];
match field.as_str()? {
"reasoning" => Some("reasoning"),
"reasoning_content" => Some("reasoning_content"),
@@ -120,10 +114,9 @@ mod tests {
}
#[test]
- fn first_tagged_block_wins_and_legacy_field_is_supported() {
+ fn first_tagged_block_owns_the_reasoning_field() {
let messages = vec![json!({"role":"assistant","content":[
- {"type":"reasoning","text":"","providerOptions":{"maka":{
- "openAiChatReasoningField":null,"kimiReasoningField":"reasoning"}}},
+ thinking("reasoning", ""),
thinking("reasoning_content", "later"), thinking("reasoning", "last")
]})];
let projected = serde_json::to_value(project(
diff --git a/crates/agent/src/history/replay.rs b/crates/agent/src/history/replay.rs
index 7b130d8d17..9c7654a585 100644
--- a/crates/agent/src/history/replay.rs
+++ b/crates/agent/src/history/replay.rs
@@ -174,10 +174,9 @@ mod tests {
};
let events: Vec<_> = [
Fact::InvocationOpened { configuration: None, input: InvocationInput::Message {
- content: "question".into(), request_fingerprint: None, source_messages: Vec::new(), skill_invocation: None,
- }},
+ content: "question".into(), request_fingerprint: None, source_messages: Vec::new(), }},
Fact::ModelRequested { step_id: "step".into(), model_id: "old".into(), route_identity: "old".into(),
- purpose: None, context: None, source_scope: maka_runtime::event::LogScope::Root, source_high_water: 1,
+ purpose: maka_runtime::context::ModelPurpose::Main, context: None, source_scope: maka_runtime::event::LogScope::Root, source_high_water: 1,
source_digest: "fixture".into(), input_digest: "fixture".into(), checkpoint_event_id: None, effective_source_digest: None },
Fact::ModelCompleted { step_id: "step".into(), output: serde_json::from_value(json!({
"parts":[
diff --git a/crates/agent/src/lib.rs b/crates/agent/src/lib.rs
index ebd82c7edd..d5bf0dc524 100644
--- a/crates/agent/src/lib.rs
+++ b/crates/agent/src/lib.rs
@@ -106,7 +106,6 @@ pub enum RunWork {
Message {
message: MessageInput,
source_messages: Vec<maka_runtime::message::RootSourceMessage>,
- skill_invocation: Option<Box<maka_runtime::skills::SkillInvocationResult>>,
tools: ToolCatalog,
max_steps: usize,
},
@@ -245,15 +244,10 @@ impl Engine {
RunWork::Message {
message,
source_messages,
- skill_invocation,
..
} => {
- maka_runtime::message::validate_opening(
- message,
- source_messages,
- skill_invocation.as_deref(),
- )
- .map_err(|reason| RunError::InvalidInput(reason.into()))?;
+ maka_runtime::message::validate_sources(message, source_messages)
+ .map_err(|reason| RunError::InvalidInput(reason.into()))?;
}
_ => {}
}
diff --git a/crates/agent/src/model_attempt.rs b/crates/agent/src/model_attempt.rs
index 65702a78f6..e81e53430d 100644
--- a/crates/agent/src/model_attempt.rs
+++ b/crates/agent/src/model_attempt.rs
@@ -220,7 +220,7 @@ async fn execute_once(
maka_runtime::event::LogScope::Lineage { .. }
))
.then(|| source.effective_source_digest.clone()),
- purpose: Some(purpose),
+ purpose,
context: input.context.clone(),
step_id: step_id.clone(),
model_id: input.provider.model.clone(),
diff --git a/crates/agent/src/runner.rs b/crates/agent/src/runner.rs
index 50b1d4bbd0..9206429194 100644
--- a/crates/agent/src/runner.rs
+++ b/crates/agent/src/runner.rs
@@ -74,13 +74,11 @@ pub async fn run(
RunWork::Message {
message,
source_messages,
- skill_invocation,
..
} => InvocationInput::Message {
content: message.clone(),
request_fingerprint: input.request_fingerprint.clone(),
source_messages: source_messages.clone(),
- skill_invocation: skill_invocation.clone(),
},
RunWork::ContextCompact => InvocationInput::ContextCompact {
request_fingerprint: input
diff --git a/crates/agent/tests/integration/attachment_projection.rs b/crates/agent/tests/integration/attachment_projection.rs
index 5f009ab4bf..135263b64c 100644
--- a/crates/agent/tests/integration/attachment_projection.rs
+++ b/crates/agent/tests/integration/attachment_projection.rs
@@ -71,7 +71,6 @@ async fn attachment_references_remain_structured_and_match_the_original_model_fo
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
content: input.clone(),
request_fingerprint: None,
@@ -90,7 +89,6 @@ async fn attachment_references_remain_structured_and_match_the_original_model_fo
invocation_id: format!("invocation{index}"),
},
Fact::MessageSteered {
- skill_invocation: Default::default(),
message: Box::new(DeliveredMessage {
message_id: "same-client-id".into(),
content: input.clone(),
diff --git a/crates/agent/tests/integration/auto_context.rs b/crates/agent/tests/integration/auto_context.rs
index 7b3fee859f..90eb50dcab 100644
--- a/crates/agent/tests/integration/auto_context.rs
+++ b/crates/agent/tests/integration/auto_context.rs
@@ -208,7 +208,7 @@ async fn midturn_waits_for_all_code_mode_results_and_replays_exact_image_anchor_
.filter(|event| matches!(
event.event.fact,
Fact::ModelRequested {
- purpose: Some(ModelPurpose::Summary),
+ purpose: ModelPurpose::Summary,
..
}
))
diff --git a/crates/agent/tests/integration/code_mode_lifetime.rs b/crates/agent/tests/integration/code_mode_lifetime.rs
index 45320cb87a..8693603434 100644
--- a/crates/agent/tests/integration/code_mode_lifetime.rs
+++ b/crates/agent/tests/integration/code_mode_lifetime.rs
@@ -65,7 +65,6 @@ fn input(base: &str, suffix: &str, effect: Arc<SlowEffect>) -> RunInput {
supports_vision: false,
configuration: invocation::configuration(ToolMode::CodeMode),
work: maka_agent::RunWork::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
message: format!("question {suffix}").into(),
tools: ToolCatalog::new([ToolRegistration {
diff --git a/crates/agent/tests/integration/context_overflow.rs b/crates/agent/tests/integration/context_overflow.rs
index 8b25ba8367..0f5e579c9e 100644
--- a/crates/agent/tests/integration/context_overflow.rs
+++ b/crates/agent/tests/integration/context_overflow.rs
@@ -116,7 +116,7 @@ async fn overflow_recovers_once_only_without_output_and_with_remaining_main_budg
.filter(|event| matches!(
event.event.fact,
Fact::ModelRequested {
- purpose: Some(ModelPurpose::Main),
+ purpose: ModelPurpose::Main,
..
}
))
@@ -129,7 +129,7 @@ async fn overflow_recovers_once_only_without_output_and_with_remaining_main_budg
.filter(|event| matches!(
event.event.fact,
Fact::ModelRequested {
- purpose: Some(ModelPurpose::Summary),
+ purpose: ModelPurpose::Summary,
..
}
))
@@ -219,7 +219,7 @@ async fn proactive_failure_spends_the_same_budget_as_overflow_recovery() {
.filter(|event| matches!(
event.event.fact,
Fact::ModelRequested {
- purpose: Some(ModelPurpose::Summary),
+ purpose: ModelPurpose::Summary,
..
}
))
@@ -232,7 +232,7 @@ async fn proactive_failure_spends_the_same_budget_as_overflow_recovery() {
.filter(|event| matches!(
event.event.fact,
Fact::ModelRequested {
- purpose: Some(ModelPurpose::Main),
+ purpose: ModelPurpose::Main,
..
}
))
diff --git a/crates/agent/tests/integration/dynamic_tools.rs b/crates/agent/tests/integration/dynamic_tools.rs
index 3dfbaac78e..624ec8ef70 100644
--- a/crates/agent/tests/integration/dynamic_tools.rs
+++ b/crates/agent/tests/integration/dynamic_tools.rs
@@ -147,7 +147,7 @@ async fn search_activates_next_step_and_only_committed_compaction_unloads_direct
let mut input = fixture::input(&base, "discovery", false);
input.configuration.tool_mode = mode;
input.context = Some(ModelRequestContext { provider_id:"openai".into(), context_window:Some(220), declared_window:Some(220) });
- input.work = RunWork::Message { skill_invocation: Default::default(), source_messages:Vec::new(), message:"Use echo".into(), tools:catalog, max_steps:3 };
+ input.work = RunWork::Message { source_messages:Vec::new(), message:"Use echo".into(), tools:catalog, max_steps:3 };
let engine = fixture::engine(log.clone());
engine.run(input, CancellationToken::new()).await.unwrap();
engine.drain().await;
diff --git a/crates/agent/tests/integration/frozen_projection.rs b/crates/agent/tests/integration/frozen_projection.rs
index d6e24d7d54..f142674b5a 100644
--- a/crates/agent/tests/integration/frozen_projection.rs
+++ b/crates/agent/tests/integration/frozen_projection.rs
@@ -40,9 +40,9 @@ async fn settled(log: &EventLog, output: &ToolOutput) {
for fact in [
Fact::InvocationOpened {
configuration: None,
- input: InvocationInput::Message { skill_invocation: Default::default(), source_messages: Vec::new(), content: "prior".into(), request_fingerprint: None },
+ input: InvocationInput::Message { source_messages: Vec::new(), content: "prior".into(), request_fingerprint: None },
},
- Fact::ModelRequested { effective_source_digest: None, purpose: None, context: None, checkpoint_event_id: None, step_id: "step".into(), model_id: "test".into(),
+ Fact::ModelRequested { effective_source_digest: None, purpose: maka_runtime::context::ModelPurpose::Main, context: None, checkpoint_event_id: None, step_id: "step".into(), model_id: "test".into(),
source_scope: maka_runtime::event::LogScope::Session { id: "session".into() },
source_high_water: 1, source_digest: "fixture".into(), input_digest: "fixture".into(),
route_identity: "fixture".into() },
diff --git a/crates/agent/tests/integration/handoff.rs b/crates/agent/tests/integration/handoff.rs
index 1f2d8ebb28..9d2fc616fc 100644
--- a/crates/agent/tests/integration/handoff.rs
+++ b/crates/agent/tests/integration/handoff.rs
@@ -105,7 +105,7 @@ async fn handoff_waits_for_settlement_rollback_keeps_run_and_seal_survives_reope
for fact in [
Fact::InvocationOpened { configuration: Some(Box::new(run.configuration.clone())),
input: InvocationInput::Message { content: text.into(), request_fingerprint: None,
- source_messages: Vec::new(), skill_invocation: None } },
+ source_messages: Vec::new(), } },
Fact::InvocationEnded { outcome: InvocationOutcome::Cancelled { source: "user".into() } },
] {
log.append(&EventWrite::plain(RuntimeEvent::new(invocation.clone(), fact)).unwrap()).await.unwrap();
diff --git a/crates/agent/tests/integration/image_projection.rs b/crates/agent/tests/integration/image_projection.rs
index b63ed49210..0de0738a86 100644
--- a/crates/agent/tests/integration/image_projection.rs
+++ b/crates/agent/tests/integration/image_projection.rs
@@ -49,8 +49,8 @@ async fn reopened_user_and_typed_tool_images_share_actual_byte_budget_with_curre
let prior = identity("historical");
let message = serde_json::from_value(json!({"text":"historical", "attachments":[attachment("historical")]})).unwrap();
let facts = [
- Fact::InvocationOpened { input: InvocationInput::Message { skill_invocation: Default::default(), source_messages: Vec::new(), content: message, request_fingerprint: None }, configuration: None },
- Fact::ModelRequested { effective_source_digest: None, purpose: None, context: None, checkpoint_event_id: None, step_id: "prior-step".into(), model_id: "test".into(),
+ Fact::InvocationOpened { input: InvocationInput::Message { source_messages: Vec::new(), content: message, request_fingerprint: None }, configuration: None },
+ Fact::ModelRequested { effective_source_digest: None, purpose: maka_runtime::context::ModelPurpose::Main, context: None, checkpoint_event_id: None, step_id: "prior-step".into(), model_id: "test".into(),
source_scope: maka_runtime::event::LogScope::Session { id: "session".into() },
source_high_water: 1, source_digest: "fixture".into(), input_digest: "fixture".into(), route_identity: "fixture".into() },
Fact::ModelCompleted { step_id: "prior-step".into(), output: serde_json::from_value(json!({
diff --git a/crates/agent/tests/integration/lifetime.rs b/crates/agent/tests/integration/lifetime.rs
index c19b8e314d..963c0eb94a 100644
--- a/crates/agent/tests/integration/lifetime.rs
+++ b/crates/agent/tests/integration/lifetime.rs
@@ -92,7 +92,6 @@ fn input(base: &str, suffix: &str, effect: Arc<Effect>) -> RunInput {
supports_vision: false,
configuration: invocation::configuration(ToolMode::Direct),
work: maka_agent::RunWork::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
message: format!("question {suffix}").into(),
tools: ToolCatalog::new([ToolRegistration {
diff --git a/crates/agent/tests/integration/model_interruption.rs b/crates/agent/tests/integration/model_interruption.rs
index 9a114c52bc..1cf24b952a 100644
--- a/crates/agent/tests/integration/model_interruption.rs
+++ b/crates/agent/tests/integration/model_interruption.rs
@@ -61,7 +61,6 @@ fn input(base: &str, suffix: &str) -> RunInput {
supports_vision: false,
configuration: invocation::configuration(ToolMode::Direct),
work: maka_agent::RunWork::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
message: format!("question {suffix}").into(),
tools: Default::default(),
@@ -202,12 +201,12 @@ async fn continuation_claim_replays_only_its_lineage_and_retries_at_fresh_bounda
let opening = |text: &str| Fact::InvocationOpened {
configuration: Some(Box::new(source.configuration.clone())),
input: InvocationInput::Message { content: text.into(), request_fingerprint: None,
- source_messages: Vec::new(), skill_invocation: None },
+ source_messages: Vec::new(), },
};
let route = format!("sha256:{:x}", Sha256::digest(serde_json::to_vec(&source.provider).unwrap()));
for fact in [
opening("question source"),
- Fact::ModelRequested { step_id: "old".into(), model_id: "test".into(), purpose: None, context: None,
+ Fact::ModelRequested { step_id: "old".into(), model_id: "test".into(), purpose: maka_runtime::context::ModelPurpose::Main, context: None,
source_scope: LogScope::Session { id: "session".into() }, source_high_water: 1,
source_digest: "fixture".into(), input_digest: "fixture".into(), route_identity: route,
checkpoint_event_id: None, effective_source_digest: None },
diff --git a/crates/agent/tests/integration/plugin_composition.rs b/crates/agent/tests/integration/plugin_composition.rs
index 4c11518d54..197f114031 100644
--- a/crates/agent/tests/integration/plugin_composition.rs
+++ b/crates/agent/tests/integration/plugin_composition.rs
@@ -156,7 +156,7 @@ async fn plugin_step_surface_survives_retry_changes_only_between_steps_and_reope
let mut input = fixture::input(&base, "composition", false);
let session = input.invocation.session_id.clone();
input.work = RunWork::Message {
- skill_invocation: None, source_messages: Vec::new(), message: "Use echo".into(),
+ source_messages: Vec::new(), message: "Use echo".into(),
tools: ToolCatalog::default().with_plugins(catalog, Scope::Session(session.clone()), None).unwrap(),
max_steps: 3,
};
diff --git a/crates/agent/tests/integration/pruning.rs b/crates/agent/tests/integration/pruning.rs
index b7ff3f4685..1c6e5f8971 100644
--- a/crates/agent/tests/integration/pruning.rs
+++ b/crates/agent/tests/integration/pruning.rs
@@ -168,7 +168,6 @@ async fn settled_results_are_pruned_before_context_limits_and_survive_failed_sum
declared_window: spent.then_some(220),
});
input.work = RunWork::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
message: "read".into(),
tools: catalog(count.clone(), repeats),
@@ -239,7 +238,7 @@ async fn settled_results_are_pruned_before_context_limits_and_survive_failed_sum
.filter(|stored| matches!(
stored.event.fact,
Fact::ModelRequested {
- purpose: Some(maka_runtime::context::ModelPurpose::Summary),
+ purpose: maka_runtime::context::ModelPurpose::Summary,
..
}
))
@@ -256,7 +255,7 @@ async fn settled_results_are_pruned_before_context_limits_and_survive_failed_sum
{
assert_eq!(
effective_source_digest.is_some(),
- purpose == Some(maka_runtime::context::ModelPurpose::Summary)
+ purpose == maka_runtime::context::ModelPurpose::Summary
);
}
}
diff --git a/crates/agent/tests/integration/stale_pruning.rs b/crates/agent/tests/integration/stale_pruning.rs
index 4a82d35a4d..eecf6b2bca 100644
--- a/crates/agent/tests/integration/stale_pruning.rs
+++ b/crates/agent/tests/integration/stale_pruning.rs
@@ -38,8 +38,8 @@ async fn legacy(log: &EventLog, id: &str, repeats: usize) {
let operation = format!("{step}:call-{id}");
for fact in [
Fact::InvocationOpened { configuration: None,
- input: InvocationInput::Message { skill_invocation: Default::default(), source_messages: Vec::new(), content: format!("question-{id}").into(), request_fingerprint: None } },
- Fact::ModelRequested { purpose: None, context: None, checkpoint_event_id: None, effective_source_digest: None,
+ input: InvocationInput::Message { source_messages: Vec::new(), content: format!("question-{id}").into(), request_fingerprint: None } },
+ Fact::ModelRequested { purpose: maka_runtime::context::ModelPurpose::Main, context: None, checkpoint_event_id: None, effective_source_digest: None,
step_id: step.clone(), model_id: "test".into(), source_scope: LogScope::Session { id: "session".into() },
source_high_water: 1, source_digest: "legacy".into(), input_digest: "legacy".into(), route_identity: "legacy".into() },
Fact::ModelCompleted { step_id: step.clone(), output: serde_json::from_value(json!({
@@ -146,7 +146,7 @@ async fn history_pruning_binds_post_coverage_first_pages_into_summary_after_reop
.iter()
.find_map(|stored| match &stored.event.fact {
Fact::ModelRequested {
- purpose: Some(ModelPurpose::Summary),
+ purpose: ModelPurpose::Summary,
source_high_water,
effective_source_digest: Some(digest),
..
diff --git a/crates/agent/tests/integration/startup_recovery.rs b/crates/agent/tests/integration/startup_recovery.rs
index 128f87495f..ae9ff6a183 100644
--- a/crates/agent/tests/integration/startup_recovery.rs
+++ b/crates/agent/tests/integration/startup_recovery.rs
@@ -307,7 +307,6 @@ async fn unknown_dispatch_stays_unknown_and_blocks_next_admission_after_reopen()
supports_vision: false,
configuration: invocation::configuration(ToolMode::Direct),
work: maka_agent::RunWork::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
message: "continue".into(),
tools: Default::default(),
diff --git a/crates/agent/tests/integration/support/agent_loop.rs b/crates/agent/tests/integration/support/agent_loop.rs
index 6ba15ef783..0b530a0579 100644
--- a/crates/agent/tests/integration/support/agent_loop.rs
+++ b/crates/agent/tests/integration/support/agent_loop.rs
@@ -96,7 +96,6 @@ pub(crate) fn input(base: &str, suffix: &str, effect: Arc<Effect>) -> RunInput {
supports_vision: false,
configuration: invocation::configuration(ToolMode::Direct),
work: maka_agent::RunWork::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
message: format!("question {suffix}").into(),
tools: ToolCatalog::new([ToolRegistration {
@@ -128,7 +127,6 @@ fn source(
},
submitted_placement: maka_runtime::message::Placement::CurrentTurn,
disposition,
- skill_invocation: Default::default(),
submitted_intent: None,
}
}
diff --git a/crates/agent/tests/integration/support/code_mode.rs b/crates/agent/tests/integration/support/code_mode.rs
index ed4bf1afbc..7345f7b92e 100644
--- a/crates/agent/tests/integration/support/code_mode.rs
+++ b/crates/agent/tests/integration/support/code_mode.rs
@@ -125,7 +125,6 @@ pub fn input(base: &str, suffix: &str, effects: Arc<Effects>) -> RunInput {
supports_vision: false,
configuration: invocation::configuration(ToolMode::CodeMode),
work: maka_agent::RunWork::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
message: format!("question {suffix}").into(),
tools,
diff --git a/crates/agent/tests/integration/support/context.rs b/crates/agent/tests/integration/support/context.rs
index 0074372910..86beb8ddf0 100644
--- a/crates/agent/tests/integration/support/context.rs
+++ b/crates/agent/tests/integration/support/context.rs
@@ -80,7 +80,6 @@ pub fn input(base: &str, id: &str, compact: bool) -> RunInput {
RunWork::ContextCompact
} else {
RunWork::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
message: format!("question-{id}").into(),
tools: Default::default(),
diff --git a/crates/agent/tests/integration/support/image_projection.rs b/crates/agent/tests/integration/support/image_projection.rs
index 932ad50a71..82f6a585fc 100644
--- a/crates/agent/tests/integration/support/image_projection.rs
+++ b/crates/agent/tests/integration/support/image_projection.rs
@@ -71,7 +71,6 @@ pub fn input(base: &str, attachments: Value) -> RunInput {
supports_vision: true,
configuration: invocation::configuration(maka_tools::ToolMode::Direct),
work: maka_agent::RunWork::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
message: serde_json::from_value(json!({"text":"current", "attachments":attachments}))
.unwrap(),
diff --git a/crates/agent/tests/integration/support/recovery.rs b/crates/agent/tests/integration/support/recovery.rs
index cf8a9dd6da..b6dc5f2dc4 100644
--- a/crates/agent/tests/integration/support/recovery.rs
+++ b/crates/agent/tests/integration/support/recovery.rs
@@ -43,7 +43,6 @@ pub async fn opening(log: &EventLog) -> RuntimeEvent {
Fact::InvocationOpened {
configuration: None,
input: maka_runtime::input::InvocationInput::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
content: "perform the operation".into(),
request_fingerprint: Some("stable".into()),
@@ -68,7 +67,7 @@ pub async fn request(log: &EventLog) {
log,
Fact::ModelRequested {
effective_source_digest: None,
- purpose: None,
+ purpose: maka_runtime::context::ModelPurpose::Main,
context: None,
checkpoint_event_id: None,
step_id: "step".into(),
diff --git a/crates/cli/migrations/deployment/0001_deployment.sql b/crates/cli/migrations/deployment/0001_deployment.sql
index 0444da03cd..b926a765cb 100644
--- a/crates/cli/migrations/deployment/0001_deployment.sql
+++ b/crates/cli/migrations/deployment/0001_deployment.sql
@@ -21,4 +21,16 @@ CREATE TABLE deployment (
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
configuration TEXT NOT NULL CHECK (json_valid(configuration) AND length(CAST(configuration AS BLOB)) <= 65536)
) STRICT;
+
+-- Pending intent does not authorize startup; only the active deployment does.
+CREATE TABLE deployment_update (
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
+ target TEXT NOT NULL CHECK (json_valid(target) AND length(CAST(target AS BLOB)) <= 65536)
+) STRICT;
+
+CREATE TABLE deployment_update_policy (
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
+ configuration TEXT NOT NULL CHECK (json_valid(configuration) AND length(CAST(configuration AS BLOB)) <= 16384)
+) STRICT;
+
PRAGMA user_version = 1;
diff --git a/crates/cli/migrations/deployment/0002_update.sql b/crates/cli/migrations/deployment/0002_update.sql
deleted file mode 100644
index c23b42555f..0000000000
--- a/crates/cli/migrations/deployment/0002_update.sql
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- The Active reader remains version 1. Pending intent does not authorize startup.
-CREATE TABLE deployment_update (
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
- target TEXT NOT NULL CHECK (json_valid(target) AND length(CAST(target AS BLOB)) <= 65536)
-) STRICT;
diff --git a/crates/cli/migrations/deployment/0003_update_policy.sql b/crates/cli/migrations/deployment/0003_update_policy.sql
deleted file mode 100644
index 80b943adeb..0000000000
--- a/crates/cli/migrations/deployment/0003_update_policy.sql
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- Optional authority; old read-only launchers still use the stable Active row.
-CREATE TABLE deployment_update_policy (
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
- configuration TEXT NOT NULL CHECK (json_valid(configuration) AND length(CAST(configuration AS BLOB)) <= 16384)
-) STRICT;
diff --git a/crates/cli/src/deployment/policy/storage.rs b/crates/cli/src/deployment/policy/storage.rs
index a3b8afac20..f18be562da 100644
--- a/crates/cli/src/deployment/policy/storage.rs
+++ b/crates/cli/src/deployment/policy/storage.rs
@@ -31,12 +31,7 @@ pub(in crate::deployment) async fn read(directory: &Path) -> Result<Record, Host
.read_only(true),
)
.await?;
- let result = async {
- let exists: bool = sqlx::query_scalar(
- "SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type='table' AND name='deployment_update_policy')"
- ).fetch_one(&mut connection).await?;
- if exists { Ok::<_, HostError>(load(&mut connection).await?) } else { Ok(Record::default()) }
- }.await;
+ let result = load(&mut connection).await;
let closed = connection.close().await;
let record = result?;
closed?;
diff --git a/crates/cli/src/deployment/query.rs b/crates/cli/src/deployment/query.rs
index 1154f1c096..6c40b0bd7b 100644
--- a/crates/cli/src/deployment/query.rs
+++ b/crates/cli/src/deployment/query.rs
@@ -171,8 +171,6 @@ pub(super) async fn pending(
directory: &Path,
current: &Deployment,
) -> Result<Option<Deployment>, HostError> {
- // Earlier installed operators have only the Active table. Observation must
- // not run migrations or create a pending table on their behalf.
let mut connection = SqliteConnection::connect_with(
&SqliteConnectOptions::new()
.filename(directory.join("deployment.sqlite"))
@@ -180,10 +178,6 @@ pub(super) async fn pending(
)
.await?;
let result = async {
- let exists: bool = sqlx::query_scalar(
- "SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type='table' AND name='deployment_update')",
- ).fetch_one(&mut connection).await?;
- if !exists { return Ok(None) }
let value: Option<String> = sqlx::query_scalar(
"SELECT CASE WHEN length(CAST(target AS BLOB)) <= 65536 THEN target ELSE '' END FROM deployment_update WHERE singleton = 1",
).fetch_optional(&mut connection).await?;
diff --git a/crates/cli/src/deployment/store.rs b/crates/cli/src/deployment/store.rs
index aca1b9b0bd..5c053792a1 100644
--- a/crates/cli/src/deployment/store.rs
+++ b/crates/cli/src/deployment/store.rs
@@ -30,7 +30,6 @@ use std::{fs::File, path::Path, sync::Arc};
const DATABASE: &str = "deployment.sqlite";
const APPLICATION_ID: i64 = 0x4d414b44;
static MIGRATIONS: sqlx::migrate::Migrator = sqlx::migrate!("./migrations/deployment");
-// user_version describes the stable Active reader, not the SQLx migration count.
pub(super) enum Installation {
Missing,
@@ -54,6 +53,7 @@ pub(super) async fn read(directory: &Path) -> Result<Installation, HostError> {
SqliteConnection::connect_with(&SqliteConnectOptions::new().filename(path).read_only(true))
.await?;
let result = async {
+ validate_schema(&mut connection).await?;
let application: i64 = sqlx::query_scalar("PRAGMA application_id").fetch_one(&mut connection).await?;
let version: i64 = sqlx::query_scalar("PRAGMA user_version").fetch_one(&mut connection).await?;
if version == 0 && application == APPLICATION_ID {
@@ -274,6 +274,18 @@ fn private_file(path: &Path) -> Result<File, HostError> {
}
async fn initialize(connection: &mut SqliteConnection) -> Result<(), StoreError> {
+ validate_schema(connection).await?;
+ sqlx::raw_sql(
+ "PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA application_id=1296124740;",
+ )
+ .execute(&mut *connection)
+ .await?;
+ Ok(())
+}
+
+/// Reject unsupported stores before either a reader serves them or a writer
+/// changes journaling or creates SQLx metadata. Version zero is initial setup only.
+async fn validate_schema(connection: &mut SqliteConnection) -> Result<(), StoreError> {
let application: i64 = sqlx::query_scalar("PRAGMA application_id")
.fetch_one(&mut *connection)
.await?;
@@ -292,11 +304,35 @@ async fn initialize(connection: &mut SqliteConnection) -> Result<(), StoreError>
"unsupported deployment database".into(),
));
}
- sqlx::raw_sql(
- "PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA application_id=1296124740;",
+ let ledger: bool = sqlx::query_scalar(
+ "SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type='table' AND name='_sqlx_migrations')",
)
- .execute(&mut *connection)
+ .fetch_one(&mut *connection)
.await?;
+ let applied: Vec<(i64, bool, Vec<u8>)> = if ledger {
+ sqlx::query_as("SELECT version, success, checksum FROM _sqlx_migrations ORDER BY version")
+ .fetch_all(&mut *connection)
+ .await?
+ } else {
+ Vec::new()
+ };
+ let valid = if version == 0 {
+ tables == i64::from(ledger) && applied.is_empty()
+ } else {
+ applied.len() == MIGRATIONS.iter().len()
+ && applied.iter().zip(MIGRATIONS.iter()).all(
+ |((version, success, checksum), expected)| {
+ *version == expected.version
+ && *success
+ && checksum.as_slice() == expected.checksum.as_ref()
+ },
+ )
+ };
+ if !valid {
+ return Err(StoreError::InvalidTransition(
+ "unsupported deployment schema".into(),
+ ));
+ }
Ok(())
}
diff --git a/crates/cli/src/deployment/updates.rs b/crates/cli/src/deployment/updates.rs
index f723c6cfb4..0689b2395c 100644
--- a/crates/cli/src/deployment/updates.rs
+++ b/crates/cli/src/deployment/updates.rs
@@ -194,8 +194,7 @@ mod tests {
.await
.unwrap();
writer.run(|connection| Box::pin(async move {
- // Model the published predecessor and a failure after migration.
- sqlx::raw_sql("DROP TABLE deployment_update_policy; DELETE FROM _sqlx_migrations WHERE version = 3; CREATE TRIGGER reject_prepare BEFORE INSERT ON deployment_update BEGIN SELECT RAISE(ABORT, 'prepare failed'); END;")
+ sqlx::raw_sql("CREATE TRIGGER reject_prepare BEFORE INSERT ON deployment_update BEGIN SELECT RAISE(ABORT, 'prepare failed'); END;")
.execute(connection).await?;
Ok(())
})).await.unwrap();
@@ -208,15 +207,17 @@ mod tests {
let writer = store::open_update_writer(&directory, lease.clone())
.await
.unwrap();
- writer.run(|connection| Box::pin(async move {
- let migrated: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM _sqlx_migrations WHERE version = 3)")
- .fetch_one(&mut *connection).await?;
- let table: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE name = 'deployment_update_policy')")
- .fetch_one(&mut *connection).await?;
- assert!(!migrated && !table, "failed preparation must also roll back migrations");
- sqlx::query("DROP TRIGGER reject_prepare").execute(connection).await?;
- Ok(())
- })).await.unwrap();
+ writer
+ .run(|connection| {
+ Box::pin(async move {
+ sqlx::query("DROP TRIGGER reject_prepare")
+ .execute(connection)
+ .await?;
+ Ok(())
+ })
+ })
+ .await
+ .unwrap();
writer.close().await.unwrap();
assert_eq!(
read(&directory, lease.clone()).await.unwrap(),
@@ -248,7 +249,7 @@ mod tests {
prepare(&directory, lease.clone(), current.clone(), target.clone())
.await
.unwrap();
- // Old startup readers still see the sole authorized version while an
+ // Startup readers still see the sole authorized version while an
// updater is absent. A different target cannot overwrite its intent.
let store::Installation::Installed(active) = store::read(&directory).await.unwrap() else {
panic!("pending intent must not change the startup reader contract");
@@ -372,7 +373,36 @@ mod tests {
)
.await
.unwrap();
- assert_eq!(read(&directory, lease).await.unwrap(), (active, None));
+ assert_eq!(
+ read(&directory, lease.clone()).await.unwrap(),
+ (active, None)
+ );
+ let writer = store::open_writer(&directory, lease.clone(), None)
+ .await
+ .unwrap();
+ writer
+ .run(|connection| {
+ Box::pin(async move {
+ sqlx::raw_sql(
+ "PRAGMA journal_mode=DELETE; UPDATE _sqlx_migrations SET checksum = X'00';",
+ )
+ .execute(connection)
+ .await?;
+ Ok(())
+ })
+ })
+ .await
+ .unwrap();
+ writer.close().await.unwrap();
+ let path = directory.join("deployment.sqlite");
+ let before = std::fs::read(&path).unwrap();
+ assert!(store::read(&directory).await.is_err());
+ assert!(store::open_writer(&directory, lease, None).await.is_err());
+ assert_eq!(
+ std::fs::read(path).unwrap(),
+ before,
+ "unsupported stores must not be rewritten"
+ );
drop(owner);
drop(RootOwner::open(&root_path, &namespaces).unwrap());
}
diff --git a/crates/cli/src/host_client.rs b/crates/cli/src/host_client.rs
index 808d52f919..73d66e7b08 100644
--- a/crates/cli/src/host_client.rs
+++ b/crates/cli/src/host_client.rs
@@ -90,7 +90,6 @@ impl HostClient {
writer
.write(&ClientHello {
client_instance_id: format!("maka-operator-{}", uuid::Uuid::new_v4()),
- activity_snapshot_version: Some(2),
protocol_min: 0,
protocol_max: 0,
compatibility_epoch: COMPATIBILITY_EPOCH,
diff --git a/crates/cli/tests/integration/candidate.rs b/crates/cli/tests/integration/candidate.rs
index 468bbd64ee..d0f5497ba3 100644
--- a/crates/cli/tests/integration/candidate.rs
+++ b/crates/cli/tests/integration/candidate.rs
@@ -321,7 +321,6 @@ impl Drop for CandidateFixture {
let _ = fs::remove_dir_all(deployment);
}
}
- let _ = fs::remove_file(control.join("owner.lock"));
let _ = fs::remove_dir(control);
let _ = fs::remove_file(&self.lock);
}
diff --git a/crates/client-capability/src/broker.rs b/crates/client-capability/src/broker.rs
index 5ed6de2879..5e9d3c0982 100644
--- a/crates/client-capability/src/broker.rs
+++ b/crates/client-capability/src/broker.rs
@@ -136,8 +136,11 @@ impl Broker {
let frame = frame(invocation_id.clone());
let tool = match &frame {
HostFrame::Call {
- session_id,
- turn_id,
+ source:
+ maka_runtime::capability::CallSource::Agent {
+ session_id,
+ turn_id,
+ },
tool_call_id,
server_id,
tool_name,
diff --git a/crates/client-capability/src/broker/dispatch.rs b/crates/client-capability/src/broker/dispatch.rs
index 43cc581103..cc1bf815b9 100644
--- a/crates/client-capability/src/broker/dispatch.rs
+++ b/crates/client-capability/src/broker/dispatch.rs
@@ -19,7 +19,7 @@
use super::{Broker, CallError, PendingCall};
use crate::Registration;
-use maka_runtime::capability::{HostFrame, HostPathAccess};
+use maka_runtime::capability::{CallSource, HostFrame, HostPathAccess};
use serde_json::{Map, Value};
use std::{sync::Arc, time::Duration};
use tokio_util::sync::CancellationToken;
@@ -38,8 +38,7 @@ pub struct ToolCall {
pub server_id: String,
pub tool_name: String,
pub arguments: Map<String, Value>,
- pub session_id: String,
- pub turn_id: String,
+ pub source: CallSource,
pub tool_call_id: String,
pub cwd: String,
}
@@ -111,8 +110,7 @@ impl Broker {
server_id: call.server_id,
tool_name: call.tool_name,
arguments: call.arguments,
- session_id: call.session_id,
- turn_id: call.turn_id,
+ source: call.source,
tool_call_id: call.tool_call_id,
cwd,
}
diff --git a/crates/client-capability/tests/client_service.rs b/crates/client-capability/tests/client_service.rs
index 6cd656be13..a2efac67ce 100644
--- a/crates/client-capability/tests/client_service.rs
+++ b/crates/client-capability/tests/client_service.rs
@@ -193,8 +193,17 @@ async fn service_roundtrip() {
server_id: format!("test-server-{access}"),
tool_name: "write".into(),
arguments: serde_json::from_value(json!({"text": access})).unwrap(),
- session_id: "interop-session".into(),
- turn_id: "interop-turn".into(),
+ source: if index == 0 {
+ maka_runtime::capability::CallSource::Agent {
+ session_id: "interop-session".into(),
+ turn_id: "interop-turn".into(),
+ }
+ } else {
+ maka_runtime::capability::CallSource::Background {
+ session_id: None,
+ grant_id: "authorized-grant".into(),
+ }
+ },
tool_call_id: format!("interop-call-{access}"),
cwd: workspace.path().to_str().unwrap().into(),
},
diff --git a/crates/client-capability/tests/journal_admission.rs b/crates/client-capability/tests/journal_admission.rs
index 1d7a0bcd4a..8cf6d24d0f 100644
--- a/crates/client-capability/tests/journal_admission.rs
+++ b/crates/client-capability/tests/journal_admission.rs
@@ -70,7 +70,6 @@ async fn run(boundary: Boundary) {
Fact::InvocationOpened {
configuration: None,
input: InvocationInput::Message {
- skill_invocation: Default::default(),
source_messages: Vec::new(),
content: "invoke".into(),
request_fingerprint: None,
@@ -118,8 +117,10 @@ async fn run(boundary: Boundary) {
server_id: "server".into(),
tool_name: "effect".into(),
arguments: serde_json::Map::new(),
- session_id: invocation.session_id.clone(),
- turn_id: invocation.turn_id.clone(),
+ source: maka_runtime::capability::CallSource::Agent {
+ session_id: invocation.session_id.clone(),
+ turn_id: invocation.turn_id.clone(),
+ },
tool_call_id: "call".into(),
cwd: "/host/private/workspace".into(),
},
diff --git a/crates/config/migrations/0001_configuration.sql b/crates/config/migrations/0001_configuration.sql
index 6f9e1521d6..a0646847bc 100644
--- a/crates/config/migrations/0001_configuration.sql
+++ b/crates/config/migrations/0001_configuration.sql
@@ -17,28 +17,70 @@
* under the License.
*/
-CREATE TABLE IF NOT EXISTS connection_catalog (
+CREATE TABLE connection_catalog (
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
revision INTEGER NOT NULL CHECK(revision >= 0),
default_target TEXT
);
-INSERT OR IGNORE INTO connection_catalog VALUES(1, 0, NULL);
-CREATE TABLE IF NOT EXISTS connections (
+INSERT INTO connection_catalog VALUES(1, 0, NULL);
+CREATE TABLE connections (
connection_id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
revision INTEGER NOT NULL CHECK(revision > 0),
document TEXT NOT NULL
);
-CREATE TABLE IF NOT EXISTS credential_vault (
+CREATE TABLE credential_vault (
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
revision INTEGER NOT NULL CHECK(revision >= 0)
);
-INSERT OR IGNORE INTO credential_vault VALUES(1, 0);
-CREATE TABLE IF NOT EXISTS credentials (
+INSERT INTO credential_vault VALUES(1, 0);
+CREATE TABLE credentials (
locator TEXT PRIMARY KEY,
credential_id TEXT NOT NULL UNIQUE,
revision INTEGER NOT NULL CHECK(revision > 0),
secret TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
+
+CREATE TABLE access_credentials (
+ document TEXT NOT NULL CHECK(json_valid(document) AND length(CAST(document AS BLOB)) <= 524288),
+ credential_id TEXT GENERATED ALWAYS AS (json_extract(document, '$.credentialId')) STORED NOT NULL UNIQUE,
+ credential_hash TEXT GENERATED ALWAYS AS (json_extract(document, '$.credentialHash')) STORED NOT NULL UNIQUE
+ CHECK(length(credential_hash) = 64)
+);
+
+CREATE TABLE runtime_policy (
+ singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
+ document TEXT NOT NULL CHECK(
+ json_valid(document) AND length(CAST(document AS BLOB)) <= 49152
+ )
+);
+
+-- Receipts survive logout/removal: they prove completion, not current authorization.
+CREATE TABLE oauth_login_receipts (
+ completion_order INTEGER PRIMARY KEY AUTOINCREMENT,
+ attempt_id TEXT NOT NULL UNIQUE CHECK(length(attempt_id) BETWEEN 1 AND 128),
+ target TEXT NOT NULL,
+ connection TEXT NOT NULL
+);
+
+CREATE TABLE plugin_credentials (
+ package_id TEXT NOT NULL,
+ scope_id TEXT NOT NULL,
+ key TEXT NOT NULL,
+ revision INTEGER NOT NULL CHECK(revision BETWEEN 1 AND 9007199254740991),
+ secret TEXT CHECK(secret IS NULL OR length(CAST(secret AS BLOB)) <= 65536),
+ PRIMARY KEY (package_id, scope_id, key)
+) STRICT;
+
+CREATE TABLE plugin_authorizations (
+ id TEXT PRIMARY KEY NOT NULL,
+ package_id TEXT NOT NULL,
+ scope_id TEXT NOT NULL,
+ operation_id TEXT NOT NULL,
+ document TEXT NOT NULL CHECK(length(CAST(document AS BLOB)) <= 65536),
+ revoked INTEGER NOT NULL DEFAULT 0 CHECK(revoked IN (0, 1)),
+ UNIQUE(package_id, scope_id, operation_id)
+) STRICT;
+
PRAGMA user_version = 1;
diff --git a/crates/config/migrations/0002_access_credentials.sql b/crates/config/migrations/0002_access_credentials.sql
deleted file mode 100644
index 5d5245cef8..0000000000
--- a/crates/config/migrations/0002_access_credentials.sql
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-CREATE TABLE access_credentials (
- document TEXT NOT NULL CHECK(json_valid(document) AND length(CAST(document AS BLOB)) <= 524288),
- credential_id TEXT GENERATED ALWAYS AS (json_extract(document, '$.credentialId')) STORED NOT NULL UNIQUE,
- credential_hash TEXT GENERATED ALWAYS AS (json_extract(document, '$.credentialHash')) STORED NOT NULL UNIQUE
- CHECK(length(credential_hash) = 64)
-);
-PRAGMA user_version = 2;
diff --git a/crates/config/migrations/0003_runtime_policy.sql b/crates/config/migrations/0003_runtime_policy.sql
deleted file mode 100644
index ede6a6a68d..0000000000
--- a/crates/config/migrations/0003_runtime_policy.sql
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
-CREATE TABLE runtime_policy (
- singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
- document TEXT NOT NULL CHECK(
- json_valid(document) AND length(CAST(document AS BLOB)) <= 49152
- )
-);
-
-PRAGMA user_version = 3;
diff --git a/crates/config/migrations/0004_model_facts.sql b/crates/config/migrations/0004_model_facts.sql
deleted file mode 100644
index e0c95b389a..0000000000
--- a/crates/config/migrations/0004_model_facts.sql
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
--- This is the catalog's observed projection basis, not another revision.
-ALTER TABLE connection_catalog ADD COLUMN model_facts_fingerprint TEXT
- CHECK(model_facts_fingerprint IS NULL OR length(model_facts_fingerprint) = 71);
-PRAGMA user_version = 4;
diff --git a/crates/config/migrations/0005_oauth_login.sql b/crates/config/migrations/0005_oauth_login.sql
deleted file mode 100644
index 2b72345e83..0000000000
--- a/crates/config/migrations/0005_oauth_login.sql
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- Receipts survive logout/removal: they prove completion, not current authorization.
-CREATE TABLE oauth_login_receipts (
- completion_order INTEGER PRIMARY KEY AUTOINCREMENT,
- attempt_id TEXT NOT NULL UNIQUE CHECK(length(attempt_id) BETWEEN 1 AND 128),
- target TEXT NOT NULL,
- connection TEXT NOT NULL
-);
-PRAGMA user_version = 5;
diff --git a/crates/config/migrations/0006_external_agents.sql b/crates/config/migrations/0006_external_agents.sql
deleted file mode 100644
index bf9723de06..0000000000
--- a/crates/config/migrations/0006_external_agents.sql
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- Preserve full old documents when the new field exceeds the wire budget.
--- Normal writes still enforce 48 KiB; recovery must not prevent shrinking them.
-CREATE TABLE runtime_policy_next (
- singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
- document TEXT NOT NULL CHECK(
- json_valid(document) AND length(CAST(document AS BLOB)) <= 65536
- )
-);
-INSERT INTO runtime_policy_next
-SELECT singleton, json_insert(document, '$.policy.externalAgents',
- json('{"antigravity":{"executable":""}}')) FROM runtime_policy;
-DROP TABLE runtime_policy;
-ALTER TABLE runtime_policy_next RENAME TO runtime_policy;
-
-PRAGMA user_version = 6;
diff --git a/crates/config/migrations/0007_model_overrides.sql b/crates/config/migrations/0007_model_overrides.sql
deleted file mode 100644
index 4f7accd4fa..0000000000
--- a/crates/config/migrations/0007_model_overrides.sql
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- A legacy relay window also declared proactive compaction. Preserve both
--- meanings while new writes keep capacity and compaction independent.
-UPDATE connections
-SET document = json_remove(
- json_insert(document, '$.modelOverrides', json((
- SELECT json_group_object(key, json(
- CASE WHEN json_type(value, '$.contextWindow') = 'integer'
- THEN json_insert(value, '$.compactionThreshold',
- json_extract(value, '$.contextWindow'))
- ELSE value END))
- FROM json_each(connections.document, '$.relayModelProfiles')
- ))),
- '$.relayModelProfiles'
-)
-WHERE json_type(document, '$.relayModelProfiles') = 'object';
-
-PRAGMA user_version = 7;
diff --git a/crates/config/migrations/0008_connection_model_authority.sql b/crates/config/migrations/0008_connection_model_authority.sql
deleted file mode 100644
index 0f11c59525..0000000000
--- a/crates/config/migrations/0008_connection_model_authority.sql
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- Verification tied to an external facts file cannot establish the new
--- connection-owned wire. Leave external files untouched; never import TS state.
-UPDATE connections
-SET document = json_remove(document, '$.lastTest', '$.lastTestModelFactsFingerprint')
-WHERE json_type(document, '$.lastTestModelFactsFingerprint') IS NOT NULL;
-
-ALTER TABLE connection_catalog DROP COLUMN model_facts_fingerprint;
-PRAGMA user_version = 8;
diff --git a/crates/config/migrations/0009_skill_preferences.sql b/crates/config/migrations/0009_skill_preferences.sql
deleted file mode 100644
index 776f59ca19..0000000000
--- a/crates/config/migrations/0009_skill_preferences.sql
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- Root-owned preferences are control state, not model history or imported TS data.
-CREATE TABLE skill_preferences (
- ref TEXT PRIMARY KEY NOT NULL CHECK (length(CAST(ref AS BLOB)) BETWEEN 1 AND 512),
- enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),
- pinned INTEGER NOT NULL CHECK (pinned IN (0, 1))
-) STRICT;
-CREATE TABLE skill_preferences_revision (
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
- revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991)
-) STRICT;
-INSERT INTO skill_preferences_revision VALUES (1, 0);
-PRAGMA user_version = 9;
-
diff --git a/crates/config/migrations/0010_plugin_credentials.sql b/crates/config/migrations/0010_plugin_credentials.sql
deleted file mode 100644
index cc0ad074d2..0000000000
--- a/crates/config/migrations/0010_plugin_credentials.sql
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-CREATE TABLE plugin_credentials (
- package_id TEXT NOT NULL,
- scope_id TEXT NOT NULL,
- key TEXT NOT NULL,
- revision INTEGER NOT NULL CHECK(revision BETWEEN 1 AND 9007199254740991),
- secret TEXT CHECK(secret IS NULL OR length(CAST(secret AS BLOB)) <= 65536),
- PRIMARY KEY (package_id, scope_id, key)
-) STRICT;
-PRAGMA user_version = 10;
diff --git a/crates/config/src/database.rs b/crates/config/src/database.rs
index 5988e65f68..c97d0b406c 100644
--- a/crates/config/src/database.rs
+++ b/crates/config/src/database.rs
@@ -107,21 +107,37 @@ async fn initialize(connection: &mut SqliteConnection) -> std::result::Result<()
.fetch_one(&mut *connection)
.await?;
if !((application_id == 0 && version == 0 && tables == 0)
- || (application_id == 0x4d414b43 && matches!(version, 0..=10)))
+ || (application_id == 0x4d414b43 && matches!(version, 0..=1)))
{
return Err(StoreError::UnsupportedDatabase);
}
- // Stamp identity before SQLx creates its ledger so interrupted setup can retry.
- sqlx::raw_sql(
- "PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; PRAGMA secure_delete = ON;",
+ let ledger: bool = sqlx::query_scalar(
+ "SELECT EXISTS(SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = '_sqlx_migrations')",
)
- .execute(&mut *connection)
+ .fetch_one(&mut *connection)
.await?;
+ if (version == 0 && tables != i64::from(ledger)) || (version == 1 && !ledger) {
+ return Err(StoreError::UnsupportedDatabase);
+ }
+ if ledger {
+ let applied: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM _sqlx_migrations")
+ .fetch_one(&mut *connection)
+ .await?;
+ if (version == 0) != (applied == 0) {
+ return Err(StoreError::UnsupportedDatabase);
+ }
+ }
+ // Stamp identity before SQLx creates its ledger so interrupted setup can retry.
if application_id == 0 {
sqlx::query("PRAGMA application_id = 1296124739")
.execute(&mut *connection)
.await?;
}
MIGRATIONS.run_direct(None, &mut *connection, false).await?;
+ sqlx::raw_sql(
+ "PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL; PRAGMA secure_delete = ON;",
+ )
+ .execute(&mut *connection)
+ .await?;
Ok(())
}
diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs
index 8f2a99cee8..ff6bb02863 100644
--- a/crates/config/src/lib.rs
+++ b/crates/config/src/lib.rs
@@ -30,10 +30,10 @@ pub mod model_fetch;
pub mod network;
pub mod oauth;
pub mod onboarding;
+pub mod plugin_authorization;
mod plugin_credentials;
mod policy;
pub mod projection;
-pub mod skills;
mod vault;
pub use maka_event_log::connection::BoxFuture;
diff --git a/crates/config/src/plugin_authorization.rs b/crates/config/src/plugin_authorization.rs
new file mode 100644
index 0000000000..1eca7dd33d
--- /dev/null
+++ b/crates/config/src/plugin_authorization.rs
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Host consent authority. Plugin data may refer to these records but cannot
+//! create, edit or reactivate them through the namespaced data API.
+
+use crate::{ConfigError, ConfigurationStore, Result, TransactionMode};
+pub use maka_plugins::authorization::Boundary;
+use maka_plugins::{
+ authorization::{Grant, Id, Request},
+ storage::Namespace,
+};
+use serde::{Deserialize, Serialize};
+use sqlx::{Row, sqlite::SqliteRow};
+use uuid::Uuid;
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(
+ tag = "kind",
+ rename_all = "snake_case",
+ rename_all_fields = "camelCase",
+ deny_unknown_fields
+)]
+pub enum Principal {
+ LocalUser {
+ client_instance_id: String,
+ },
+ Credential {
+ credential_id: String,
+ client_instance_id: String,
+ },
+}
+
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+struct Document {
+ request: Request,
+ principal: Principal,
+ boundary: Boundary,
+}
+pub struct Record {
+ pub grant: Grant,
+ pub principal: Principal,
+ pub boundary: Boundary,
+}
+pub enum Approval {
+ Granted(Box<Record>),
+ Conflict,
+}
+
+impl ConfigurationStore {
+ /// Exact retry returns the original boundary, including its revocation.
+ /// Re-approval requires a new operation identity and explicit consent.
+ pub async fn approve_plugin_authorization(
+ &self,
+ namespace: Namespace,
+ principal: Principal,
+ request: Request,
+ boundary: Boundary,
+ ) -> Result<Approval> {
+ request
+ .validate()
+ .map_err(|error| ConfigError::Invalid(error.to_string()))?;
+ self.transaction(TransactionMode::Immediate, move |connection| {
+ Box::pin(async move {
+ let scope = String::from(namespace.scope().clone());
+ let previous = sqlx::query("SELECT id, document, revoked FROM plugin_authorizations WHERE package_id = ? AND scope_id = ? AND operation_id = ?")
+ .bind(namespace.package()).bind(&scope).bind(request.operation_id.to_string())
+ .fetch_optional(&mut *connection).await?;
+ if let Some(row) = previous {
+ let record = decode(row)?;
+ return Ok(if record.grant.request == request && record.principal == principal {
+ Approval::Granted(Box::new(record))
+ } else {
+ Approval::Conflict
+ });
+ }
+ boundary.validate(&request)
+ .map_err(|error| ConfigError::Invalid(error.to_string()))?;
+ let id = Id(Uuid::new_v4());
+ let document = Document {
+ request,
+ principal,
+ boundary,
+ };
+ let bytes = serde_json::to_string(&document)?;
+ if bytes.len() > 64 * 1024 {
+ return Err(ConfigError::Invalid("authorization exceeds 64 KiB".into()));
+ }
+ sqlx::query("INSERT INTO plugin_authorizations (id, package_id, scope_id, operation_id, document) VALUES (?, ?, ?, ?, ?)")
+ .bind(id.0.to_string()).bind(namespace.package()).bind(scope).bind(document.request.operation_id.to_string()).bind(bytes)
+ .execute(&mut *connection).await?;
+ Ok(Approval::Granted(Box::new(Record {
+ grant: Grant {
+ id,
+ request: document.request,
+ revoked: false,
+ },
+ principal: document.principal,
+ boundary: document.boundary,
+ })))
+ })
+ }).await
+ }
+
+ /// Read does not confer authority. Host must also check the current principal,
+ /// workspace identity, Session boundary and live plugin ownership before use.
+ pub async fn plugin_authorization(
+ &self,
+ namespace: Namespace,
+ id: Id,
+ ) -> Result<Option<Record>> {
+ self.transaction(TransactionMode::Deferred, move |connection| {
+ Box::pin(async move {
+ sqlx::query("SELECT id, document, revoked FROM plugin_authorizations WHERE id = ? AND package_id = ? AND scope_id = ?")
+ .bind(id.0.to_string()).bind(namespace.package()).bind(String::from(namespace.scope().clone()))
+ .fetch_optional(&mut *connection).await?.map(decode).transpose()
+ })
+ }).await
+ }
+
+ /// Recover an approval receipt without recapturing a potentially changed
+ /// target. The caller must compare both proposal and issuing principal.
+ pub async fn plugin_authorization_operation(
+ &self,
+ namespace: Namespace,
+ operation_id: Uuid,
+ ) -> Result<Option<Record>> {
+ self.transaction(TransactionMode::Deferred, move |connection| {
+ Box::pin(async move {
+ sqlx::query("SELECT id, document, revoked FROM plugin_authorizations WHERE package_id = ? AND scope_id = ? AND operation_id = ?")
+ .bind(namespace.package()).bind(String::from(namespace.scope().clone())).bind(operation_id.to_string())
+ .fetch_optional(&mut *connection).await?.map(decode).transpose()
+ })
+ }).await
+ }
+
+ /// Idempotent and monotonic; never delete the idempotency tombstone.
+ pub async fn revoke_plugin_authorization(&self, namespace: Namespace, id: Id) -> Result<()> {
+ self.transaction(TransactionMode::Immediate, move |connection| {
+ Box::pin(async move {
+ sqlx::query("UPDATE plugin_authorizations SET revoked = 1 WHERE id = ? AND package_id = ? AND scope_id = ?")
+ .bind(id.0.to_string()).bind(namespace.package()).bind(String::from(namespace.scope().clone()))
+ .execute(&mut *connection).await?;
+ Ok(())
+ })
+ }).await
+ }
+}
+
+fn decode(row: SqliteRow) -> Result<Record> {
+ let id: String = row.try_get("id")?;
+ let document: Document = serde_json::from_str(row.try_get("document")?)?;
+ document
+ .request
+ .validate()
+ .map_err(|error| ConfigError::Invalid(error.to_string()))?;
+ document
+ .boundary
+ .validate(&document.request)
+ .map_err(|error| ConfigError::Invalid(error.to_string()))?;
+ Ok(Record {
+ grant: Grant {
+ id: Id(Uuid::parse_str(&id).map_err(|error| ConfigError::Invalid(error.to_string()))?),
+ request: document.request,
+ revoked: row.try_get::<i64, _>("revoked")? != 0,
+ },
+ principal: document.principal,
+ boundary: document.boundary,
+ })
+}
diff --git a/crates/config/src/policy.rs b/crates/config/src/policy.rs
index be8c15dba1..9f5ab6837f 100644
--- a/crates/config/src/policy.rs
+++ b/crates/config/src/policy.rs
@@ -21,16 +21,12 @@ use crate::{ConfigError, ConfigurationStore, Result, TransactionMode};
use maka_runtime::configuration::{
policy::{
ChatDefaults, EnabledPolicy, MAX_POLICY_SNAPSHOT_BYTES, Personalization, RuntimePolicy,
- RuntimePolicyMutationResult, RuntimePolicySnapshot, decode_canonical_document,
+ RuntimePolicyMutationResult, RuntimePolicySnapshot, decode_canonical_snapshot,
},
validation::{MAX_SAFE_INTEGER, revision},
};
use sqlx::SqliteConnection;
-// Matches migration 0006. Recovery has headroom for schema growth; ordinary
-// writes still require a policy that fits the unchanged 48 KiB wire budget.
-const MAX_POLICY_DOCUMENT_BYTES: usize = 64 * 1024;
-
impl ConfigurationStore {
pub async fn set_subagents(
&self,
@@ -162,7 +158,7 @@ pub(crate) async fn read(connection: &mut SqliteConnection) -> Result<RuntimePol
policy: RuntimePolicy::default(),
});
};
- if size < 0 || size > MAX_POLICY_DOCUMENT_BYTES as i64 {
+ if size < 0 || size > MAX_POLICY_SNAPSHOT_BYTES as i64 {
return Err(ConfigError::UnsupportedDatabase);
}
let document: String =
@@ -170,5 +166,5 @@ pub(crate) async fn read(connection: &mut SqliteConnection) -> Result<RuntimePol
.fetch_one(connection)
.await?;
let value = serde_json::from_str(&document)?;
- decode_canonical_document(value).map_err(|_| ConfigError::UnsupportedDatabase)
+ decode_canonical_snapshot(value).map_err(|_| ConfigError::UnsupportedDatabase)
}
diff --git a/crates/config/src/skills.rs b/crates/config/src/skills.rs
deleted file mode 100644
index 0fef735424..0000000000
--- a/crates/config/src/skills.rs
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::{ConfigError, ConfigurationStore, Result, TransactionMode};
-use maka_runtime::{configuration::validation::MAX_SAFE_INTEGER, skills::SkillPreference};
-use sqlx::SqliteConnection;
-use std::collections::BTreeMap;
-
-const MAX_PREFERENCES: usize = 16_384;
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct SkillPreferences {
- pub revision: u64,
- pub entries: BTreeMap<String, SkillPreference>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum PreferenceUpdate {
- Committed { revision: u64 },
- Conflict { actual_revision: u64 },
-}
-
-impl ConfigurationStore {
- pub async fn skill_preferences(&self) -> Result<SkillPreferences> {
- self.transaction(TransactionMode::Deferred, |connection| {
- Box::pin(read(connection))
- })
- .await
- }
-
- /// The catalog's later filesystem revision check must include this control
- /// revision; a stale preference writer cannot overwrite a newer decision.
- pub async fn set_skill_preference(
- &self,
- expected_revision: u64,
- reference: String,
- preference: SkillPreference,
- ) -> Result<PreferenceUpdate> {
- if expected_revision > MAX_SAFE_INTEGER || !valid_reference(&reference) {
- return Err(ConfigError::Invalid(
- "invalid skill preference target or revision".into(),
- ));
- }
- self.transaction(TransactionMode::Immediate, move |connection| Box::pin(async move {
- let current = read(connection).await?;
- if current.revision != expected_revision {
- return Ok(PreferenceUpdate::Conflict { actual_revision: current.revision });
- }
- if current.entries.get(&reference).copied().unwrap_or_default() == preference {
- return Ok(PreferenceUpdate::Committed { revision: current.revision });
- }
- if current.revision == MAX_SAFE_INTEGER
- || (current.entries.len() == MAX_PREFERENCES && !current.entries.contains_key(&reference))
- {
- return Err(ConfigError::Invalid("skill preference capacity exhausted".into()));
- }
- if preference == SkillPreference::default() {
- sqlx::query("DELETE FROM skill_preferences WHERE ref = ?")
- .bind(reference).execute(&mut *connection).await?;
- } else {
- sqlx::query("INSERT INTO skill_preferences(ref, enabled, pinned) VALUES (?, ?, ?) ON CONFLICT(ref) DO UPDATE SET enabled = excluded.enabled, pinned = excluded.pinned")
- .bind(reference).bind(preference.enabled).bind(preference.pinned)
- .execute(&mut *connection).await?;
- }
- let revision = current.revision + 1;
- sqlx::query("UPDATE skill_preferences_revision SET revision = ? WHERE singleton = 1")
- .bind(revision as i64).execute(connection).await?;
- Ok(PreferenceUpdate::Committed { revision })
- })).await
- }
-}
-
-async fn read(connection: &mut SqliteConnection) -> Result<SkillPreferences> {
- let revisions: Vec<(i64, i64)> =
- sqlx::query_as("SELECT singleton, revision FROM skill_preferences_revision LIMIT 2")
- .fetch_all(&mut *connection)
- .await?;
- let [(1, revision)] = revisions.as_slice() else {
- return Err(ConfigError::UnsupportedDatabase);
- };
- let revision = u64::try_from(*revision)
- .ok()
- .filter(|r| *r <= MAX_SAFE_INTEGER)
- .ok_or(ConfigError::UnsupportedDatabase)?;
- let invalid: i64 = sqlx::query_scalar(
- "SELECT COUNT(*) FROM skill_preferences WHERE length(CAST(ref AS BLOB)) NOT BETWEEN 1 AND 512 OR enabled NOT IN (0, 1) OR pinned NOT IN (0, 1)"
- ).fetch_one(&mut *connection).await?;
- if invalid != 0 {
- return Err(ConfigError::UnsupportedDatabase);
- }
- let rows: Vec<(String, bool, bool)> =
- sqlx::query_as("SELECT ref, enabled, pinned FROM skill_preferences ORDER BY ref LIMIT ?")
- .bind((MAX_PREFERENCES + 1) as i64)
- .fetch_all(connection)
- .await?;
- if rows.len() > MAX_PREFERENCES
- || rows
- .iter()
- .any(|(reference, _, _)| !valid_reference(reference))
- {
- return Err(ConfigError::UnsupportedDatabase);
- }
- Ok(SkillPreferences {
- revision,
- entries: rows
- .into_iter()
- .map(|(reference, enabled, pinned)| (reference, SkillPreference { enabled, pinned }))
- .collect(),
- })
-}
-
-fn valid_reference(reference: &str) -> bool {
- !reference.is_empty()
- && reference.len() <= 512
- && !reference
- .chars()
- .any(|c| matches!(c, '\u{0000}'..='\u{001f}' | '\u{007f}'))
-}
diff --git a/crates/config/tests/migrations.rs b/crates/config/tests/migrations.rs
index db0dbd20a1..06f62ea2d8 100644
--- a/crates/config/tests/migrations.rs
+++ b/crates/config/tests/migrations.rs
@@ -19,9 +19,7 @@
use maka_config::ConfigurationStore;
use maka_event_log::root::{RootNamespaces, RootOwner};
-use maka_runtime::configuration::*;
-use rusqlite::{Connection, params};
-use serde_json::json;
+use rusqlite::Connection;
use std::{path::Path, sync::Arc};
fn private_database(root: &Path) -> Connection {
@@ -55,220 +53,6 @@ fn root(temp: &Path) -> Arc<RootOwner> {
)
}
-#[tokio::test]
-async fn legacy_catalog_and_vault_migrate_without_losing_revisions_or_secrets() {
- let temp = tempfile::tempdir().unwrap();
- let owner = root(temp.path());
- let database = private_database(owner.canonical_path());
- // This is the previous Rust schema, deliberately independent of the new migration.
- database
- .execute_batch(
- "
- PRAGMA application_id = 1296124739;
- PRAGMA user_version = 1;
- CREATE TABLE connection_catalog (
- singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
- revision INTEGER NOT NULL CHECK(revision >= 0), default_target TEXT);
- CREATE TABLE connections (
- connection_id TEXT PRIMARY KEY, slug TEXT NOT NULL UNIQUE,
- revision INTEGER NOT NULL CHECK(revision > 0), document TEXT NOT NULL);
- CREATE TABLE credential_vault (
- singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
- revision INTEGER NOT NULL CHECK(revision >= 0));
- CREATE TABLE credentials (
- locator TEXT PRIMARY KEY, credential_id TEXT NOT NULL UNIQUE,
- revision INTEGER NOT NULL CHECK(revision > 0),
- secret TEXT NOT NULL, updated_at INTEGER NOT NULL);
- INSERT INTO credential_vault VALUES(1, 9);
- ",
- )
- .unwrap();
- let connection_id = "11111111-1111-4111-8111-111111111111".to_owned();
- let target = ConnectionTarget {
- connection_id: connection_id.clone(),
- model_id: "fixture".into(),
- };
- let mut entry: ConnectionCatalogEntry = serde_json::from_value(json!({
- "connectionId":connection_id, "revision":3, "slug":"legacy", "name":"Legacy",
- "providerType":"openai-compatible", "baseUrl":"http://127.0.0.1:18080/v1",
- "enabled":true, "enabledModelIds":["fixture"], "models":[]
- }))
- .unwrap();
- let mut legacy_document = serde_json::to_value(&entry).unwrap();
- legacy_document["relayModelProfiles"] = json!({
- "fixture":{"contextWindow":8192,"vision":true}
- });
- legacy_document["lastTest"] = json!({"status":"verified","checkedAt":"legacy"});
- legacy_document["lastTestModelFactsFingerprint"] = json!("sha256:old-file-basis");
- let external = owner.canonical_path().join("model-facts.json");
- std::fs::write(&external, b"not a Rust configuration source").unwrap();
- entry.model_overrides = Some(
- serde_json::from_value(json!({
- "fixture":{"contextWindow":8192,"compactionThreshold":8192,"vision":true}
- }))
- .unwrap(),
- );
- database
- .execute(
- "INSERT INTO connection_catalog VALUES(1, 7, ?)",
- [serde_json::to_string(&target).unwrap()],
- )
- .unwrap();
- database
- .execute(
- "INSERT INTO connections VALUES(?, 'legacy', 3, ?)",
- params![
- connection_id,
- serde_json::to_string(&legacy_document).unwrap()
- ],
- )
- .unwrap();
- let key = CredentialLocator::Connection {
- connection_id: connection_id.clone(),
- kind: ConnectionCredentialKind::ApiKey,
- };
- let unrelated = CredentialLocator::NetworkProxy {
- kind: PasswordKind::Password,
- };
- for (locator, id, secret) in [
- (
- &key,
- "22222222-2222-4222-8222-222222222222",
- "legacy-private-key",
- ),
- (
- &unrelated,
- "33333333-3333-4333-8333-333333333333",
- "legacy-proxy-password",
- ),
- ] {
- database
- .execute(
- "INSERT INTO credentials VALUES(?, ?, 4, ?, 42)",
- params![serde_json::to_string(locator).unwrap(), id, secret],
- )
- .unwrap();
- }
- drop(database);
-
- let store = ConfigurationStore::for_root(owner.clone()).await.unwrap();
- let snapshot = store.catalog().await.unwrap();
- assert_eq!(
- std::fs::read(&external).unwrap(),
- b"not a Rust configuration source"
- );
- assert_eq!(snapshot.revision, 7);
- assert_eq!(snapshot.connections, vec![entry]);
- assert_eq!(snapshot.default_target, Some(target));
- assert!(
- !serde_json::to_string(&snapshot)
- .unwrap()
- .contains("legacy-private-key")
- );
- assert_eq!(
- store
- .credential_secret(&key, None)
- .await
- .unwrap()
- .as_deref(),
- Some("legacy-private-key")
- );
- let status = store.credential_status(key.clone()).await.unwrap();
- assert!(matches!(
- &status,
- CredentialVaultQueryResult::Status {
- status: CredentialStatus {
- state: CredentialState::Configured {
- revision: 4,
- updated_at: 42,
- ..
- },
- ..
- }
- }
- ));
- let basis = ConnectionVersionBasis {
- connection_id,
- revision: 3,
- };
- let mut stale = basis.clone();
- stale.revision = 2;
- assert_eq!(
- store
- .remove_connection(RemoveCatalogConnectionInput {
- expected: stale.clone()
- })
- .await
- .unwrap(),
- CatalogMutationResult::ConnectionStale {
- expected: stale,
- actual: Some(basis.clone())
- }
- );
- assert!(matches!(
- store
- .delete_credential(DeleteCredentialInput {
- expected: CredentialVersionBasis {
- locator: key.clone(),
- credential_id: "22222222-2222-4222-8222-222222222222".into(),
- revision: 3
- }
- })
- .await
- .unwrap(),
- CredentialMutationResult::CredentialStale { .. }
- ));
- assert_eq!(store.catalog().await.unwrap(), snapshot);
- assert_eq!(store.credential_status(key.clone()).await.unwrap(), status);
- store.close().await.unwrap();
-
- let store = ConfigurationStore::for_root(owner.clone()).await.unwrap();
- assert_eq!(store.catalog().await.unwrap(), snapshot);
- assert_eq!(store.credential_status(key.clone()).await.unwrap(), status);
- assert_eq!(
- store
- .remove_connection(RemoveCatalogConnectionInput { expected: basis })
- .await
- .unwrap(),
- CatalogMutationResult::Committed {
- catalog_revision: 8,
- connection: None
- }
- );
- assert_eq!(store.catalog().await.unwrap().default_target, None);
- assert_eq!(
- store
- .credential_secret(&unrelated, None)
- .await
- .unwrap()
- .as_deref(),
- Some("legacy-proxy-password")
- );
- store.close().await.unwrap();
- let database =
- Connection::open(owner.canonical_path().join("configuration-rust.sqlite")).unwrap();
- assert_eq!(
- database
- .query_row(
- "SELECT revision, (SELECT COUNT(*) FROM credentials) FROM credential_vault",
- [],
- |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
- )
- .unwrap(),
- (10, 1)
- );
- assert_eq!(
- database
- .query_row(
- "SELECT COUNT(*) FROM _sqlx_migrations WHERE success = 1",
- [],
- |row| row.get::<_, i64>(0)
- )
- .unwrap(),
- 10
- );
-}
-
#[tokio::test]
async fn invalid_databases_are_rejected_without_mutation_and_interrupted_initialization_resumes() {
for corruption in [
@@ -276,6 +60,9 @@ async fn invalid_databases_are_rejected_without_mutation_and_interrupted_initial
"UPDATE _sqlx_migrations SET version = version + 999",
"PRAGMA application_id = 123",
"PRAGMA user_version = 999",
+ "DROP TABLE _sqlx_migrations",
+ "DELETE FROM _sqlx_migrations",
+ "PRAGMA user_version = 0",
] {
let temp = tempfile::tempdir().unwrap();
let owner = root(temp.path());
@@ -284,6 +71,9 @@ async fn invalid_databases_are_rejected_without_mutation_and_interrupted_initial
let path = owner.canonical_path().join("configuration-rust.sqlite");
let database = Connection::open(&path).unwrap();
database.execute_batch(corruption).unwrap();
+ database
+ .execute_batch("PRAGMA journal_mode = DELETE")
+ .unwrap();
drop(database);
let before = std::fs::read(&path).unwrap();
assert!(
diff --git a/crates/config/tests/plugin_authorization.rs b/crates/config/tests/plugin_authorization.rs
new file mode 100644
index 0000000000..aa21806ee7
--- /dev/null
+++ b/crates/config/tests/plugin_authorization.rs
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use maka_config::{
+ ConfigurationStore,
+ plugin_authorization::{Approval, Boundary, Principal},
+};
+use maka_event_log::root::{RootNamespaces, RootOwner};
+use maka_plugins::{
+ authorization::{Capability, Request, Target},
+ composition::Scope,
+ execution::SessionBoundary,
+ storage::Namespace,
+};
+use maka_runtime::execution::{PermissionMode, WorkspaceIdentity};
+use std::sync::Arc;
+use uuid::Uuid;
+
+#[tokio::test]
+async fn consent_retries_preserve_original_boundary_and_revocation_across_restart() {
+ let temp = tempfile::tempdir().unwrap();
+ let owner = Arc::new(
+ RootOwner::create(
+ &temp.path().join("root"),
+ &RootNamespaces {
+ ownership: temp.path().join("owners"),
+ control: temp.path().join("control"),
+ },
+ )
+ .unwrap(),
+ );
+ let store = ConfigurationStore::for_root(owner.clone()).await.unwrap();
+ let namespace = Namespace::new("example.background", Scope::Profile).unwrap();
+ let request = Request {
+ operation_id: Uuid::new_v4(),
+ title: "Review changes".into(),
+ target: Target::Session {
+ session_id: "session".into(),
+ },
+ capabilities: [Capability::Executions].into(),
+ };
+ let boundary = |revision, permission_mode| Boundary::Session {
+ boundary: SessionBoundary {
+ session_id: "session".into(),
+ boundary_revision: revision,
+ permission_mode,
+ cwd: temp.path().to_str().unwrap().into(),
+ },
+ workspace_identity: WorkspaceIdentity::from_marker_id(
+ "00000000-0000-4000-8000-000000000001",
+ )
+ .unwrap(),
+ };
+ let approve = |principal, request, boundary| {
+ store.approve_plugin_authorization(namespace.clone(), principal, request, boundary)
+ };
+ let first = boundary(1, PermissionMode::Ask);
+ let (left, right) = tokio::join!(
+ approve(
+ Principal::LocalUser {
+ client_instance_id: "client".into()
+ },
+ request.clone(),
+ first.clone()
+ ),
+ approve(
+ Principal::LocalUser {
+ client_instance_id: "client".into()
+ },
+ request.clone(),
+ first.clone()
+ ),
+ );
+ let (Approval::Granted(left), Approval::Granted(right)) = (left.unwrap(), right.unwrap())
+ else {
+ panic!("identical approval conflicted")
+ };
+ assert_eq!(left.grant, right.grant);
+ let id = left.grant.id;
+ // Lost reply retries cannot recapture a newer/wider Session configuration.
+ let Approval::Granted(retry) = approve(
+ Principal::LocalUser {
+ client_instance_id: "client".into(),
+ },
+ request.clone(),
+ boundary(2, PermissionMode::Bypass),
+ )
+ .await
+ .unwrap() else {
+ panic!("retry conflicted")
+ };
+ assert_eq!(retry.boundary, first);
+ let mut changed = request.clone();
+ changed.capabilities.insert(Capability::Notifications);
+ assert!(matches!(
+ approve(
+ Principal::LocalUser {
+ client_instance_id: "client".into()
+ },
+ changed,
+ first.clone()
+ )
+ .await
+ .unwrap(),
+ Approval::Conflict
+ ));
+ assert!(matches!(
+ approve(
+ Principal::Credential {
+ credential_id: "another-user".into(),
+ client_instance_id: "desktop".into()
+ },
+ request.clone(),
+ first.clone()
+ )
+ .await
+ .unwrap(),
+ Approval::Conflict
+ ));
+ for other in [
+ Namespace::new("other", Scope::Profile).unwrap(),
+ Namespace::new("example.background", Scope::Session("session".into())).unwrap(),
+ ] {
+ assert!(
+ store
+ .plugin_authorization(other.clone(), id)
+ .await
+ .unwrap()
+ .is_none()
+ );
+ store.revoke_plugin_authorization(other, id).await.unwrap();
+ }
+ assert!(
+ !store
+ .plugin_authorization(namespace.clone(), id)
+ .await
+ .unwrap()
+ .unwrap()
+ .grant
+ .revoked
+ );
+ store
+ .revoke_plugin_authorization(namespace.clone(), id)
+ .await
+ .unwrap();
+ store.close().await.unwrap();
+ let store = ConfigurationStore::for_root(owner).await.unwrap();
+ let Approval::Granted(retry) = store
+ .approve_plugin_authorization(
+ namespace.clone(),
+ Principal::LocalUser {
+ client_instance_id: "client".into(),
+ },
+ request,
+ first.clone(),
+ )
+ .await
+ .unwrap()
+ else {
+ panic!("revoked retry conflicted")
+ };
+ assert_eq!(retry.grant.id, id);
+ assert!(retry.grant.revoked);
+ assert_eq!(retry.boundary, first);
+ assert!(
+ store
+ .plugin_authorization(namespace, id)
+ .await
+ .unwrap()
+ .unwrap()
+ .grant
+ .revoked
+ );
+ store.close().await.unwrap();
+}
diff --git a/crates/config/tests/runtime_policy.rs b/crates/config/tests/runtime_policy.rs
index 4b5424fd03..50815fb22a 100644
--- a/crates/config/tests/runtime_policy.rs
+++ b/crates/config/tests/runtime_policy.rs
@@ -233,67 +233,11 @@ async fn defaults_are_read_only_and_cas_is_durable_even_for_same_value() {
RuntimePolicyMutationResult::Committed { revision: 3 }
);
store.close().await.unwrap();
- // Reconstruct the previous Rust policy document and migration ledger. The
- // upgrade must preserve user values/revisions without weakening wire decode.
- db.execute_batch(
- "UPDATE runtime_policy SET document = json_remove(document, '$.policy.externalAgents');
- DELETE FROM _sqlx_migrations WHERE version = 6; PRAGMA user_version = 5;",
- )
- .unwrap();
- let document: String = db
- .query_row("SELECT document FROM runtime_policy", [], |row| row.get(0))
- .unwrap();
- let mut legacy: serde_json::Value = serde_json::from_str(&document).unwrap();
- // A valid legacy document at the old byte limit must survive schema growth.
- while legacy.to_string().len() + 515 < MAX_POLICY_SNAPSHOT_BYTES {
- let domains = legacy["policy"]["networkProxy"]["bypassList"]
- .as_array_mut()
- .unwrap();
- domains.push(serde_json::json!(format!(
- "{:03}{}",
- domains.len(),
- "x".repeat(509)
- )));
- }
- let remaining = MAX_POLICY_SNAPSHOT_BYTES - legacy.to_string().len();
- if remaining > 3 {
- legacy["policy"]["networkProxy"]["bypassList"]
- .as_array_mut()
- .unwrap()
- .push(serde_json::json!("y".repeat(remaining - 3)));
- }
- let document = legacy.to_string();
- assert!(document.len() <= MAX_POLICY_SNAPSHOT_BYTES);
- assert!(document.len() > MAX_POLICY_SNAPSHOT_BYTES - 40);
- db.execute("UPDATE runtime_policy SET document = ?", [&document])
- .unwrap();
- let reopened = ConfigurationStore::for_root(owner.clone()).await.unwrap();
+ let reopened = ConfigurationStore::for_root(owner).await.unwrap();
let snapshot = reopened.runtime_policy().await.unwrap();
assert_eq!(snapshot.revision, 3);
assert_eq!(snapshot.policy.chat_defaults, clear);
- assert_eq!(snapshot.policy.external_agents, ExternalAgents::default());
- assert_eq!(
- serde_json::to_value(&snapshot.policy.network_proxy).unwrap(),
- legacy["policy"]["networkProxy"]
- );
- assert!(snapshot.validate().is_err()); // Still too large for the wire.
- assert_eq!(
- reopened.network_configuration().await.unwrap().proxy,
- snapshot.policy.network_proxy
- );
- assert!(matches!(
- reopened.set_chat_defaults(3, clear).await,
- Err(ConfigError::Invalid(_))
- ));
- assert_eq!(reopened.runtime_policy().await.unwrap(), snapshot);
- assert!(matches!(
- reopened
- .set_network_proxy(3, initial.policy.network_proxy.clone())
- .await
- .unwrap(),
- RuntimePolicyMutationResult::Committed { revision: 4 }
- ));
- reopened.runtime_policy().await.unwrap().validate().unwrap();
+ snapshot.validate().unwrap();
assert_eq!(count(&db), 1);
reopened.close().await.unwrap();
}
diff --git a/crates/config/tests/skills.rs b/crates/config/tests/skills.rs
deleted file mode 100644
index 19b242d0c4..0000000000
--- a/crates/config/tests/skills.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use maka_config::{ConfigurationStore, skills::PreferenceUpdate};
-use maka_event_log::root::{RootNamespaces, RootOwner};
-use maka_runtime::skills::SkillPreference;
-use std::sync::Arc;
-
-#[tokio::test]
-async fn root_preferences_are_atomic_revisioned_and_corruption_is_not_default_enablement() {
- let temporary = tempfile::tempdir().unwrap();
- let owner = Arc::new(
- RootOwner::create(
- &temporary.path().join("root"),
- &RootNamespaces {
- ownership: temporary.path().join("owners"),
- control: temporary.path().join("control"),
- },
- )
- .unwrap(),
- );
- let legacy = owner.canonical_path().join(".maka");
- std::fs::create_dir(&legacy).unwrap();
- let legacy = legacy.join("skills-state.json");
- std::fs::write(&legacy, "original TS state is not an input").unwrap();
- let store = ConfigurationStore::for_root(owner.clone()).await.unwrap();
- let initial = store.skill_preferences().await.unwrap();
- assert_eq!(initial.revision, 0);
- assert!(initial.entries.is_empty());
- let reference = "project:maka:review".to_owned();
- let disabled = SkillPreference {
- enabled: false,
- pinned: true,
- };
- assert_eq!(
- store
- .set_skill_preference(0, reference.clone(), disabled)
- .await
- .unwrap(),
- PreferenceUpdate::Committed { revision: 1 }
- );
- assert_eq!(
- store
- .set_skill_preference(0, reference.clone(), SkillPreference::default())
- .await
- .unwrap(),
- PreferenceUpdate::Conflict { actual_revision: 1 }
- );
- assert_eq!(
- store
- .set_skill_preference(1, reference.clone(), disabled)
- .await
- .unwrap(),
- PreferenceUpdate::Committed { revision: 1 }
- );
- store.close().await.unwrap();
- let store = ConfigurationStore::for_root(owner.clone()).await.unwrap();
- assert_eq!(
- store.skill_preferences().await.unwrap().entries[&reference],
- disabled
- );
- assert_eq!(
- store
- .set_skill_preference(1, reference.clone(), SkillPreference::default())
- .await
- .unwrap(),
- PreferenceUpdate::Committed { revision: 2 }
- );
- let empty = store.skill_preferences().await.unwrap();
- assert_eq!(empty.revision, 2);
- assert!(empty.entries.is_empty());
-
- let db = rusqlite::Connection::open(owner.canonical_path().join("configuration-rust.sqlite"))
- .unwrap();
- db.execute_batch(
- "PRAGMA ignore_check_constraints = ON;
- INSERT INTO skill_preferences VALUES ('project:maka:review', 7, 0);",
- )
- .unwrap();
- assert!(store.skill_preferences().await.is_err());
- assert!(
- store
- .set_skill_preference(2, reference, disabled)
- .await
- .is_err()
- );
- assert_eq!(
- db.query_row(
- "SELECT revision FROM skill_preferences_revision",
- [],
- |row| row.get::<_, i64>(0)
- )
- .unwrap(),
- 2
- );
- assert_eq!(
- std::fs::read_to_string(legacy).unwrap(),
- "original TS state is not an input"
- );
- store.close().await.unwrap();
-}
diff --git a/crates/event-log/migrations/0001_runtime.sql b/crates/event-log/migrations/0001_runtime.sql
index 6301f9d642..68e461f82d 100644
--- a/crates/event-log/migrations/0001_runtime.sql
+++ b/crates/event-log/migrations/0001_runtime.sql
@@ -17,34 +17,19 @@
* under the License.
*/
+-- Canonical facts and durable control state. Disposable projections are built at startup.
--- Canonical execution facts and independent Session control state.
--- Disposable transcript/catalog projections are rebuilt from these facts.
-PRAGMA application_id = 1296124754;
-PRAGMA user_version = 1;
-
-CREATE TABLE IF NOT EXISTS runtime_events (
+CREATE TABLE event_log (
sequence INTEGER PRIMARY KEY AUTOINCREMENT CHECK(sequence > 0),
event_id TEXT NOT NULL UNIQUE,
- invocation_id TEXT NOT NULL,
+ invocation_id TEXT,
kind TEXT NOT NULL,
operation_id TEXT,
- event_json TEXT NOT NULL
-);
-CREATE INDEX IF NOT EXISTS invocation_sequence ON runtime_events(invocation_id, sequence);
-CREATE INDEX IF NOT EXISTS session_event_sequence ON runtime_events(
- json_extract(event_json, '$.invocation.session_id'), sequence
+ event_json TEXT NOT NULL,
+ CHECK(invocation_id IS NOT NULL OR operation_id IS NULL)
);
-CREATE INDEX IF NOT EXISTS turn_opening_lookup ON runtime_events(
- json_extract(event_json, '$.invocation.session_id'),
- json_extract(event_json, '$.invocation.turn_id'), sequence DESC
-) WHERE kind = 'invocation_opened';
-CREATE UNIQUE INDEX IF NOT EXISTS operation_fact ON runtime_events(operation_id, kind)
- WHERE operation_id IS NOT NULL;
-CREATE UNIQUE INDEX IF NOT EXISTS invocation_boundary ON runtime_events(invocation_id, kind)
- WHERE kind IN ('invocation_opened', 'invocation_ended');
-CREATE TABLE IF NOT EXISTS session_control (
+CREATE TABLE session_control (
id TEXT PRIMARY KEY,
fingerprint TEXT NOT NULL,
revision INTEGER NOT NULL CHECK(revision > 0),
@@ -53,8 +38,388 @@ CREATE TABLE IF NOT EXISTS session_control (
archived INTEGER NOT NULL CHECK(archived IN (0, 1)),
configuration TEXT NOT NULL
);
-CREATE TABLE IF NOT EXISTS session_catalog_revision (
+
+CREATE TABLE session_catalog_revision (
singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
revision INTEGER NOT NULL CHECK(revision >= 0)
);
-INSERT OR IGNORE INTO session_catalog_revision VALUES (1, 0);
+
+CREATE TABLE session_read_state (
+ session_id TEXT PRIMARY KEY REFERENCES session_control(id),
+ has_unread INTEGER NOT NULL CHECK(has_unread IN (0, 1)),
+ last_read_message_id TEXT
+);
+
+CREATE TABLE interaction_requests (
+ request_id TEXT PRIMARY KEY,
+ session_id TEXT NOT NULL,
+ created_at INTEGER NOT NULL,
+ record_json TEXT NOT NULL CHECK(length(CAST(record_json AS BLOB)) <= 20480)
+);
+
+CREATE INDEX interaction_session_pending ON interaction_requests(session_id, created_at, request_id);
+
+CREATE TABLE interaction_outcomes (
+ request_id TEXT PRIMARY KEY REFERENCES interaction_requests(request_id),
+ outcome_json TEXT NOT NULL CHECK(length(CAST(outcome_json AS BLOB)) <= 8192)
+);
+
+CREATE TABLE client_capability_session_grants (
+ authority_key TEXT PRIMARY KEY,
+ record_json TEXT NOT NULL CHECK(length(CAST(record_json AS BLOB)) <= 12288)
+);
+
+CREATE TABLE artifacts (
+ session_id TEXT NOT NULL REFERENCES session_control(id),
+ id TEXT NOT NULL,
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
+ record_json TEXT NOT NULL CHECK(length(CAST(record_json AS BLOB)) <= 16384),
+ content_sha256 TEXT NOT NULL,
+ payload BLOB NOT NULL CHECK(length(payload) <= 52428800),
+ PRIMARY KEY (session_id, id),
+ CHECK(json_extract(record_json, '$.sessionId') = session_id),
+ CHECK(json_extract(record_json, '$.id') = id),
+ CHECK(json_extract(record_json, '$.createdAt') = created_at),
+ CHECK(json_extract(record_json, '$.sizeBytes') = length(payload))
+);
+
+CREATE INDEX artifacts_session_order ON artifacts(session_id, created_at DESC, id);
+
+CREATE TABLE artifact_catalog (
+ session_id TEXT PRIMARY KEY REFERENCES session_control(id),
+ revision INTEGER NOT NULL CHECK(revision > 0 AND revision <= 9007199254740991)
+);
+
+CREATE TABLE shell_runs (
+ session_id TEXT NOT NULL REFERENCES session_control(id),
+ id TEXT NOT NULL,
+ started_at INTEGER NOT NULL CHECK(started_at >= 0),
+ active INTEGER NOT NULL CHECK(active IN (0, 1)),
+ record_json TEXT NOT NULL CHECK(length(CAST(record_json AS BLOB)) <= 4194304),
+ PRIMARY KEY (session_id, id),
+ CHECK(json_extract(record_json, '$.sessionId') IS session_id),
+ CHECK(json_extract(record_json, '$.id') IS id),
+ CHECK(json_extract(record_json, '$.startedAt') IS started_at),
+ CHECK(active IS (json_extract(record_json, '$.state.kind') IN ('starting', 'running')))
+);
+
+CREATE INDEX shell_runs_session_order ON shell_runs(session_id, started_at, id);
+
+CREATE INDEX shell_runs_active ON shell_runs(active) WHERE active = 1;
+
+CREATE TABLE message_admissions (
+ session_id TEXT NOT NULL,
+ message_id TEXT NOT NULL,
+ position INTEGER NOT NULL,
+ record_json TEXT NOT NULL,
+ PRIMARY KEY(session_id, message_id)
+);
+
+CREATE INDEX message_admission_order ON message_admissions(session_id, position, message_id);
+
+CREATE TABLE message_queue_state (
+ session_id TEXT PRIMARY KEY NOT NULL,
+ revision INTEGER NOT NULL CHECK(revision >= 0)
+);
+
+CREATE TABLE message_cancellations (
+ session_id TEXT NOT NULL,
+ message_id TEXT NOT NULL,
+ cancellation_id TEXT NOT NULL,
+ PRIMARY KEY(session_id, message_id)
+);
+
+CREATE TABLE queue_command_receipts (
+ host_epoch TEXT NOT NULL,
+ session_id TEXT NOT NULL,
+ operation TEXT NOT NULL,
+ command_id TEXT NOT NULL,
+ fingerprint TEXT NOT NULL,
+ receipt_json TEXT NOT NULL,
+ PRIMARY KEY(host_epoch, session_id, operation, command_id)
+);
+
+CREATE TABLE message_submit_receipts (
+ host_epoch TEXT NOT NULL,
+ session_id TEXT NOT NULL,
+ message_id TEXT NOT NULL,
+ receipt_json TEXT NOT NULL,
+ PRIMARY KEY(host_epoch, session_id, message_id)
+);
+
+CREATE TABLE message_interrupt_receipts (
+ host_epoch TEXT NOT NULL,
+ session_id TEXT NOT NULL,
+ interrupt_id TEXT NOT NULL,
+ turn_id TEXT NOT NULL,
+ run_id TEXT NOT NULL,
+ fence_json TEXT,
+ PRIMARY KEY(host_epoch, session_id, interrupt_id)
+);
+
+CREATE INDEX message_interrupt_run ON message_interrupt_receipts(session_id, run_id)
+ WHERE fence_json IS NOT NULL;
+
+CREATE TABLE projects (
+ id TEXT PRIMARY KEY NOT NULL,
+ identity TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL CHECK(length(CAST(name AS BLOB)) BETWEEN 1 AND 16384),
+ last_used_at INTEGER NOT NULL CHECK(last_used_at BETWEEN 0 AND 9007199254740991),
+ archived_at INTEGER CHECK(archived_at BETWEEN 0 AND 9007199254740991)
+) STRICT;
+
+CREATE TABLE project_identities (
+ id TEXT PRIMARY KEY NOT NULL,
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE
+) STRICT;
+
+CREATE INDEX project_identity_owner ON project_identities(project_id, id);
+
+CREATE TABLE project_locations (
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
+ path TEXT NOT NULL CHECK(length(CAST(path AS BLOB)) BETWEEN 1 AND 4096),
+ is_worktree INTEGER NOT NULL CHECK(is_worktree IN (0, 1)),
+ last_used_at INTEGER NOT NULL CHECK(last_used_at BETWEEN 0 AND 9007199254740991),
+ PRIMARY KEY(project_id, path)
+) STRICT;
+
+CREATE UNIQUE INDEX pending_root_invocation ON message_admissions (
+ json_extract(record_json, '$.invocation.invocation_id')
+) WHERE json_extract(record_json, '$.source.disposition') = 'turn_started';
+
+CREATE TABLE "tool_result_payloads" (
+ event_id TEXT PRIMARY KEY NOT NULL REFERENCES event_log(event_id),
+ payload BLOB NOT NULL CHECK(length(payload) <= 67108864)
+);
+
+CREATE VIEW runtime_events AS
+ SELECT * FROM event_log WHERE invocation_id IS NOT NULL;
+
+CREATE VIEW session_events AS
+ SELECT sequence, event_id, kind, event_json FROM event_log WHERE invocation_id IS NULL;
+
+CREATE INDEX invocation_sequence ON event_log(invocation_id, sequence);
+
+CREATE INDEX session_event_sequence ON event_log(
+ json_extract(event_json, '$.invocation.session_id'), sequence
+);
+
+CREATE INDEX session_control_sequence ON event_log(
+ json_extract(event_json, '$.session_id'), sequence
+) WHERE invocation_id IS NULL;
+
+CREATE INDEX turn_opening_lookup ON event_log(
+ json_extract(event_json, '$.invocation.session_id'),
+ json_extract(event_json, '$.invocation.turn_id'), sequence DESC
+) WHERE kind = 'invocation_opened';
+
+CREATE UNIQUE INDEX operation_fact ON event_log(operation_id, kind)
+ WHERE operation_id IS NOT NULL;
+
+CREATE UNIQUE INDEX invocation_boundary ON event_log(invocation_id, kind)
+ WHERE kind IN ('invocation_opened', 'invocation_ended');
+
+CREATE UNIQUE INDEX message_steering_identity ON event_log(
+ json_extract(event_json, '$.invocation.session_id'),
+ json_extract(event_json, '$.fact.message.message_id')
+) WHERE kind = 'message_steered';
+
+CREATE UNIQUE INDEX workhub_stop_resolution ON event_log(
+ json_extract(event_json, '$.fact.action_id')
+) WHERE kind = 'workhub_stop_resolved';
+
+CREATE INDEX workhub_stop_request_subject ON event_log(
+ json_extract(event_json, '$.fact.intent.delegation_action_id')
+) WHERE kind = 'workhub_stop_requested';
+
+CREATE VIEW workhub_stops AS
+ SELECT json_extract(q.event_json, '$.fact.intent.request.action_id') AS action_id,
+ json_extract(q.event_json, '$.fact.intent.delegation_action_id') AS delegation_action_id,
+ json_extract(q.event_json, '$.fact.intent') AS record_json,
+ json_extract(r.event_json, '$.fact.resolution') AS resolution_json
+ FROM session_events q LEFT JOIN session_events r
+ ON r.kind = 'workhub_stop_resolved'
+ AND json_extract(r.event_json, '$.fact.action_id') =
+ json_extract(q.event_json, '$.fact.intent.request.action_id')
+ WHERE q.kind = 'workhub_stop_requested';
+
+CREATE INDEX workhub_assignment_target ON event_log(
+ json_extract(event_json, '$.fact.delegation.target.session_id'), sequence DESC
+) WHERE kind = 'workhub_delegated';
+
+CREATE UNIQUE INDEX workhub_action_identity ON event_log(
+ CASE
+ WHEN kind IN ('workhub_stop_requested', 'workhub_correction_requested')
+ THEN json_extract(event_json, '$.fact.intent.request.action_id')
+ WHEN kind = 'workhub_delegated' AND invocation_id IS NOT NULL
+ THEN json_extract(event_json, '$.fact.delegation.action_id')
+ WHEN kind = 'workhub_resume_observed'
+ THEN json_extract(event_json, '$.fact.resume.action_id')
+ WHEN kind = 'invocation_opened'
+ AND json_extract(event_json, '$.fact.input.kind') = 'continuation'
+ THEN json_extract(event_json, '$.fact.input.workhub_resume.action_id')
+ END
+) WHERE kind IN ('workhub_delegated', 'workhub_resume_observed', 'invocation_opened', 'workhub_stop_requested', 'workhub_correction_requested');
+
+CREATE UNIQUE INDEX workhub_assignment_action ON event_log(
+ json_extract(event_json, '$.fact.delegation.action_id')
+) WHERE kind = 'workhub_delegated';
+
+CREATE UNIQUE INDEX workhub_correction_subject ON event_log(
+ json_extract(event_json, '$.fact.intent.request.replaces_action_id')
+) WHERE kind = 'workhub_correction_requested';
+
+CREATE UNIQUE INDEX workhub_correction_resolution ON event_log(
+ json_extract(event_json, '$.fact.action_id')
+) WHERE kind IN ('workhub_superseded', 'workhub_correction_aborted');
+
+CREATE VIEW workhub_assignments AS
+ SELECT sequence, event_id, event_json,
+ COALESCE(json_extract(event_json, '$.invocation'),
+ json_extract(event_json, '$.fact.coordinator')) AS coordinator_json
+ FROM event_log WHERE kind = 'workhub_delegated';
+
+CREATE VIEW workhub_corrections AS
+ SELECT q.sequence,
+ json_extract(q.event_json, '$.fact.intent.request.action_id') AS action_id,
+ json_extract(q.event_json, '$.fact.intent.request.replaces_action_id') AS replaces_action_id,
+ json_extract(q.event_json, '$.fact.intent') AS intent_json,
+ r.kind AS resolution_kind,
+ json_extract(r.event_json, '$.fact.reason') AS abort_reason
+ FROM session_events q LEFT JOIN session_events r
+ ON r.kind IN ('workhub_superseded', 'workhub_correction_aborted')
+ AND json_extract(r.event_json, '$.fact.action_id') =
+ json_extract(q.event_json, '$.fact.intent.request.action_id')
+ WHERE q.kind = 'workhub_correction_requested';
+
+CREATE UNIQUE INDEX continuation_claim_id ON event_log(
+ json_extract(event_json, '$.fact.input.claim.id')
+) WHERE kind = 'invocation_opened' AND json_extract(event_json, '$.fact.input.kind') IN ('continuation','handoff');
+
+CREATE UNIQUE INDEX continuation_source_boundary ON event_log(
+ json_extract(event_json, '$.invocation.session_id'),
+ json_extract(event_json, '$.fact.input.claim.source.invocation.run_id'),
+ json_extract(event_json, '$.fact.input.claim.source.high_water')
+) WHERE kind = 'invocation_opened' AND json_extract(event_json, '$.fact.input.kind') IN ('continuation','handoff');
+
+CREATE UNIQUE INDEX handoff_successor_run ON event_log(
+ json_extract(event_json, '$.fact.outcome.pause.intent.successor_run_id')
+) WHERE kind = 'invocation_ended' AND json_extract(event_json, '$.fact.outcome.kind') = 'handoff_paused';
+
+CREATE UNIQUE INDEX handoff_successor_invocation ON event_log(
+ json_extract(event_json, '$.fact.outcome.pause.intent.successor_invocation_id')
+) WHERE kind = 'invocation_ended' AND json_extract(event_json, '$.fact.outcome.kind') = 'handoff_paused';
+
+CREATE UNIQUE INDEX handoff_claim ON event_log(
+ json_extract(event_json, '$.fact.outcome.pause.intent.claim_id')
+) WHERE kind = 'invocation_ended' AND json_extract(event_json, '$.fact.outcome.kind') = 'handoff_paused';
+
+CREATE TABLE plugin_composition (
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
+ ledger_json TEXT NOT NULL CHECK (json_valid(ledger_json))
+);
+
+CREATE TABLE plugin_package_blobs (
+ digest TEXT PRIMARY KEY
+);
+
+CREATE TABLE plugin_package_files (
+ digest TEXT NOT NULL REFERENCES plugin_package_blobs(digest) ON DELETE CASCADE,
+ path TEXT NOT NULL,
+ payload BLOB NOT NULL,
+ PRIMARY KEY (digest, path)
+);
+
+CREATE TABLE plugin_packages (
+ id TEXT PRIMARY KEY,
+ digest TEXT NOT NULL REFERENCES plugin_package_blobs(digest)
+);
+
+CREATE TABLE plugin_data (
+ package_id TEXT NOT NULL,
+ scope_id TEXT NOT NULL,
+ key TEXT NOT NULL,
+ revision INTEGER NOT NULL CHECK (revision > 0),
+ value_json TEXT CHECK (value_json IS NULL OR json_valid(value_json)),
+ PRIMARY KEY (package_id, scope_id, key)
+);
+
+CREATE TABLE plugin_execution_receipts (
+ package_id TEXT NOT NULL,
+ scope_id TEXT NOT NULL,
+ operation_id TEXT NOT NULL,
+ receipt_json TEXT NOT NULL CHECK (json_valid(receipt_json)),
+ PRIMARY KEY (package_id, scope_id, operation_id)
+);
+
+CREATE TABLE graph_epochs (
+ root_session_id TEXT NOT NULL,
+ epoch INTEGER NOT NULL CHECK(epoch > 0),
+ graph_id TEXT NOT NULL UNIQUE,
+ mode TEXT NOT NULL CHECK(mode IN ('graph', 'swarm')),
+ created_at INTEGER NOT NULL CHECK(created_at >= 0),
+ stop_requested INTEGER NOT NULL DEFAULT 0 CHECK(stop_requested IN (0, 1)),
+ PRIMARY KEY(root_session_id, epoch)
+);
+
+CREATE TABLE graph_updates (
+ graph_id TEXT NOT NULL REFERENCES graph_epochs(graph_id),
+ update_id TEXT NOT NULL,
+ revision INTEGER NOT NULL CHECK(revision > 0),
+ fingerprint TEXT NOT NULL,
+ update_json TEXT NOT NULL CHECK(json_valid(update_json)),
+ byte_count INTEGER GENERATED ALWAYS AS (length(CAST(update_json AS BLOB))) STORED,
+ committed_at INTEGER NOT NULL CHECK(committed_at >= 0),
+ PRIMARY KEY(graph_id, revision),
+ UNIQUE(graph_id, update_id)
+);
+
+CREATE TABLE graph_intents (
+ graph_id TEXT NOT NULL REFERENCES graph_epochs(graph_id),
+ work_id TEXT NOT NULL,
+ intent_json TEXT NOT NULL CHECK(json_valid(intent_json)),
+ PRIMARY KEY(graph_id, work_id)
+);
+
+CREATE TABLE graph_wakes (
+ graph_id TEXT NOT NULL REFERENCES graph_epochs(graph_id),
+ snapshot_key TEXT NOT NULL,
+ request_json TEXT NOT NULL CHECK(json_valid(request_json)),
+ PRIMARY KEY(graph_id, snapshot_key)
+);
+
+CREATE TABLE request_compositions (
+ digest TEXT PRIMARY KEY NOT NULL,
+ surface BLOB NOT NULL CHECK(length(surface) <= 4194304)
+);
+
+CREATE TABLE model_request_compositions (
+ event_id TEXT PRIMARY KEY NOT NULL REFERENCES event_log(event_id),
+ digest TEXT NOT NULL REFERENCES request_compositions(digest)
+);
+
+CREATE INDEX model_request_composition_digest ON model_request_compositions(digest);
+
+CREATE TABLE session_managers (
+ session_id TEXT PRIMARY KEY NOT NULL,
+ package_id TEXT NOT NULL,
+ scope_id TEXT NOT NULL,
+ fingerprint TEXT NOT NULL
+);
+
+CREATE TABLE host_effects (
+ id TEXT PRIMARY KEY,
+ package_id TEXT NOT NULL,
+ scope_id TEXT NOT NULL,
+ request TEXT NOT NULL CHECK(json_valid(request) AND length(CAST(request AS BLOB)) <= 8388608),
+ outcome TEXT CHECK(outcome IS NULL OR (json_valid(outcome) AND length(CAST(outcome AS BLOB)) <= 8388608)),
+ payload BLOB CHECK(payload IS NULL OR length(payload) <= 5242880),
+ CHECK(outcome IS NOT NULL OR payload IS NULL)
+) STRICT;
+
+CREATE INDEX host_effects_namespace ON host_effects(package_id, scope_id);
+
+INSERT INTO session_catalog_revision VALUES (1, 0);
+INSERT INTO plugin_composition VALUES (1, '{"generation":0,"packageLayers":[],"overlays":[]}');
+
+PRAGMA user_version = 1;
diff --git a/crates/event-log/migrations/0002_session_read_state.sql b/crates/event-log/migrations/0002_session_read_state.sql
deleted file mode 100644
index be0cfa8e6f..0000000000
--- a/crates/event-log/migrations/0002_session_read_state.sql
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-PRAGMA user_version = 2;
-
--- Read acknowledgement is durable control state, never a rebuilt event cache.
-CREATE TABLE IF NOT EXISTS session_read_state (
- session_id TEXT PRIMARY KEY REFERENCES session_control(id),
- has_unread INTEGER NOT NULL CHECK(has_unread IN (0, 1)),
- last_read_message_id TEXT
-);
-
--- Existing Rust sessions predate acknowledgement support. Terminal history
--- initializes unread once; subsequent cache reconstruction cannot change it.
-INSERT OR IGNORE INTO session_read_state (session_id, has_unread, last_read_message_id)
-SELECT id, EXISTS(
- SELECT 1 FROM runtime_events
- WHERE json_extract(event_json, '$.invocation.session_id') = session_control.id
- AND kind = 'invocation_ended'
-), NULL FROM session_control;
diff --git a/crates/event-log/migrations/0003_interactions.sql b/crates/event-log/migrations/0003_interactions.sql
deleted file mode 100644
index 6434129521..0000000000
--- a/crates/event-log/migrations/0003_interactions.sql
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- Independent canonical interaction facts; grants never derive from UI history.
-PRAGMA user_version = 3;
-CREATE TABLE IF NOT EXISTS interaction_requests (
- request_id TEXT PRIMARY KEY,
- session_id TEXT NOT NULL,
- created_at INTEGER NOT NULL,
- record_json TEXT NOT NULL CHECK(length(CAST(record_json AS BLOB)) <= 20480)
-);
-CREATE INDEX IF NOT EXISTS interaction_session_pending ON interaction_requests(session_id, created_at, request_id);
-CREATE TABLE IF NOT EXISTS interaction_outcomes (
- request_id TEXT PRIMARY KEY REFERENCES interaction_requests(request_id),
- outcome_json TEXT NOT NULL CHECK(length(CAST(outcome_json AS BLOB)) <= 8192)
-);
-CREATE TABLE IF NOT EXISTS client_capability_session_grants (
- authority_key TEXT PRIMARY KEY,
- record_json TEXT NOT NULL CHECK(length(CAST(record_json AS BLOB)) <= 12288)
-);
diff --git a/crates/event-log/migrations/0004_artifacts.sql b/crates/event-log/migrations/0004_artifacts.sql
deleted file mode 100644
index 62642c537c..0000000000
--- a/crates/event-log/migrations/0004_artifacts.sql
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
--- Artifact control authority is independent of invocation history. Metadata and
--- immutable payload become visible together; query projections never scan BLOBs.
-PRAGMA user_version = 4;
-CREATE TABLE IF NOT EXISTS artifacts (
- session_id TEXT NOT NULL REFERENCES session_control(id),
- id TEXT NOT NULL,
- created_at INTEGER NOT NULL CHECK(created_at >= 0),
- record_json TEXT NOT NULL CHECK(length(CAST(record_json AS BLOB)) <= 16384),
- content_sha256 TEXT NOT NULL,
- payload BLOB NOT NULL CHECK(length(payload) <= 52428800),
- PRIMARY KEY (session_id, id),
- CHECK(json_extract(record_json, '$.sessionId') = session_id),
- CHECK(json_extract(record_json, '$.id') = id),
- CHECK(json_extract(record_json, '$.createdAt') = created_at),
- CHECK(json_extract(record_json, '$.sizeBytes') = length(payload))
-);
-CREATE INDEX IF NOT EXISTS artifacts_session_order ON artifacts(session_id, created_at DESC, id);
-CREATE TABLE IF NOT EXISTS artifact_catalog (
- session_id TEXT PRIMARY KEY REFERENCES session_control(id),
- revision INTEGER NOT NULL CHECK(revision > 0 AND revision <= 9007199254740991)
-);
diff --git a/crates/event-log/migrations/0005_tool_result_payloads.sql b/crates/event-log/migrations/0005_tool_result_payloads.sql
deleted file mode 100644
index cd9819f4ac..0000000000
--- a/crates/event-log/migrations/0005_tool_result_payloads.sql
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- Raw outcomes are part of the execution log; identity and digest live in T2.
-PRAGMA user_version = 5;
-CREATE TABLE IF NOT EXISTS tool_result_payloads (
- event_id TEXT PRIMARY KEY NOT NULL REFERENCES runtime_events(event_id),
- payload BLOB NOT NULL CHECK(length(payload) <= 67108864)
-);
diff --git a/crates/event-log/migrations/0006_shell_runs.sql b/crates/event-log/migrations/0006_shell_runs.sql
deleted file mode 100644
index 2c1ff39f78..0000000000
--- a/crates/event-log/migrations/0006_shell_runs.sql
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
--- Operational shell authority belongs to the existing RootOwner/connection.
--- Invocation history remains canonical for model/tool interactions, not for
--- live process attachment. No operating-system PID is persisted for recovery.
-PRAGMA user_version = 6;
-CREATE TABLE IF NOT EXISTS shell_runs (
- session_id TEXT NOT NULL REFERENCES session_control(id),
- id TEXT NOT NULL,
- started_at INTEGER NOT NULL CHECK(started_at >= 0),
- active INTEGER NOT NULL CHECK(active IN (0, 1)),
- record_json TEXT NOT NULL CHECK(length(CAST(record_json AS BLOB)) <= 4194304),
- PRIMARY KEY (session_id, id),
- CHECK(json_extract(record_json, '$.sessionId') IS session_id),
- CHECK(json_extract(record_json, '$.id') IS id),
- CHECK(json_extract(record_json, '$.startedAt') IS started_at),
- CHECK(active IS (json_extract(record_json, '$.state.kind') IN ('starting', 'running')))
-);
-CREATE INDEX IF NOT EXISTS shell_runs_session_order ON shell_runs(session_id, started_at, id);
-CREATE INDEX IF NOT EXISTS shell_runs_active ON shell_runs(active) WHERE active = 1;
diff --git a/crates/event-log/migrations/0007_steering.sql b/crates/event-log/migrations/0007_steering.sql
deleted file mode 100644
index 059b92ab63..0000000000
--- a/crates/event-log/migrations/0007_steering.sql
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-PRAGMA user_version = 7;
-CREATE UNIQUE INDEX IF NOT EXISTS message_steering_identity ON runtime_events(
- json_extract(event_json, '$.invocation.session_id'),
- json_extract(event_json, '$.fact.message.message_id')
-) WHERE kind = 'message_steered';
-
--- Client message identities are Session-scoped. Only disposable projections
--- change; all existing canonical execution facts remain byte-for-byte intact.
-DROP TABLE IF EXISTS transcript_rows;
-DROP TABLE IF EXISTS transcript_progress;
diff --git a/crates/event-log/migrations/0008_message_sources.sql b/crates/event-log/migrations/0008_message_sources.sql
deleted file mode 100644
index 28258e1fda..0000000000
--- a/crates/event-log/migrations/0008_message_sources.sql
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-PRAGMA user_version = 8;
-DROP INDEX IF EXISTS message_steering_identity;
--- Source identity lookup is rebuilt from the immutable log on initialization.
--- Only disposable UI projections need invalidation for source-owned user IDs.
-DROP TABLE IF EXISTS transcript_rows;
-DROP TABLE IF EXISTS transcript_progress;
diff --git a/crates/event-log/migrations/0009_message_admissions.sql b/crates/event-log/migrations/0009_message_admissions.sql
deleted file mode 100644
index bc362f584c..0000000000
--- a/crates/event-log/migrations/0009_message_admissions.sql
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-PRAGMA user_version = 9;
-CREATE TABLE message_admissions (
- session_id TEXT NOT NULL,
- message_id TEXT NOT NULL,
- position INTEGER NOT NULL,
- record_json TEXT NOT NULL,
- PRIMARY KEY(session_id, message_id)
-);
-CREATE INDEX message_admission_order ON message_admissions(session_id, position, message_id);
diff --git a/crates/event-log/migrations/0010_message_queue.sql b/crates/event-log/migrations/0010_message_queue.sql
deleted file mode 100644
index 0557d101dc..0000000000
--- a/crates/event-log/migrations/0010_message_queue.sql
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-PRAGMA user_version = 10;
-CREATE TABLE message_queue_state (
- session_id TEXT PRIMARY KEY NOT NULL,
- revision INTEGER NOT NULL CHECK(revision >= 0)
-);
-CREATE TABLE message_cancellations (
- session_id TEXT NOT NULL,
- message_id TEXT NOT NULL,
- cancellation_id TEXT NOT NULL,
- PRIMARY KEY(session_id, message_id)
-);
diff --git a/crates/event-log/migrations/0011_queue_command_receipts.sql b/crates/event-log/migrations/0011_queue_command_receipts.sql
deleted file mode 100644
index cc3ba16057..0000000000
--- a/crates/event-log/migrations/0011_queue_command_receipts.sql
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-PRAGMA user_version = 11;
-
-CREATE TABLE queue_command_receipts (
- host_epoch TEXT NOT NULL,
- session_id TEXT NOT NULL,
- operation TEXT NOT NULL,
- command_id TEXT NOT NULL,
- fingerprint TEXT NOT NULL,
- receipt_json TEXT NOT NULL,
- PRIMARY KEY(host_epoch, session_id, operation, command_id)
-);
diff --git a/crates/event-log/migrations/0012_message_submit_receipts.sql b/crates/event-log/migrations/0012_message_submit_receipts.sql
deleted file mode 100644
index dfd72a56cd..0000000000
--- a/crates/event-log/migrations/0012_message_submit_receipts.sql
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-PRAGMA user_version = 12;
-
-CREATE TABLE message_submit_receipts (
- host_epoch TEXT NOT NULL,
- session_id TEXT NOT NULL,
- message_id TEXT NOT NULL,
- receipt_json TEXT NOT NULL,
- PRIMARY KEY(host_epoch, session_id, message_id)
-);
diff --git a/crates/event-log/migrations/0013_message_interrupts.sql b/crates/event-log/migrations/0013_message_interrupts.sql
deleted file mode 100644
index f187b6876e..0000000000
--- a/crates/event-log/migrations/0013_message_interrupts.sql
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-PRAGMA user_version = 13;
-
--- A NULL fence records a rejected exact command; accepted fences revoke pending
--- delivery atomically. Terminal results remain canonical invocation facts.
-CREATE TABLE message_interrupt_receipts (
- host_epoch TEXT NOT NULL,
- session_id TEXT NOT NULL,
- interrupt_id TEXT NOT NULL,
- turn_id TEXT NOT NULL,
- run_id TEXT NOT NULL,
- fence_json TEXT,
- PRIMARY KEY(host_epoch, session_id, interrupt_id)
-);
-CREATE INDEX message_interrupt_run ON message_interrupt_receipts(session_id, run_id)
- WHERE fence_json IS NOT NULL;
diff --git a/crates/event-log/migrations/0014_continuation_claims.sql b/crates/event-log/migrations/0014_continuation_claims.sql
deleted file mode 100644
index f50894cade..0000000000
--- a/crates/event-log/migrations/0014_continuation_claims.sql
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- The canonical opening is the claim. These indexes add no independent state.
-CREATE UNIQUE INDEX continuation_claim_id ON runtime_events (
- json_extract(event_json, '$.fact.input.claim.id')
-) WHERE kind = 'invocation_opened' AND json_extract(event_json, '$.fact.input.kind') = 'continuation';
-
-CREATE UNIQUE INDEX continuation_source_boundary ON runtime_events (
- json_extract(event_json, '$.invocation.session_id'),
- json_extract(event_json, '$.fact.input.claim.source.invocation.run_id'),
- json_extract(event_json, '$.fact.input.claim.source.high_water')
-) WHERE kind = 'invocation_opened' AND json_extract(event_json, '$.fact.input.kind') = 'continuation';
-
-PRAGMA user_version = 14;
diff --git a/crates/event-log/migrations/0015_projects.sql b/crates/event-log/migrations/0015_projects.sql
deleted file mode 100644
index 1a37ca85ac..0000000000
--- a/crates/event-log/migrations/0015_projects.sql
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
--- Project control state shares the Session transaction authority, not runtime history.
-CREATE TABLE projects (
- id TEXT PRIMARY KEY NOT NULL,
- identity TEXT NOT NULL UNIQUE,
- name TEXT NOT NULL CHECK(length(CAST(name AS BLOB)) BETWEEN 1 AND 16384),
- last_used_at INTEGER NOT NULL CHECK(last_used_at BETWEEN 0 AND 9007199254740991),
- archived_at INTEGER CHECK(archived_at BETWEEN 0 AND 9007199254740991)
-) STRICT;
-
--- Canonical IDs and absorbed aliases occupy one namespace.
-CREATE TABLE project_identities (
- id TEXT PRIMARY KEY NOT NULL,
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE
-) STRICT;
-CREATE INDEX project_identity_owner ON project_identities(project_id, id);
-
-CREATE TABLE project_locations (
- project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
- path TEXT NOT NULL CHECK(length(CAST(path AS BLOB)) BETWEEN 1 AND 4096),
- is_worktree INTEGER NOT NULL CHECK(is_worktree IN (0, 1)),
- last_used_at INTEGER NOT NULL CHECK(last_used_at BETWEEN 0 AND 9007199254740991),
- PRIMARY KEY(project_id, path)
-) STRICT;
-
-PRAGMA user_version = 15;
-
diff --git a/crates/event-log/migrations/0016_interrupted_transcript.sql b/crates/event-log/migrations/0016_interrupted_transcript.sql
deleted file mode 100644
index 806e2160f0..0000000000
--- a/crates/event-log/migrations/0016_interrupted_transcript.sql
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
--- Rebuild only the disposable transcript projection with epoch 152 markers.
--- Canonical runtime events and execution state remain unchanged.
-DROP TABLE IF EXISTS transcript_rows;
-DROP TABLE IF EXISTS transcript_progress;
-PRAGMA user_version = 16;
diff --git a/crates/event-log/migrations/0018_workhub_stops.sql b/crates/event-log/migrations/0018_workhub_stops.sql
deleted file mode 100644
index 136d09397a..0000000000
--- a/crates/event-log/migrations/0018_workhub_stops.sql
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
-CREATE TABLE workhub_stops (
- action_id TEXT PRIMARY KEY NOT NULL,
- delegation_action_id TEXT NOT NULL,
- record_json TEXT NOT NULL CHECK(json_valid(record_json)),
- resolution_json TEXT CHECK(resolution_json IS NULL OR json_valid(resolution_json))
-);
-CREATE INDEX workhub_stop_subject ON workhub_stops(delegation_action_id);
-CREATE UNIQUE INDEX workhub_stop_claim ON workhub_stops(delegation_action_id)
- WHERE resolution_json IS NULL OR json_extract(resolution_json, '$.outcome') != 'not_owned';
-PRAGMA user_version = 18;
diff --git a/crates/event-log/migrations/0019_workhub_resume.sql b/crates/event-log/migrations/0019_workhub_resume.sql
deleted file mode 100644
index b0a58ef5e4..0000000000
--- a/crates/event-log/migrations/0019_workhub_resume.sql
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
-DROP INDEX workhub_action_identity;
-CREATE UNIQUE INDEX workhub_action_identity ON runtime_events (
- CASE
- WHEN kind = 'workhub_delegated'
- THEN json_extract(event_json, '$.fact.delegation.action_id')
- WHEN kind = 'workhub_resume_observed'
- THEN json_extract(event_json, '$.fact.resume.action_id')
- WHEN kind = 'invocation_opened'
- AND json_extract(event_json, '$.fact.input.kind') = 'continuation'
- THEN json_extract(event_json, '$.fact.input.workhub_resume.action_id')
- END
-) WHERE kind IN ('workhub_delegated', 'workhub_resume_observed', 'invocation_opened');
-
-PRAGMA user_version = 19;
diff --git a/crates/event-log/migrations/0020_session_events.sql b/crates/event-log/migrations/0020_session_events.sql
deleted file mode 100644
index dcbe7b16bb..0000000000
--- a/crates/event-log/migrations/0020_session_events.sql
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- One ordered ledger, with owner-specific read views. Existing execution bytes
--- and positions remain unchanged; Session facts never enter a Run prefix.
-CREATE TABLE event_log (
- sequence INTEGER PRIMARY KEY AUTOINCREMENT CHECK(sequence > 0),
- event_id TEXT NOT NULL UNIQUE,
- invocation_id TEXT,
- kind TEXT NOT NULL,
- operation_id TEXT,
- event_json TEXT NOT NULL,
- CHECK(invocation_id IS NOT NULL OR operation_id IS NULL)
-);
-INSERT INTO event_log SELECT * FROM runtime_events ORDER BY sequence;
-UPDATE sqlite_sequence SET seq = MAX(seq, COALESCE(
- (SELECT seq FROM sqlite_sequence WHERE name = 'runtime_events'), 0
-)) WHERE name = 'event_log';
-
-CREATE TABLE tool_result_payloads_next (
- event_id TEXT PRIMARY KEY NOT NULL REFERENCES event_log(event_id),
- payload BLOB NOT NULL CHECK(length(payload) <= 67108864)
-);
-INSERT INTO tool_result_payloads_next SELECT * FROM tool_result_payloads;
-DROP TABLE tool_result_payloads;
-DROP TABLE runtime_events;
-ALTER TABLE tool_result_payloads_next RENAME TO tool_result_payloads;
-
-CREATE VIEW runtime_events AS
- SELECT * FROM event_log WHERE invocation_id IS NOT NULL;
-CREATE VIEW session_events AS
- SELECT sequence, event_id, kind, event_json FROM event_log WHERE invocation_id IS NULL;
-
-CREATE INDEX invocation_sequence ON event_log(invocation_id, sequence);
-CREATE INDEX session_event_sequence ON event_log(
- json_extract(event_json, '$.invocation.session_id'), sequence
-);
-CREATE INDEX session_control_sequence ON event_log(
- json_extract(event_json, '$.session_id'), sequence
-) WHERE invocation_id IS NULL;
-CREATE INDEX turn_opening_lookup ON event_log(
- json_extract(event_json, '$.invocation.session_id'),
- json_extract(event_json, '$.invocation.turn_id'), sequence DESC
-) WHERE kind = 'invocation_opened';
-CREATE UNIQUE INDEX operation_fact ON event_log(operation_id, kind)
- WHERE operation_id IS NOT NULL;
-CREATE UNIQUE INDEX invocation_boundary ON event_log(invocation_id, kind)
- WHERE kind IN ('invocation_opened', 'invocation_ended');
-CREATE UNIQUE INDEX message_steering_identity ON event_log(
- json_extract(event_json, '$.invocation.session_id'),
- json_extract(event_json, '$.fact.message.message_id')
-) WHERE kind = 'message_steered';
-CREATE UNIQUE INDEX continuation_claim_id ON event_log(
- json_extract(event_json, '$.fact.input.claim.id')
-) WHERE kind = 'invocation_opened' AND json_extract(event_json, '$.fact.input.kind') = 'continuation';
-CREATE UNIQUE INDEX continuation_source_boundary ON event_log(
- json_extract(event_json, '$.invocation.session_id'),
- json_extract(event_json, '$.fact.input.claim.source.invocation.run_id'),
- json_extract(event_json, '$.fact.input.claim.source.high_water')
-) WHERE kind = 'invocation_opened' AND json_extract(event_json, '$.fact.input.kind') = 'continuation';
-CREATE UNIQUE INDEX workhub_action_identity ON event_log(
- CASE
- WHEN kind = 'workhub_stop_requested'
- THEN json_extract(event_json, '$.fact.intent.request.action_id')
- WHEN kind = 'workhub_delegated'
- THEN json_extract(event_json, '$.fact.delegation.action_id')
- WHEN kind = 'workhub_resume_observed'
- THEN json_extract(event_json, '$.fact.resume.action_id')
- WHEN kind = 'invocation_opened'
- AND json_extract(event_json, '$.fact.input.kind') = 'continuation'
- THEN json_extract(event_json, '$.fact.input.workhub_resume.action_id')
- END
-) WHERE kind IN ('workhub_delegated', 'workhub_resume_observed', 'invocation_opened', 'workhub_stop_requested');
-
-CREATE UNIQUE INDEX workhub_stop_resolution ON event_log(
- json_extract(event_json, '$.fact.action_id')
-) WHERE kind = 'workhub_stop_resolved';
-CREATE INDEX workhub_stop_request_subject ON event_log(
- json_extract(event_json, '$.fact.intent.delegation_action_id')
-) WHERE kind = 'workhub_stop_requested';
-
--- Pre-cutover receipts have no historical sequence or timestamp. Keep them
--- readable, never invent a timeline for them, and never write new receipts here.
-ALTER TABLE workhub_stops RENAME TO legacy_workhub_stops;
-CREATE VIEW workhub_stops AS
- SELECT json_extract(q.event_json, '$.fact.intent.request.action_id') AS action_id,
- json_extract(q.event_json, '$.fact.intent.delegation_action_id') AS delegation_action_id,
- json_extract(q.event_json, '$.fact.intent') AS record_json,
- json_extract(r.event_json, '$.fact.resolution') AS resolution_json
- FROM session_events q LEFT JOIN session_events r
- ON r.kind = 'workhub_stop_resolved'
- AND json_extract(r.event_json, '$.fact.action_id') =
- json_extract(q.event_json, '$.fact.intent.request.action_id')
- WHERE q.kind = 'workhub_stop_requested'
- UNION ALL
- SELECT q.action_id, q.delegation_action_id, q.record_json,
- COALESCE(json_extract(r.event_json, '$.fact.resolution'), q.resolution_json)
- FROM legacy_workhub_stops q LEFT JOIN session_events r
- ON r.kind = 'workhub_stop_resolved'
- AND json_extract(r.event_json, '$.fact.action_id') = q.action_id;
-
-PRAGMA user_version = 20;
diff --git a/crates/event-log/migrations/0021_workhub_assignment_target.sql b/crates/event-log/migrations/0021_workhub_assignment_target.sql
deleted file mode 100644
index 688732401a..0000000000
--- a/crates/event-log/migrations/0021_workhub_assignment_target.sql
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
-CREATE INDEX workhub_assignment_target ON event_log(
- json_extract(event_json, '$.fact.delegation.target.session_id'), sequence DESC
-) WHERE kind = 'workhub_delegated';
-
-PRAGMA user_version = 21;
diff --git a/crates/event-log/migrations/0022_workhub_correction.sql b/crates/event-log/migrations/0022_workhub_correction.sql
deleted file mode 100644
index 53d7eaaf51..0000000000
--- a/crates/event-log/migrations/0022_workhub_correction.sql
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- Assignments keep their original owner and bytes. Replacement commits have
--- Session ownership; the intent, not the later assignment, owns the action ID.
-DROP INDEX workhub_action_identity;
-CREATE UNIQUE INDEX workhub_action_identity ON event_log(
- CASE
- WHEN kind IN ('workhub_stop_requested', 'workhub_correction_requested')
- THEN json_extract(event_json, '$.fact.intent.request.action_id')
- WHEN kind = 'workhub_delegated' AND invocation_id IS NOT NULL
- THEN json_extract(event_json, '$.fact.delegation.action_id')
- WHEN kind = 'workhub_resume_observed'
- THEN json_extract(event_json, '$.fact.resume.action_id')
- WHEN kind = 'invocation_opened'
- AND json_extract(event_json, '$.fact.input.kind') = 'continuation'
- THEN json_extract(event_json, '$.fact.input.workhub_resume.action_id')
- END
-) WHERE kind IN ('workhub_delegated', 'workhub_resume_observed', 'invocation_opened', 'workhub_stop_requested', 'workhub_correction_requested');
-
-CREATE UNIQUE INDEX workhub_assignment_action ON event_log(
- json_extract(event_json, '$.fact.delegation.action_id')
-) WHERE kind = 'workhub_delegated';
-CREATE UNIQUE INDEX workhub_correction_subject ON event_log(
- json_extract(event_json, '$.fact.intent.request.replaces_action_id')
-) WHERE kind = 'workhub_correction_requested';
-CREATE UNIQUE INDEX workhub_correction_resolution ON event_log(
- json_extract(event_json, '$.fact.action_id')
-) WHERE kind IN ('workhub_superseded', 'workhub_correction_aborted');
-
-CREATE VIEW workhub_assignments AS
- SELECT sequence, event_id, event_json,
- COALESCE(json_extract(event_json, '$.invocation'),
- json_extract(event_json, '$.fact.coordinator')) AS coordinator_json
- FROM event_log WHERE kind = 'workhub_delegated';
-
-CREATE VIEW workhub_corrections AS
- SELECT q.sequence,
- json_extract(q.event_json, '$.fact.intent.request.action_id') AS action_id,
- json_extract(q.event_json, '$.fact.intent.request.replaces_action_id') AS replaces_action_id,
- json_extract(q.event_json, '$.fact.intent') AS intent_json,
- r.kind AS resolution_kind,
- json_extract(r.event_json, '$.fact.reason') AS abort_reason
- FROM session_events q LEFT JOIN session_events r
- ON r.kind IN ('workhub_superseded', 'workhub_correction_aborted')
- AND json_extract(r.event_json, '$.fact.action_id') =
- json_extract(q.event_json, '$.fact.intent.request.action_id')
- WHERE q.kind = 'workhub_correction_requested';
-
-PRAGMA user_version = 22;
-
diff --git a/crates/event-log/migrations/0023_handoff_claims.sql b/crates/event-log/migrations/0023_handoff_claims.sql
deleted file mode 100644
index 9098e88b4a..0000000000
--- a/crates/event-log/migrations/0023_handoff_claims.sql
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
--- Both continuations acquire the source through their canonical opening.
-DROP INDEX continuation_claim_id;
-DROP INDEX continuation_source_boundary;
-CREATE UNIQUE INDEX continuation_claim_id ON event_log(
- json_extract(event_json, '$.fact.input.claim.id')
-) WHERE kind = 'invocation_opened' AND json_extract(event_json, '$.fact.input.kind') IN ('continuation','handoff');
-CREATE UNIQUE INDEX continuation_source_boundary ON event_log(
- json_extract(event_json, '$.invocation.session_id'),
- json_extract(event_json, '$.fact.input.claim.source.invocation.run_id'),
- json_extract(event_json, '$.fact.input.claim.source.high_water')
-) WHERE kind = 'invocation_opened' AND json_extract(event_json, '$.fact.input.kind') IN ('continuation','handoff');
-
--- No two paused owners can reserve the same future physical execution.
-CREATE UNIQUE INDEX handoff_successor_run ON event_log(
- json_extract(event_json, '$.fact.outcome.pause.intent.successor_run_id')
-) WHERE kind = 'invocation_ended' AND json_extract(event_json, '$.fact.outcome.kind') = 'handoff_paused';
-CREATE UNIQUE INDEX handoff_successor_invocation ON event_log(
- json_extract(event_json, '$.fact.outcome.pause.intent.successor_invocation_id')
-) WHERE kind = 'invocation_ended' AND json_extract(event_json, '$.fact.outcome.kind') = 'handoff_paused';
-CREATE UNIQUE INDEX handoff_claim ON event_log(
- json_extract(event_json, '$.fact.outcome.pause.intent.claim_id')
-) WHERE kind = 'invocation_ended' AND json_extract(event_json, '$.fact.outcome.kind') = 'handoff_paused';
-
-PRAGMA user_version = 23;
diff --git a/crates/event-log/migrations/0024_plugins.sql b/crates/event-log/migrations/0024_plugins.sql
deleted file mode 100644
index 0359080892..0000000000
--- a/crates/event-log/migrations/0024_plugins.sql
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-
-CREATE TABLE plugin_composition (
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
- ledger_json TEXT NOT NULL CHECK (json_valid(ledger_json))
-);
-INSERT INTO plugin_composition VALUES (1, '{"generation":0,"packageLayers":[],"overlays":[]}');
-
-CREATE TABLE plugin_package_blobs (
- digest TEXT PRIMARY KEY
-);
-CREATE TABLE plugin_package_files (
- digest TEXT NOT NULL REFERENCES plugin_package_blobs(digest) ON DELETE CASCADE,
- path TEXT NOT NULL,
- payload BLOB NOT NULL,
- PRIMARY KEY (digest, path)
-);
-CREATE TABLE plugin_packages (
- id TEXT PRIMARY KEY,
- digest TEXT NOT NULL REFERENCES plugin_package_blobs(digest)
-);
-
-CREATE TABLE plugin_data (
- package_id TEXT NOT NULL,
- scope_id TEXT NOT NULL,
- key TEXT NOT NULL,
- revision INTEGER NOT NULL CHECK (revision > 0),
- value_json TEXT CHECK (value_json IS NULL OR json_valid(value_json)),
- PRIMARY KEY (package_id, scope_id, key)
-);
-
-CREATE TABLE plugin_execution_receipts (
- package_id TEXT NOT NULL,
- scope_id TEXT NOT NULL,
- operation_id TEXT NOT NULL,
- receipt_json TEXT NOT NULL CHECK (json_valid(receipt_json)),
- PRIMARY KEY (package_id, scope_id, operation_id)
-);
-
-PRAGMA user_version = 24;
diff --git a/crates/event-log/migrations/0025_graph.sql b/crates/event-log/migrations/0025_graph.sql
deleted file mode 100644
index 1db96c82ce..0000000000
--- a/crates/event-log/migrations/0025_graph.sql
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-CREATE TABLE graph_epochs (
- root_session_id TEXT NOT NULL,
- epoch INTEGER NOT NULL CHECK(epoch > 0),
- graph_id TEXT NOT NULL UNIQUE,
- mode TEXT NOT NULL CHECK(mode IN ('graph', 'swarm')),
- created_at INTEGER NOT NULL CHECK(created_at >= 0),
- stop_requested INTEGER NOT NULL DEFAULT 0 CHECK(stop_requested IN (0, 1)),
- PRIMARY KEY(root_session_id, epoch)
-);
-CREATE TABLE graph_updates (
- graph_id TEXT NOT NULL REFERENCES graph_epochs(graph_id),
- update_id TEXT NOT NULL,
- revision INTEGER NOT NULL CHECK(revision > 0),
- fingerprint TEXT NOT NULL,
- update_json TEXT NOT NULL CHECK(json_valid(update_json)),
- byte_count INTEGER GENERATED ALWAYS AS (length(CAST(update_json AS BLOB))) STORED,
- committed_at INTEGER NOT NULL CHECK(committed_at >= 0),
- PRIMARY KEY(graph_id, revision),
- UNIQUE(graph_id, update_id)
-);
-CREATE TABLE graph_intents (
- graph_id TEXT NOT NULL REFERENCES graph_epochs(graph_id),
- work_id TEXT NOT NULL,
- intent_json TEXT NOT NULL CHECK(json_valid(intent_json)),
- PRIMARY KEY(graph_id, work_id)
-);
-CREATE TABLE graph_wakes (
- graph_id TEXT NOT NULL REFERENCES graph_epochs(graph_id),
- snapshot_key TEXT NOT NULL,
- request_json TEXT NOT NULL CHECK(json_valid(request_json)),
- PRIMARY KEY(graph_id, snapshot_key)
-);
-PRAGMA user_version = 25;
diff --git a/crates/event-log/migrations/0026_request_composition.sql b/crates/event-log/migrations/0026_request_composition.sql
deleted file mode 100644
index adf12a38c8..0000000000
--- a/crates/event-log/migrations/0026_request_composition.sql
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-CREATE TABLE request_compositions (
- digest TEXT PRIMARY KEY NOT NULL,
- surface BLOB NOT NULL CHECK(length(surface) <= 4194304)
-);
-CREATE TABLE model_request_compositions (
- event_id TEXT PRIMARY KEY NOT NULL REFERENCES event_log(event_id),
- digest TEXT NOT NULL REFERENCES request_compositions(digest)
-);
-CREATE INDEX model_request_composition_digest ON model_request_compositions(digest);
-PRAGMA user_version = 26;
diff --git a/crates/event-log/migrations/0027_session_managers.sql b/crates/event-log/migrations/0027_session_managers.sql
deleted file mode 100644
index fe1dd4f909..0000000000
--- a/crates/event-log/migrations/0027_session_managers.sql
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-CREATE TABLE session_managers (
- session_id TEXT PRIMARY KEY NOT NULL,
- package_id TEXT NOT NULL,
- scope_id TEXT NOT NULL,
- fingerprint TEXT NOT NULL
-);
-PRAGMA user_version = 27;
diff --git a/crates/event-log/src/archive/effective.rs b/crates/event-log/src/archive/effective.rs
index a34bb16538..8cbd4a2147 100644
--- a/crates/event-log/src/archive/effective.rs
+++ b/crates/event-log/src/archive/effective.rs
@@ -94,7 +94,7 @@ pub(crate) async fn affected_after(
pub(crate) async fn validate_summary(
connection: &mut SqliteConnection,
source: &SourceEvidence,
- expected: Option<&str>,
+ expected: &str,
requested: u64,
) -> Result<(), StoreError> {
let selection = Selection::resolve(connection, &source.scope).await?;
@@ -106,31 +106,11 @@ pub(crate) async fn validate_summary(
"summary effective source changed after its request".into(),
));
}
- match expected {
- Some(expected)
- if digest_selected(connection, &selection, source, requested).await? == expected =>
- {
- Ok(())
- }
- Some(_) => Err(StoreError::InvalidTransition(
+ if digest_selected(connection, &selection, source, requested).await? == expected {
+ Ok(())
+ } else {
+ Err(StoreError::InvalidTransition(
"summary effective source digest mismatch".into(),
- )),
- None => {
- let archive_filter = Selection::predicate("a", "?3");
- let target_filter = Selection::predicate("t", "?3");
- let pruned: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
- "SELECT EXISTS(SELECT 1 FROM runtime_events a JOIN runtime_events t
- ON t.event_id=json_extract(a.event_json,'$.fact.placeholder.identity.runtime_event_id')
- WHERE a.kind='tool_result_archived' AND json_extract(a.event_json,'$.invocation.session_id')=?
- AND t.sequence <= ?2 AND {archive_filter} AND {target_filter})"
- ))).bind(&selection.session).bind(source.high_water as i64).bind(&selection.lineage).fetch_one(connection).await?;
- if pruned {
- Err(StoreError::InvalidTransition(
- "legacy summary cannot cover archived projections".into(),
- ))
- } else {
- Ok(())
- }
- }
+ ))
}
}
diff --git a/crates/event-log/src/context/boundary.rs b/crates/event-log/src/context/boundary.rs
index 2625396ea7..24db4571bb 100644
--- a/crates/event-log/src/context/boundary.rs
+++ b/crates/event-log/src/context/boundary.rs
@@ -105,9 +105,7 @@ pub(super) async fn summary_span(
let work: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM runtime_events WHERE invocation_id = ?1 AND sequence > ?2 AND sequence < ?3
AND (kind IN ('message_steered','tool_dispatched','tool_rejected','tool_settled')
- OR (kind = 'model_requested' AND COALESCE(json_extract(event_json, '$.fact.purpose'),
- (SELECT CASE json_extract(o.event_json, '$.fact.input.kind') WHEN 'context_compact' THEN 'summary' ELSE 'main' END
- FROM runtime_events o WHERE o.invocation_id = runtime_events.invocation_id AND o.kind = 'invocation_opened')) != 'summary')))",
+ OR (kind = 'model_requested' AND json_extract(event_json, '$.fact.purpose') != 'summary')))",
).bind(&event.invocation.invocation_id).bind(first as i64).bind(through as i64).fetch_one(connection).await?;
if work {
return Err(invalid(
@@ -131,10 +129,7 @@ pub(super) async fn summary_start(
WHERE invocation_id = ?1 AND kind = 'context_checkpoint_recorded' AND sequence < ?2)
SELECT (SELECT MIN(s.sequence) FROM runtime_events s
WHERE s.invocation_id = ?1 AND s.kind = 'model_requested' AND s.sequence > cut AND s.sequence < ?2
- AND (json_extract(s.event_json, '$.fact.purpose') = 'summary'
- OR (json_extract(s.event_json, '$.fact.purpose') IS NULL AND EXISTS (
- SELECT 1 FROM runtime_events o WHERE o.invocation_id = ?1 AND o.kind = 'invocation_opened'
- AND json_extract(o.event_json, '$.fact.input.kind') = 'context_compact')))),
+ AND json_extract(s.event_json, '$.fact.purpose') = 'summary'),
cut = 0 OR EXISTS (SELECT 1 FROM runtime_events completed
JOIN runtime_events request ON request.invocation_id = completed.invocation_id
AND request.operation_id = completed.operation_id AND request.kind = 'model_requested'
@@ -142,7 +137,7 @@ pub(super) async fn summary_start(
WHERE completed.invocation_id = ?1 AND completed.kind = 'model_completed'
AND completed.sequence > cut AND completed.sequence < ?2
AND json_extract(opening.event_json, '$.fact.input.kind') != 'context_compact'
- AND COALESCE(json_extract(request.event_json, '$.fact.purpose'), 'main') = 'main')
+ AND json_extract(request.event_json, '$.fact.purpose') = 'main')
FROM boundary",
).bind(invocation).bind(through as i64).fetch_one(connection).await?;
if !renewed {
diff --git a/crates/event-log/src/context/latest_main.rs b/crates/event-log/src/context/latest_main.rs
index 16512550a8..79283efa30 100644
--- a/crates/event-log/src/context/latest_main.rs
+++ b/crates/event-log/src/context/latest_main.rs
@@ -126,12 +126,12 @@ pub(super) async fn read_selected(
return None;
}
let kind: &str = row.try_get("opening_kind").ok()?;
- let purpose: Option<&str> = row.try_get("purpose").ok()?;
+ let purpose: &str = row.try_get("purpose").ok()?;
let purpose = match (kind, purpose) {
- ("message" | "continuation", None | Some("main")) => ModelPurpose::Main,
- ("handoff", Some("main")) => ModelPurpose::Main,
- ("message" | "continuation" | "handoff" | "context_compact", Some("summary"))
- | ("context_compact", None) => ModelPurpose::Summary,
+ ("message" | "continuation" | "handoff", "main") => ModelPurpose::Main,
+ ("message" | "continuation" | "handoff" | "context_compact", "summary") => {
+ ModelPurpose::Summary
+ }
_ => return None,
};
if purpose == ModelPurpose::Summary {
diff --git a/crates/event-log/src/context/proof.rs b/crates/event-log/src/context/proof.rs
index 5c1f87a8c9..61dee8d32e 100644
--- a/crates/event-log/src/context/proof.rs
+++ b/crates/event-log/src/context/proof.rs
@@ -127,7 +127,9 @@ pub(super) async fn validate(
high_water: *source_high_water,
digest: source_digest.clone(),
},
- effective_source_digest.as_deref(),
+ effective_source_digest
+ .as_deref()
+ .ok_or_else(|| invalid("summary requires effective source evidence"))?,
first_summary,
)
.await?;
@@ -153,14 +155,13 @@ pub(super) async fn validate(
));
}
let changed_route: bool = sqlx::query_scalar(
- "SELECT EXISTS(SELECT 1 FROM runtime_events WHERE invocation_id = ?1 AND kind = 'model_requested' AND sequence >= ?10 AND sequence < ?2
- AND (json_extract(event_json, '$.fact.purpose') = 'summary' OR (?3 AND json_extract(event_json, '$.fact.purpose') IS NULL))
- AND (json_extract(event_json, '$.fact.model_id') != ?4 OR json_extract(event_json, '$.fact.route_identity') != ?5
- OR json_extract(event_json, '$.fact.source_high_water') != ?6 OR json_extract(event_json, '$.fact.source_digest') != ?7
- OR json_extract(event_json, '$.fact.checkpoint_event_id') IS NOT ?8
- OR json_extract(event_json, '$.fact.effective_source_digest') IS NOT ?9))",
+ "SELECT EXISTS(SELECT 1 FROM runtime_events WHERE invocation_id = ?1 AND kind = 'model_requested' AND sequence >= ?9 AND sequence < ?2
+ AND json_extract(event_json, '$.fact.purpose') = 'summary'
+ AND (json_extract(event_json, '$.fact.model_id') != ?3 OR json_extract(event_json, '$.fact.route_identity') != ?4
+ OR json_extract(event_json, '$.fact.source_high_water') != ?5 OR json_extract(event_json, '$.fact.source_digest') != ?6
+ OR json_extract(event_json, '$.fact.checkpoint_event_id') IS NOT ?7
+ OR json_extract(event_json, '$.fact.effective_source_digest') IS NOT ?8))",
).bind(&event.invocation.invocation_id).bind(sequence as i64)
- .bind(matches!(input, maka_runtime::event::InvocationInput::ContextCompact { .. }))
.bind(model_id).bind(route_identity).bind(checkpoint.covered_through as i64).bind(&checkpoint.source_digest)
.bind(&checkpoint.previous_checkpoint_id).bind(effective_source_digest).bind(first_summary as i64).fetch_one(&mut *connection).await?;
if changed_route {
diff --git a/crates/event-log/src/context/request.rs b/crates/event-log/src/context/request.rs
index bdf0319236..14d0695b99 100644
--- a/crates/event-log/src/context/request.rs
+++ b/crates/event-log/src/context/request.rs
@@ -43,18 +43,6 @@ pub(super) async fn validate(
else {
return Ok(());
};
- // Legacy facts remain replayable. New explicit purposes must agree with the
- // canonical opening; automatic summaries can never use the legacy default.
- if purpose.is_none() {
- let compact: bool=sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM runtime_events WHERE invocation_id=? AND kind='invocation_opened' AND json_extract(event_json,'$.fact.input.kind') IN ('context_compact','continuation','handoff'))")
- .bind(&event.invocation.invocation_id).fetch_one(connection).await?;
- if compact {
- return Err(invalid(
- "compact requires explicit summary purpose; continuation requires explicit main/summary purpose and effective source",
- ));
- }
- return Ok(());
- }
let (opened, opening) = proof::by_kind(
connection,
&event.invocation.invocation_id,
@@ -125,13 +113,12 @@ pub(super) async fn validate(
high_water: high,
digest: source_digest.clone(),
};
- if effective_source_digest.is_none() {
- return Err(invalid("model request requires effective source evidence"));
- }
crate::archive::validate_summary(
connection,
&source,
- effective_source_digest.as_deref(),
+ effective_source_digest
+ .as_deref()
+ .ok_or_else(|| invalid("model request requires effective source evidence"))?,
i64::MAX as u64,
)
.await?;
@@ -150,8 +137,15 @@ pub(super) async fn validate(
if expected != *effective_source_digest {
return Err(invalid("summary repair changed effective source"));
}
- crate::archive::validate_summary(connection, &source, expected.as_deref(), sequence as u64)
- .await?;
+ crate::archive::validate_summary(
+ connection,
+ &source,
+ expected
+ .as_deref()
+ .ok_or_else(|| invalid("summary requires effective source evidence"))?,
+ sequence as u64,
+ )
+ .await?;
}
let first: Option<(String, String, i64, String, Option<String>)> = sqlx::query_as(
"SELECT json_extract(event_json, '$.fact.model_id'), json_extract(event_json, '$.fact.route_identity'),
diff --git a/crates/event-log/src/effects.rs b/crates/event-log/src/effects.rs
new file mode 100644
index 0000000000..5f2d6096c4
--- /dev/null
+++ b/crates/event-log/src/effects.rs
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! One-shot Host resource facts outside an Agent invocation. Only the Host
+//! adapter writes these records; plugin business state uses separate storage.
+
+use crate::{EventLog, StoreError};
+use maka_plugins::{call::Identity, fiber, storage::Namespace};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use sqlx::{Connection, Row};
+use uuid::Uuid;
+
+#[derive(Serialize, Deserialize)]
+#[serde(tag = "kind", content = "input", rename_all = "snake_case")]
+pub enum Operation {
+ File(maka_plugins::filesystem::Operation),
+ Model {
+ input: maka_plugins::llm::Generate,
+ model: maka_runtime::execution::ModelBinding,
+ thinking_level: Option<maka_runtime::execution::ThinkingLevel>,
+ },
+ Client {
+ input: maka_plugins::client_capability::Call,
+ provider: maka_runtime::capability::Identity,
+ registration_id: String,
+ offer_id: String,
+ tool_call_id: String,
+ },
+}
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase", deny_unknown_fields)]
+pub struct Request {
+ pub source: Identity,
+ pub owner: fiber::Identity,
+ pub boundary: maka_plugins::authorization::Boundary,
+ pub operation: Operation,
+}
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+#[serde(
+ tag = "kind",
+ rename_all = "snake_case",
+ rename_all_fields = "camelCase"
+)]
+pub enum Outcome {
+ Completed { value: Value },
+ Image { mime_type: String, digest: String },
+ Failed { message: String },
+ Unknown { message: String },
+}
+pub struct Record {
+ pub request: Request,
+ pub outcome: Option<Outcome>,
+ pub payload: Option<Vec<u8>>,
+}
+impl EventLog {
+ /// Caller may start the effect only after this durable admission succeeds.
+ pub async fn begin_host_effect(&self, request: Request) -> Result<Uuid, StoreError> {
+ self.validate_root()?;
+ if matches!(request.source, Identity::Agent { .. }) {
+ return Err(invalid("Agent effects belong to their invocation journal"));
+ }
+ let namespace = Namespace::new(
+ request.owner.package_id.clone(),
+ request.owner.scope.clone(),
+ )
+ .map_err(|error| invalid(error.to_string()))?;
+ let raw = serde_json::to_string(&request)?;
+ let id = Uuid::new_v4();
+ self.connection.run(move |connection| {
+ Box::pin(async move {
+ let mut tx = connection.begin_with("BEGIN IMMEDIATE").await?;
+ sqlx::query("INSERT INTO host_effects (id, package_id, scope_id, request) VALUES (?, ?, ?, ?)")
+ .bind(id.to_string()).bind(namespace.package()).bind(String::from(namespace.scope().clone())).bind(raw)
+ .execute(&mut *tx).await?;
+ tx.commit().await.map_err(StoreError::CommitUnknown)?;
+ Ok(id)
+ })
+ }).await
+ }
+
+ /// Settlement cannot overwrite a known outcome, including recovery's unknown.
+ pub async fn settle_host_effect(
+ &self,
+ id: Uuid,
+ outcome: Outcome,
+ payload: Option<Vec<u8>>,
+ ) -> Result<(), StoreError> {
+ self.validate_root()?;
+ match (&outcome, &payload) {
+ (Outcome::Image { mime_type, digest }, Some(bytes))
+ if matches!(
+ mime_type.as_str(),
+ "image/png" | "image/jpeg" | "image/gif" | "image/webp"
+ ) && &maka_runtime::artifact::content_digest(bytes) == digest => {}
+ (Outcome::Image { .. }, _) | (_, Some(_)) => {
+ return Err(invalid("invalid effect image payload"));
+ }
+ _ => {}
+ }
+ let raw = serde_json::to_string(&outcome)?;
+ self.connection.run(move |connection| {
+ Box::pin(async move {
+ let mut tx = connection.begin_with("BEGIN IMMEDIATE").await?;
+ let result = sqlx::query("UPDATE host_effects SET outcome = ?, payload = ? WHERE id = ? AND outcome IS NULL")
+ .bind(raw).bind(payload).bind(id.to_string()).execute(&mut *tx).await?;
+ if result.rows_affected() != 1 {
+ return Err(StoreError::EventConflict);
+ }
+ tx.commit().await.map_err(StoreError::CommitUnknown)?;
+ Ok(())
+ })
+ }).await
+ }
+
+ pub async fn host_effect(
+ &self,
+ namespace: Namespace,
+ id: Uuid,
+ ) -> Result<Option<Record>, StoreError> {
+ self.validate_root()?;
+ self.connection.run(move |connection| {
+ Box::pin(async move {
+ let row = sqlx::query("SELECT request, outcome, payload FROM host_effects WHERE id = ? AND package_id = ? AND scope_id = ?")
+ .bind(id.to_string()).bind(namespace.package()).bind(String::from(namespace.scope().clone()))
+ .fetch_optional(connection).await?;
+ row.map(|row| {
+ Ok(Record {
+ request: serde_json::from_str(row.try_get("request")?)?,
+ outcome: row.try_get::<Option<&str>, _>("outcome")?.map(serde_json::from_str).transpose()?,
+ payload: row.try_get("payload")?,
+ })
+ }).transpose()
+ })
+ }).await
+ }
+
+ /// Called only at exclusive Host recovery, before accepting new resource work.
+ pub async fn recover_host_effects(&self) -> Result<(), StoreError> {
+ self.validate_root()?;
+ let outcome = serde_json::to_string(&Outcome::Unknown {
+ message:
+ "Host stopped before recording the effect outcome; do not replay automatically"
+ .into(),
+ })?;
+ self.connection
+ .run(move |connection| {
+ Box::pin(async move {
+ let mut tx = connection.begin_with("BEGIN IMMEDIATE").await?;
+ sqlx::query("UPDATE host_effects SET outcome = ? WHERE outcome IS NULL")
+ .bind(outcome)
+ .execute(&mut *tx)
+ .await?;
+ tx.commit().await.map_err(StoreError::CommitUnknown)?;
+ Ok(())
+ })
... 33401 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org