@moretea-labs/forge 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +70 -7
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +17 -2
- package/CONTRIBUTING.md +3 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/adapters/chatgpt/browser-delivery-host.ts +7 -1
- package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
- package/adapters/chatgpt/controller-binding-store.ts +101 -22
- package/adapters/chatgpt/controller-round-host.ts +15 -25
- package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
- package/adapters/chatgpt/provider-delivery.ts +228 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
- package/adapters/computer/chatgpt-extension-target.ts +463 -0
- package/adapters/computer/chatgpt-target-router.ts +103 -0
- package/adapters/computer/chrome-extension/background.js +385 -0
- package/adapters/computer/chrome-extension/content.js +378 -0
- package/adapters/computer/chrome-extension/core.js +24 -0
- package/adapters/computer/chrome-extension/manifest.json +30 -0
- package/adapters/computer/desktop-operator-discovery.ts +11 -31
- package/adapters/computer/interaction-target-authority.ts +142 -18
- package/adapters/mcp/auth.ts +0 -1
- package/adapters/mcp/instructions.ts +5 -4
- package/adapters/mcp/multi-repository.ts +3 -19
- package/adapters/mcp/oauth.ts +73 -0
- package/adapters/mcp/paths.ts +4 -1
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
- package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
- package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
- package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
- package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
- package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
- package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
- package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
- package/adapters/mcp/runtime-gateway/router.ts +18 -504
- package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
- package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
- package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
- package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
- package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
- package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
- package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
- package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
- package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
- package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
- package/adapters/mcp/server.ts +67 -23
- package/adapters/mcp/setup.ts +1 -3
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
- package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
- package/adapters/mcp/toolset-names.ts +5 -0
- package/adapters/mcp/toolset.ts +6 -5
- package/adapters/mcp/transports/http-observation.ts +365 -0
- package/adapters/mcp/transports/http.ts +154 -677
- package/adapters/mcp/transports/oauth-http.ts +322 -0
- package/adapters/mcp/transports/session-registry.ts +6 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
- package/assets/plugin-registry.v1.json +6 -4
- package/assets/reference-configs/agentic-development-flow.md +25 -0
- package/assets/reference-configs/global-working-rules.md +19 -0
- package/assets/skill-version.json +2 -2
- package/docs/architecture/CURRENT.md +111 -41
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
- package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
- package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
- package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
- package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
- package/docs/architecture/domains/verification.md +1 -0
- package/docs/architecture/domains/workflow-engine.md +6 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/forge-plugin-management.md +2 -2
- package/docs/operations/controller-performance-and-502.md +17 -0
- package/docs/operations/controller-reliability-runbook.md +2 -2
- package/docs/operations/features.md +1 -1
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +2 -0
- package/docs/operations/releasing.md +2 -0
- package/docs/operations/standalone-disaster-recovery.md +14 -6
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +25 -14
- package/packages/kernel/cognition/api/index.ts +6 -0
- package/packages/kernel/cognition/application/activation-service.ts +451 -0
- package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
- package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
- package/packages/kernel/cognition/application/memory-service.ts +64 -0
- package/packages/kernel/cognition/domain/learning.ts +106 -0
- package/packages/kernel/cognition/domain/memory.ts +254 -0
- package/packages/kernel/controller/api/index.ts +1 -0
- package/packages/kernel/controller/application/continuation-service.ts +281 -0
- package/packages/kernel/controller/application/controller-round-service.ts +12 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
- package/packages/kernel/controller/domain/controller-round.ts +4 -1
- package/packages/kernel/controller/domain/execution-quality.ts +3 -1
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
- package/packages/kernel/controller/ports/controller-host.ts +2 -0
- package/packages/kernel/identity/api/index.ts +10 -0
- package/packages/kernel/identity/domain/activity-projection.ts +43 -0
- package/packages/kernel/identity/domain/capability-broker.ts +60 -0
- package/packages/kernel/identity/domain/grant.ts +72 -0
- package/packages/kernel/identity/domain/owned-resource.ts +116 -0
- package/packages/kernel/identity/domain/user-request.ts +93 -0
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
- package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
- package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
- package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
- package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
- package/packages/kernel/scheduler/api/index.ts +0 -2
- package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
- package/packages/kernel/scheduler/application/settlement.ts +0 -15
- package/packages/kernel/scheduler/domain/schedule.ts +87 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
- package/packages/kernel/work/api/index.ts +3 -2
- package/packages/kernel/work/application/work-service.ts +17 -42
- package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
- package/packages/kernel/work/domain/engineering-design.ts +10 -5
- package/packages/kernel/work/domain/engineering-profile.ts +8 -12
- package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
- package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
- package/packages/kernel/work/domain/implementation-review.ts +14 -331
- package/packages/kernel/work/domain/objective-graph.ts +119 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +25 -63
- package/packages/kernel/work/domain/types.ts +57 -51
- package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
- package/packages/kernel/work/ports/work-contract-store.ts +5 -2
- package/packages/plugin-runtime/browser/session-authority.ts +2 -1
- package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
- package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
- package/packages/plugin-runtime/computer/index.ts +2 -0
- package/packages/plugin-runtime/computer/target-authority.ts +26 -1
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
- package/packages/protocols/computer/contract.ts +27 -1
- package/scripts/activate-source-baseline.ts +7 -9
- package/scripts/canary-controller-semantic-wait.ts +12 -24
- package/scripts/check-mcp-compatibility.ts +48 -136
- package/scripts/check-runtime-architecture.mjs +622 -217
- package/scripts/forge-local-recovery-helper.mjs +93 -15
- package/scripts/lib/project-init-lib.sh +3 -0
- package/scripts/open-source-audit-allowlist.txt +1 -2
- package/scripts/run-bounded-command.mjs +5 -0
- package/scripts/run-governed-gate.ts +0 -1
- package/scripts/run-mobile-monitor-bridge.ts +0 -1
- package/scripts/smoke-control-plane-failure-domain.ts +94 -0
- package/scripts/smoke-mcp-http-runtime.ts +5 -0
- package/scripts/smoke-runtime-recovery.ts +12 -43
- package/scripts/smoke-schedule-engine.ts +23 -15
- package/scripts/smoke-unattended-transport-recovery.ts +223 -0
- package/scripts/smoke-user-request-continuation-restart.ts +232 -0
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
- package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
- package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
- package/scripts/smoke-workflow-supervisor.ts +103 -0
- package/scripts/verify-forge-runtime.sh +2 -1
- package/src/cli/agent-jobs/executor-health.ts +11 -34
- package/src/cli/agent-jobs/integration.ts +18 -49
- package/src/cli/agent-jobs/job-manager.ts +7 -3
- package/src/cli/commands/brain-assistant.ts +27 -13
- package/src/cli/commands/brain.ts +13 -15
- package/src/cli/commands/chatgpt.ts +1 -163
- package/src/cli/commands/recovery.ts +135 -121
- package/src/cli/commands/runtime.ts +3 -3
- package/src/cli/commands/setup-profile.ts +8 -0
- package/src/cli/controller/check-runner.ts +13 -2
- package/src/cli/controller/completion-backlog.ts +3 -3
- package/src/cli/controller/completion-orchestrator.ts +4 -43
- package/src/cli/controller/completion-target.ts +3 -72
- package/src/cli/controller/context/source-materializer.ts +81 -100
- package/src/cli/controller/context-pack.ts +10 -8
- package/src/cli/controller/execution-completion.ts +4 -7
- package/src/cli/controller/execution-policy.ts +36 -153
- package/src/cli/controller/governance.ts +2 -3
- package/src/cli/controller/issue-store.ts +4 -99
- package/src/cli/controller/launch-agents.ts +59 -18
- package/src/cli/controller/operational-plan.ts +24 -51
- package/src/cli/controller/runtime-config.ts +5 -1
- package/src/cli/controller/stuck-state-migration.ts +2 -3
- package/src/cli/controller/systemd-user.ts +39 -1
- package/src/cli/editing/edit-session.ts +1 -7
- package/src/cli/local-bridge/console-automations.ts +18 -13
- package/src/cli/local-bridge/console-view-models.ts +3 -12
- package/src/cli/local-bridge/facade-api.ts +64 -191
- package/src/cli/local-bridge/server.ts +24 -147
- package/src/cli/local-bridge/types.ts +0 -1
- package/src/cli/mcp/oauth-client-retirement.ts +74 -0
- package/src/cli/repositories/command-executor.ts +29 -23
- package/src/cli/repositories/command-scope.ts +35 -16
- package/src/cli/repositories/controller-home.ts +48 -0
- package/src/cli/repositories/ephemeral-workspace.ts +20 -30
- package/src/cli/repositories/git-command-executor.ts +2 -2
- package/src/cli/repositories/registry.ts +77 -11
- package/src/cli/repositories/structured-git.ts +62 -2
- package/src/cli/repository/session-cache.ts +92 -6
- package/src/runtime/context/assistant-context.ts +20 -7
- package/src/runtime/context/assistant-work-context.ts +89 -27
- package/src/runtime/context/automatic-learning.ts +565 -0
- package/src/runtime/context/cognitive-attention.ts +173 -0
- package/src/runtime/context/cognitive-skill-canary.ts +158 -0
- package/src/runtime/context/context-closure.ts +46 -9
- package/src/runtime/context/context-pack-process.ts +103 -0
- package/src/runtime/context/context-plane.ts +1 -9
- package/src/runtime/context/direct-controller-learning.ts +298 -0
- package/src/runtime/context/semantic-navigation-contract.ts +9 -0
- package/src/runtime/context/semantic-navigation.ts +6 -34
- package/src/runtime/context/typescript-navigation-process.ts +145 -0
- package/src/runtime/context/typescript-navigation.ts +89 -3
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
- package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
- package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
- package/src/runtime/control-plane/execution/session-store.ts +31 -1
- package/src/runtime/control-plane/execution/validation.ts +6 -8
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
- package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
- package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
- package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
- package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
- package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
- package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
- package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
- package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
- package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
- package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
- package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
- package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
- package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
- package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
- package/src/runtime/control-plane/facade/index.ts +0 -1
- package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
- package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
- package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
- package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
- package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
- package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
- package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
- package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
- package/src/runtime/control-plane/facade/types.ts +49 -108
- package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
- package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
- package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
- package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
- package/src/runtime/control-plane/governance/access-policy.ts +19 -2
- package/src/runtime/control-plane/governance/authorization.ts +20 -3
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
- package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
- package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
- package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
- package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
- package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
- package/src/runtime/control-plane/release-retention.ts +135 -47
- package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
- package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
- package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
- package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
- package/src/runtime/diagnostics/incident-repair.ts +313 -146
- package/src/runtime/diagnostics/mcp-timing.ts +114 -0
- package/src/runtime/evidence/artifact-store.ts +158 -40
- package/src/runtime/evidence/evidence-store.ts +112 -8
- package/src/runtime/evidence/output-handle-index.ts +225 -0
- package/src/runtime/evidence/result-store.ts +158 -44
- package/src/runtime/execution/jobs/store.ts +3 -4
- package/src/runtime/execution/managed-workspace.ts +57 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
- package/src/runtime/execution/process-runtime/check-result.ts +105 -1
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
- package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
- package/src/runtime/execution/process-runtime/gc.ts +15 -2
- package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
- package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
- package/src/runtime/execution/process-runtime/index.ts +3 -0
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
- package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
- package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
- package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
- package/src/runtime/execution/process-runtime/runtime.ts +35 -2
- package/src/runtime/execution/process-runtime/store.ts +30 -2
- package/src/runtime/execution/process-runtime/types.ts +6 -1
- package/src/runtime/execution/workers/executor.ts +2 -6
- package/src/runtime/platform/service-inventory.ts +130 -0
- package/src/runtime/platform/service-manager.ts +8 -0
- package/src/runtime/plugins/action-application.ts +85 -28
- package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
- package/src/runtime/plugins/browser-adapter.ts +757 -34
- package/src/runtime/plugins/browser-automation-service.ts +23 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
- package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
- package/src/runtime/plugins/browser-native-session-service.ts +63 -13
- package/src/runtime/plugins/browser-registration.ts +16 -0
- package/src/runtime/plugins/browser-runtime.ts +14 -1
- package/src/runtime/plugins/browser-session-authority.ts +2 -2
- package/src/runtime/plugins/browser-session-store.ts +46 -76
- package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
- package/src/runtime/plugins/computer-registration.ts +222 -11
- package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
- package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
- package/src/runtime/plugins/external-adapter.ts +42 -6
- package/src/runtime/plugins/external-registration.ts +21 -1
- package/src/runtime/plugins/gmail-adapter.ts +23 -2
- package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
- package/src/runtime/plugins/google-shared.ts +14 -7
- package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
- package/src/runtime/plugins/ios-adapter.ts +1 -1
- package/src/runtime/plugins/lightweight-action.ts +208 -2
- package/src/runtime/plugins/local-system-adapter.ts +12 -2
- package/src/runtime/plugins/managed-process-adapter.ts +12 -5
- package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
- package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +602 -428
- package/src/runtime/plugins/types.ts +20 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
- package/src/runtime/projections/invalidation.ts +7 -3
- package/src/runtime/projections/materialized-view.ts +80 -36
- package/src/runtime/recovery/actions.ts +3 -3
- package/src/runtime/recovery/capability-action-application.ts +46 -45
- package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
- package/src/runtime/recovery/maintenance-executor.ts +374 -123
- package/src/runtime/recovery/store.ts +29 -11
- package/src/runtime/release/release-coordinator.ts +296 -0
- package/src/runtime/release/release-session.ts +716 -0
- package/src/runtime/release/release-state-migration.ts +25 -0
- package/src/runtime/resources/leases/store.ts +56 -25
- package/src/runtime/root/computer-composition.ts +191 -6
- package/src/runtime/root/controller-progression-composition.ts +192 -0
- package/src/runtime/root/controller-round-composition.ts +20 -1
- package/src/runtime/root/deployment-topology.ts +106 -0
- package/src/runtime/root/entry.ts +4 -3
- package/src/runtime/root/known-good-recovery.ts +184 -0
- package/src/runtime/root/local-bridge.ts +0 -1
- package/src/runtime/root/mcp-transport.ts +111 -18
- package/src/runtime/root/ownership.ts +261 -9
- package/src/runtime/root/package-connector-service.ts +105 -19
- package/src/runtime/root/package-runtime-release.ts +117 -18
- package/src/runtime/root/package-runtime-service.ts +17 -11
- package/src/runtime/root/release-execution-canary.ts +44 -12
- package/src/runtime/root/release-loader.ts +90 -8
- package/src/runtime/root/release-manifest.ts +80 -6
- package/src/runtime/root/release-materialize.ts +372 -28
- package/src/runtime/root/release-store.ts +395 -36
- package/src/runtime/root/runtime-lane.ts +191 -0
- package/src/runtime/root/runtime.ts +159 -27
- package/src/runtime/root/scheduled-controller-composition.ts +28 -3
- package/src/runtime/root/service-runner.ts +2 -2
- package/src/runtime/root/service.ts +36 -33
- package/src/runtime/root/status.ts +48 -0
- package/src/runtime/root/types.ts +32 -3
- package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
- package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
- package/src/runtime/root/write-fence.ts +50 -2
- package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
- package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
- package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
- package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
- package/src/runtime/shared/process-environment.ts +20 -9
- package/src/runtime/shared/response-budget.ts +1 -3
- package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
- package/src/runtime/standalone-recovery/core.ts +3702 -342
- package/src/runtime/standalone-recovery/entry.ts +469 -39
- package/src/runtime/standalone-recovery/installer.ts +262 -148
- package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
- package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
- package/src/runtime/standalone-recovery/performance.ts +54 -16
- package/src/runtime/standalone-recovery/release.ts +53 -5
- package/src/runtime/standalone-recovery/service-labels.ts +5 -2
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
- package/src/runtime/workflow/schedules/engine.ts +138 -172
- package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
- package/src/runtime/workflows/runtime.ts +15 -19
- package/src/testing/test-governance.ts +5 -1
- package/supervisor/chatgpt-conversation.ts +23 -0
- package/supervisor/client.ts +201 -0
- package/supervisor/control-plane.ts +868 -0
- package/supervisor/entry.ts +15 -0
- package/supervisor/forge-validators.ts +80 -0
- package/supervisor/index.ts +5 -0
- package/supervisor/native-browser-adapter.ts +786 -0
- package/supervisor/native-messaging/host.ts +39 -0
- package/supervisor/native-messaging/manifest.ts +26 -0
- package/supervisor/native-messaging/protocol.ts +27 -0
- package/supervisor/paths.ts +18 -0
- package/supervisor/protocol.ts +202 -0
- package/supervisor/server.ts +404 -0
- package/supervisor/store.ts +1451 -0
- package/supervisor/tsconfig.json +8 -0
- package/supervisor/types.ts +255 -0
- package/adapters/chatgpt/controller-host.ts +0 -119
- package/adapters/mcp/controller-round-compatibility.ts +0 -184
- package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
- package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
- package/packages/kernel/progression/api/index.ts +0 -2
- package/packages/kernel/progression/application/projector.ts +0 -211
- package/packages/kernel/progression/domain/types.ts +0 -116
- package/packages/kernel/progression/index.ts +0 -1
- package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
- package/packages/kernel/scheduler/domain/continuation.ts +0 -38
- package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
- package/packages/kernel/work/domain/admission-policy.ts +0 -34
- package/scripts/benchmark-background-check-overlap.ts +0 -228
- package/scripts/benchmark-computer-hotpath.ts +0 -255
- package/scripts/benchmark-controller-hotpath.ts +0 -212
- package/scripts/benchmark-controller-round2.ts +0 -191
- package/scripts/benchmark-ios-agent-device.ts +0 -511
- package/scripts/benchmark-mcp-host.sh +0 -117
- package/scripts/benchmark-mcp-transport.ts +0 -169
- package/scripts/benchmark-operational-memory-activation.ts +0 -235
- package/scripts/benchmark-operational-shadow.ts +0 -28
- package/scripts/benchmark-quality-harness.ts +0 -146
- package/scripts/benchmark-route-session-concurrency.ts +0 -915
- package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
- package/scripts/benchmark-thin-harness.ts +0 -251
- package/scripts/benchmark-typescript-navigation.ts +0 -186
- package/scripts/benchmark-work-efficiency.ts +0 -334
- package/scripts/build-controller-ui.ts +0 -28
- package/scripts/check-npm-release.sh +0 -32
- package/scripts/check-public-release-surface.sh +0 -19
- package/scripts/check-release-published.sh +0 -57
- package/scripts/check-release-readiness.sh +0 -37
- package/scripts/check-release-version.mjs +0 -74
- package/scripts/check-tarball-install-smoke.sh +0 -150
- package/scripts/package-source-archive.sh +0 -147
- package/scripts/public-release-files.txt +0 -39
- package/scripts/public-release.ts +0 -174
- package/scripts/publish-release-tarball.sh +0 -27
- package/scripts/route-nl-vs-ts-eval.ts +0 -679
- package/scripts/stage-runtime-release.ts +0 -44
- package/src/cli/controller/progress.ts +0 -656
- package/src/cli/controller/work-mode.ts +0 -181
- package/src/cli/local-bridge/console-assets.ts +0 -17
- package/src/cli/local-bridge/dashboard.ts +0 -17
- package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
- package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
- package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
- package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
- package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/styles.css +0 -28
- package/src/cli/local-bridge/frontend/src/types.ts +0 -15
- package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
- package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
- package/src/cli/local-bridge/ui-dist/app.css +0 -1
- package/src/cli/local-bridge/ui-dist/app.js +0 -11228
- package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
- package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
- package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
- package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
- package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
- package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
- package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
- package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
- package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
- package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
- package/src/runtime/control-plane/routing/index.ts +0 -1
- package/src/runtime/control-plane/routing/route-policy.ts +0 -401
- package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import type { RepositoryRecord } from '../../cli/repositories/types';
|
|
4
|
-
import { CONTROLLER_SCOPE_REPO_ID, controllerSystemRoot, ensureControllerHome, repositoryControllerRoot } from '../../cli/repositories/controller-home';
|
|
4
|
+
import { CONTROLLER_SCOPE_REPO_ID, FORGE_INSTANCE_SCOPE_KEY, controllerSystemRoot, ensureControllerHome, repositoryControllerRoot } from '../../cli/repositories/controller-home';
|
|
5
5
|
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync } from 'fs';
|
|
6
6
|
import type { ExecutionJob, ResourceClaimSpec } from '../execution/jobs/types';
|
|
7
7
|
import { appendRuntimeEvent } from '../evidence/event-ledger';
|
|
@@ -20,17 +20,14 @@ import {
|
|
|
20
20
|
} from './capability-authorization-grants';
|
|
21
21
|
import { markControllerContextProjectionDirty } from '../projections/controller-context';
|
|
22
22
|
import { acquireExecutionLeases, releaseExactExecutionLeases } from '../resources/leases/store';
|
|
23
|
-
import {
|
|
23
|
+
import { isRuntimeWriteFenceError } from '../root/write-fence';
|
|
24
24
|
import {
|
|
25
|
-
acceptSubmittedWorkContract,
|
|
26
|
-
activateWorkContract,
|
|
27
25
|
appendWorkEvidence,
|
|
28
|
-
failWorkContract,
|
|
29
26
|
getWorkContract,
|
|
30
27
|
recordWorkCompletionReceipt,
|
|
31
28
|
updateWorkContract,
|
|
32
29
|
} from '../../../packages/kernel/work/api/index';
|
|
33
|
-
import {
|
|
30
|
+
import { isTerminalSemanticWorkState, type RemoteEffectCompletionReceipt, type WorkContract } from '../control-plane/facade/types';
|
|
34
31
|
import type {
|
|
35
32
|
AssistantPluginAdapter,
|
|
36
33
|
AssistantPluginActionDescriptor,
|
|
@@ -75,65 +72,191 @@ function now(): string {
|
|
|
75
72
|
return new Date().toISOString();
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
|
|
75
|
+
const CONTROLLER_PLUGIN_CACHE_SCOPE = 'controller';
|
|
76
|
+
const CONTROLLER_PLUGIN_GRANT_SCOPE = 'controller:global';
|
|
77
|
+
const CONTROLLER_PLUGIN_SCOPE_KEY = FORGE_INSTANCE_SCOPE_KEY;
|
|
78
|
+
|
|
79
|
+
interface PluginActionScopeContext {
|
|
80
|
+
kind: 'controller' | 'repository';
|
|
81
|
+
scopeKey: string;
|
|
82
|
+
grantRepoId: string;
|
|
83
|
+
root: string;
|
|
84
|
+
checkoutId?: string;
|
|
85
|
+
repository?: RepositoryRecord;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function repositoryPluginActionScope(repository: RepositoryRecord): PluginActionScopeContext {
|
|
89
|
+
return {
|
|
90
|
+
kind: 'repository',
|
|
91
|
+
scopeKey: repository.repoId,
|
|
92
|
+
grantRepoId: repository.repoId,
|
|
93
|
+
root: repository.canonicalRoot,
|
|
94
|
+
checkoutId: repository.activeCheckoutId,
|
|
95
|
+
repository,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function controllerPluginActionScope(controllerHome: string): PluginActionScopeContext {
|
|
100
|
+
return {
|
|
101
|
+
kind: 'controller',
|
|
102
|
+
scopeKey: CONTROLLER_PLUGIN_SCOPE_KEY,
|
|
103
|
+
grantRepoId: CONTROLLER_PLUGIN_GRANT_SCOPE,
|
|
104
|
+
root: controllerSystemRoot(controllerHome),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pluginActionExecutionFields(scope: PluginActionScopeContext): { repoId: string; repoRoot: string } {
|
|
109
|
+
// Adapter fields are a compatibility projection. controller:global identifies
|
|
110
|
+
// the Grant/config domain; it is not a Repository Registry identity.
|
|
111
|
+
return { repoId: scope.grantRepoId, repoRoot: scope.root };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function controllerPluginsRoot(controllerHome: string): string {
|
|
115
|
+
return join(controllerSystemRoot(controllerHome), 'plugins');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function controllerManifestPath(controllerHome: string, pluginId: string): string {
|
|
119
|
+
return join(controllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function controllerIndexPath(controllerHome: string): string {
|
|
123
|
+
return join(controllerPluginsRoot(controllerHome), 'index.json');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function adapterMatchesController(adapter: AssistantPluginAdapter): boolean {
|
|
127
|
+
const scope = adapter.scope ?? 'repository';
|
|
128
|
+
return scope === 'controller' || scope === 'controller_with_repository_overlay';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function adapterMatchesRepository(adapter: AssistantPluginAdapter, _repository: RepositoryRecord): boolean {
|
|
132
|
+
const scope = adapter.scope ?? 'repository';
|
|
133
|
+
return scope === 'repository' || scope === 'controller_with_repository_overlay';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function computeControllerManifest(
|
|
137
|
+
controllerHome: string,
|
|
138
|
+
pluginId: string,
|
|
139
|
+
resolvedAdapter?: AssistantPluginAdapter,
|
|
140
|
+
): AssistantPluginManifest {
|
|
141
|
+
const adapter = resolvedAdapter ?? resolvePluginAdapter(controllerHome, pluginId);
|
|
142
|
+
if (!adapter || !adapterMatchesController(adapter)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
|
|
143
|
+
const previous = readControllerStoredPluginManifest(controllerHome, pluginId);
|
|
79
144
|
const root = controllerSystemRoot(controllerHome);
|
|
80
|
-
const
|
|
145
|
+
const built = adapter.buildManifest(previous?.revision ?? 0, previous?.updatedAt, root, {
|
|
146
|
+
controllerHome,
|
|
147
|
+
repoId: CONTROLLER_PLUGIN_GRANT_SCOPE,
|
|
148
|
+
repoRoot: root,
|
|
149
|
+
controllerScoped: true,
|
|
150
|
+
});
|
|
151
|
+
const changed = !previous || fingerprintManifest(previous) !== fingerprintManifest(built);
|
|
81
152
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
canonicalRoot: root,
|
|
86
|
-
localRoot: root,
|
|
87
|
-
activeCheckoutId: 'controller',
|
|
88
|
-
checkouts: [],
|
|
89
|
-
defaultBranch: 'none',
|
|
90
|
-
repositoryType: 'local-git',
|
|
91
|
-
enabled: true,
|
|
92
|
-
createdAt: timestamp,
|
|
93
|
-
updatedAt: timestamp,
|
|
94
|
-
lastSeenAt: timestamp,
|
|
95
|
-
configurationPath: join(root, 'local-system', 'targets.json'),
|
|
96
|
-
stateStorageStrategy: 'controller-home',
|
|
153
|
+
...built,
|
|
154
|
+
revision: previous ? (changed ? previous.revision + 1 : previous.revision) : 1,
|
|
155
|
+
updatedAt: changed ? now() : previous?.updatedAt ?? built.updatedAt,
|
|
97
156
|
};
|
|
98
157
|
}
|
|
99
158
|
|
|
100
|
-
/**
|
|
101
|
-
* Controller-scoped product facade. The synthetic RepositoryRecord remains an
|
|
102
|
-
* internal storage/projection compatibility shape only; callers need no repo.
|
|
103
|
-
*/
|
|
104
159
|
export function readControllerStoredPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest | undefined {
|
|
105
|
-
|
|
160
|
+
try {
|
|
161
|
+
const canonicalPath = controllerManifestPath(controllerHome, pluginId);
|
|
162
|
+
if (existsSync(canonicalPath)) return readJsonFile<AssistantPluginManifest>(canonicalPath);
|
|
163
|
+
const legacyPath = join(legacyControllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
|
|
164
|
+
return existsSync(legacyPath) ? readJsonFile<AssistantPluginManifest>(legacyPath) : undefined;
|
|
165
|
+
} catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function listControllerPluginIds(controllerHome: string): string[] {
|
|
171
|
+
return listPluginAdapters(controllerHome).filter(adapterMatchesController).map((adapter) => adapter.pluginId).sort();
|
|
106
172
|
}
|
|
107
173
|
|
|
108
174
|
export function listControllerPluginManifests(
|
|
109
175
|
controllerHome: string,
|
|
110
176
|
options: ListAssistantPluginManifestsOptions = {},
|
|
111
177
|
): AssistantPluginManifest[] {
|
|
112
|
-
|
|
178
|
+
const preferStored = options.preferStored === true && options.forceRefresh !== true;
|
|
179
|
+
const fallbackToLive = options.fallbackToLive !== false || !preferStored;
|
|
180
|
+
const cacheKey = listCacheKey(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, preferStored, fallbackToLive);
|
|
181
|
+
if (options.forceRefresh !== true) {
|
|
182
|
+
const cached = readPluginManifestCache(pluginManifestListCache, cacheKey, preferStored ? PLUGIN_MANIFEST_STORED_CACHE_TTL_MS : PLUGIN_MANIFEST_LIVE_CACHE_TTL_MS);
|
|
183
|
+
if (cached) return cached;
|
|
184
|
+
}
|
|
185
|
+
const manifests = listControllerPluginIds(controllerHome).map((pluginId) => {
|
|
186
|
+
if (preferStored) {
|
|
187
|
+
const stored = readControllerStoredPluginManifest(controllerHome, pluginId);
|
|
188
|
+
if (stored) return stored;
|
|
189
|
+
if (!fallbackToLive) return undefined;
|
|
190
|
+
}
|
|
191
|
+
return computeControllerManifest(controllerHome, pluginId);
|
|
192
|
+
}).filter((manifest): manifest is AssistantPluginManifest => Boolean(manifest)).sort((left, right) => left.pluginId.localeCompare(right.pluginId));
|
|
193
|
+
primePluginManifestItemCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifests, preferStored);
|
|
194
|
+
return writePluginManifestCache(pluginManifestListCache, cacheKey, manifests);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getControllerPluginManifestForExecution(
|
|
198
|
+
controllerHome: string,
|
|
199
|
+
pluginId: string,
|
|
200
|
+
adapter: AssistantPluginAdapter,
|
|
201
|
+
): { manifest: AssistantPluginManifest; providerIdentityPrevalidated: boolean } {
|
|
202
|
+
const cacheKey = itemCacheKey(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId, false);
|
|
203
|
+
const cached = readPluginManifestCache(pluginManifestItemCache, cacheKey);
|
|
204
|
+
const wrap = (manifest: AssistantPluginManifest) => ({
|
|
205
|
+
manifest,
|
|
206
|
+
providerIdentityPrevalidated: manifest.enabled && manifest.health.probed && manifest.health.ready,
|
|
207
|
+
});
|
|
208
|
+
if (cached) return wrap(cached);
|
|
209
|
+
return wrap(writePluginManifestCache(pluginManifestItemCache, cacheKey, computeControllerManifest(controllerHome, pluginId, adapter)));
|
|
113
210
|
}
|
|
114
211
|
|
|
115
212
|
export function getControllerPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest {
|
|
116
|
-
|
|
213
|
+
const adapter = resolvePluginAdapter(controllerHome, pluginId);
|
|
214
|
+
if (!adapter || !adapterMatchesController(adapter)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
|
|
215
|
+
return getControllerPluginManifestForExecution(controllerHome, pluginId, adapter).manifest;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function writeControllerRegistry(controllerHome: string, manifests: AssistantPluginManifest[]): AssistantPluginRegistryIndex {
|
|
219
|
+
const index: AssistantPluginRegistryIndex = {
|
|
220
|
+
schemaVersion: 1,
|
|
221
|
+
updatedAt: now(),
|
|
222
|
+
plugins: manifests.map((manifest) => ({
|
|
223
|
+
...pluginIndexEntry(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest),
|
|
224
|
+
manifestPath: controllerManifestPath(controllerHome, manifest.pluginId),
|
|
225
|
+
})).sort((left, right) => left.pluginId.localeCompare(right.pluginId)),
|
|
226
|
+
};
|
|
227
|
+
writeJsonAtomic(controllerIndexPath(controllerHome), index);
|
|
228
|
+
return index;
|
|
117
229
|
}
|
|
118
230
|
|
|
119
231
|
export function syncControllerPluginRegistry(controllerHome: string) {
|
|
120
232
|
reconcileFirstPartyExternalPluginRegistrations(controllerHome);
|
|
121
|
-
|
|
233
|
+
invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE);
|
|
234
|
+
const manifests = listControllerPluginManifests(controllerHome, { forceRefresh: true });
|
|
235
|
+
for (const manifest of manifests) writeJsonAtomic(controllerManifestPath(controllerHome, manifest.pluginId), manifest);
|
|
236
|
+
return { manifests, index: writeControllerRegistry(controllerHome, manifests) };
|
|
122
237
|
}
|
|
123
238
|
|
|
124
239
|
export function syncControllerPluginManifest(controllerHome: string, pluginId: string) {
|
|
125
240
|
reconcileFirstPartyExternalPluginRegistration(controllerHome, pluginId);
|
|
126
|
-
|
|
241
|
+
invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId);
|
|
242
|
+
const manifest = computeControllerManifest(controllerHome, pluginId);
|
|
243
|
+
writeJsonAtomic(controllerManifestPath(controllerHome, pluginId), manifest);
|
|
244
|
+
cacheAssistantPluginManifest(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest, false);
|
|
245
|
+
cacheAssistantPluginManifest(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest, true);
|
|
246
|
+
const manifests = listControllerPluginIds(controllerHome).map((candidate) => candidate === pluginId
|
|
247
|
+
? manifest
|
|
248
|
+
: readControllerStoredPluginManifest(controllerHome, candidate) ?? cachedManifestForRepository(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, candidate))
|
|
249
|
+
.filter((entry): entry is AssistantPluginManifest => Boolean(entry)).sort((left, right) => left.pluginId.localeCompare(right.pluginId));
|
|
250
|
+
return { manifest, index: writeControllerRegistry(controllerHome, manifests) };
|
|
127
251
|
}
|
|
128
252
|
|
|
129
253
|
export function removeControllerPluginManifestProjection(controllerHome: string, pluginId: string) {
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return repository.repoId === CONTROLLER_SCOPE_REPO_ID ? scope === 'controller' : scope === 'repository';
|
|
254
|
+
invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId);
|
|
255
|
+
rmSync(controllerManifestPath(controllerHome, pluginId), { force: true });
|
|
256
|
+
const current = readJsonFile<AssistantPluginRegistryIndex>(controllerIndexPath(controllerHome), { schemaVersion: 1, updatedAt: now(), plugins: [] });
|
|
257
|
+
const next = { ...current, updatedAt: now(), plugins: current.plugins.filter((entry) => entry.pluginId !== pluginId) };
|
|
258
|
+
writeJsonAtomic(controllerIndexPath(controllerHome), next);
|
|
259
|
+
return next;
|
|
137
260
|
}
|
|
138
261
|
|
|
139
262
|
function cloneCacheValue<T>(value: T): T {
|
|
@@ -226,7 +349,13 @@ export function clearAssistantPluginManifestCacheForTest(): void {
|
|
|
226
349
|
}
|
|
227
350
|
|
|
228
351
|
function pluginsRoot(controllerHome: string, repoId: string): string {
|
|
229
|
-
return
|
|
352
|
+
return repoId === CONTROLLER_SCOPE_REPO_ID
|
|
353
|
+
? join(controllerSystemRoot(controllerHome), 'plugins')
|
|
354
|
+
: join(repositoryControllerRoot(controllerHome, repoId), 'plugins');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function legacyControllerPluginsRoot(controllerHome: string): string {
|
|
358
|
+
return join(repositoryControllerRoot(controllerHome, CONTROLLER_SCOPE_REPO_ID), 'plugins');
|
|
230
359
|
}
|
|
231
360
|
|
|
232
361
|
function manifestPath(controllerHome: string, repoId: string, pluginId: string): string {
|
|
@@ -271,8 +400,14 @@ function pluginIndexEntry(controllerHome: string, repoId: string, manifest: Assi
|
|
|
271
400
|
}
|
|
272
401
|
|
|
273
402
|
function readStoredManifest(controllerHome: string, repoId: string, pluginId: string): AssistantPluginManifest | undefined {
|
|
403
|
+
const canonicalPath = manifestPath(controllerHome, repoId, pluginId);
|
|
274
404
|
try {
|
|
275
|
-
return readJsonFile<AssistantPluginManifest>(
|
|
405
|
+
if (existsSync(canonicalPath)) return readJsonFile<AssistantPluginManifest>(canonicalPath);
|
|
406
|
+
if (repoId === CONTROLLER_SCOPE_REPO_ID) {
|
|
407
|
+
const legacyPath = join(legacyControllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
|
|
408
|
+
if (existsSync(legacyPath)) return readJsonFile<AssistantPluginManifest>(legacyPath);
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
276
411
|
} catch {
|
|
277
412
|
return undefined;
|
|
278
413
|
}
|
|
@@ -348,7 +483,7 @@ function computeManifest(
|
|
|
348
483
|
controllerHome,
|
|
349
484
|
repoId: repository.repoId,
|
|
350
485
|
repoRoot: repository.canonicalRoot,
|
|
351
|
-
controllerScoped:
|
|
486
|
+
controllerScoped: false,
|
|
352
487
|
});
|
|
353
488
|
const changed = !previous || fingerprintManifest(previous) !== fingerprintManifest(built);
|
|
354
489
|
return {
|
|
@@ -370,59 +505,76 @@ function writeRegistry(controllerHome: string, repoId: string, manifests: Assist
|
|
|
370
505
|
return index;
|
|
371
506
|
}
|
|
372
507
|
|
|
373
|
-
|
|
508
|
+
function claimsForPluginActionScope(
|
|
374
509
|
action: AssistantPluginActionDescriptor,
|
|
375
|
-
|
|
510
|
+
scope: PluginActionScopeContext,
|
|
376
511
|
trustedPluginId: string,
|
|
377
512
|
): ResourceClaimSpec[] {
|
|
378
|
-
const controllerScoped = repository.repoId === CONTROLLER_SCOPE_REPO_ID;
|
|
379
513
|
const pluginId = trustedPluginId.trim().replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
380
514
|
if (!pluginId) throw new Error('PLUGIN_ID_REQUIRED_FOR_RESOURCE_CLAIMS');
|
|
381
515
|
return action.resourceClaims.map((claim) => ({
|
|
382
516
|
resourceKey: claim.resource === 'remote' || claim.resource === 'provider-state'
|
|
383
|
-
// The manifest/plugin id was resolved from the trusted registration; a
|
|
384
|
-
// provider never supplies arbitrary resource-key text.
|
|
385
517
|
? `provider-state:${pluginId}`
|
|
386
|
-
:
|
|
518
|
+
: scope.kind === 'controller'
|
|
387
519
|
? `controller-system:${claim.resource}`
|
|
388
520
|
: claim.resource === 'workspace'
|
|
389
|
-
? `workspace:${
|
|
521
|
+
? `workspace:${scope.checkoutId}`
|
|
390
522
|
: claim.resource === 'git-refs'
|
|
391
|
-
? `git-refs:${
|
|
392
|
-
: `repo-state:${
|
|
523
|
+
? `git-refs:${scope.scopeKey}`
|
|
524
|
+
: `repo-state:${scope.scopeKey}`,
|
|
393
525
|
mode: claim.mode,
|
|
394
526
|
}));
|
|
395
527
|
}
|
|
396
528
|
|
|
529
|
+
export function claimsForAssistantPluginAction(
|
|
530
|
+
action: AssistantPluginActionDescriptor,
|
|
531
|
+
repository: RepositoryRecord,
|
|
532
|
+
trustedPluginId: string,
|
|
533
|
+
): ResourceClaimSpec[] {
|
|
534
|
+
return claimsForPluginActionScope(action, repositoryPluginActionScope(repository), trustedPluginId);
|
|
535
|
+
}
|
|
536
|
+
|
|
397
537
|
async function withAssistantPluginResourceLeases<T>(
|
|
398
538
|
controllerHome: string,
|
|
399
|
-
|
|
539
|
+
scope: PluginActionScopeContext,
|
|
400
540
|
action: AssistantPluginActionDescriptor,
|
|
401
541
|
request: AssistantPluginActionRequest,
|
|
402
542
|
operation: () => Promise<T>,
|
|
403
543
|
): Promise<T> {
|
|
404
|
-
const claims =
|
|
544
|
+
const claims = claimsForPluginActionScope(action, scope, request.pluginId).map((claim) => ({
|
|
405
545
|
...claim,
|
|
406
|
-
repoId:
|
|
407
|
-
checkoutId:
|
|
546
|
+
repoId: scope.scopeKey,
|
|
547
|
+
...(scope.checkoutId ? { checkoutId: scope.checkoutId } : {}),
|
|
408
548
|
}));
|
|
409
549
|
if (claims.length === 0) return operation();
|
|
410
550
|
|
|
411
|
-
|
|
551
|
+
// requestId is the semantic idempotency key, but before its receipt/index is
|
|
552
|
+
// persisted two concurrent replays can enter this function. Give each live
|
|
553
|
+
// invocation a distinct lease owner so the later replay contends on the
|
|
554
|
+
// first invocation's declared resource instead of silently accumulating a
|
|
555
|
+
// second same-owner lease and then failing exact release with LEASE_SET_MISMATCH.
|
|
556
|
+
const ownerJobId = `plugin:${request.requestId}:${randomUUID()}`;
|
|
412
557
|
const timeoutMs = Math.max(5_000, Math.min(10 * 60_000, request.timeoutMs ?? action.defaultTimeoutMs));
|
|
413
|
-
const acquisition = acquireExecutionLeases(controllerHome,
|
|
558
|
+
const acquisition = acquireExecutionLeases(controllerHome, scope.scopeKey, ownerJobId, claims, {
|
|
414
559
|
ttlMs: timeoutMs + 60_000,
|
|
560
|
+
// This lease exists only to fence concurrent invocations of one external
|
|
561
|
+
// effect. It is not Scheduler/recovery authority, so derived projection,
|
|
562
|
+
// event-ledger, and wake side effects must never participate in the effect
|
|
563
|
+
// outcome. The lease file + fencing token remain cross-process durable for
|
|
564
|
+
// the bounded invocation and expire if its process dies.
|
|
565
|
+
visibility: 'ephemeral',
|
|
415
566
|
ownerIdentity: {
|
|
416
|
-
repositoryId:
|
|
417
|
-
checkoutId:
|
|
418
|
-
worktreeId:
|
|
567
|
+
repositoryId: scope.scopeKey,
|
|
568
|
+
checkoutId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
|
|
569
|
+
worktreeId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
|
|
419
570
|
branch: 'workflow-plugin-action',
|
|
420
571
|
principalId: request.origin.actor?.trim() || 'workflow-plugin-action',
|
|
421
572
|
controllerInstanceId: process.env.FORGE_RUNTIME_INSTANCE_ID?.trim()
|
|
422
573
|
|| process.env.FORGE_WRITER_INSTANCE_ID?.trim()
|
|
423
574
|
|| process.env.FORGE_DAEMON_INSTANCE_ID?.trim()
|
|
424
575
|
|| `process:${process.pid}`,
|
|
425
|
-
controllerGeneration: process.env.
|
|
576
|
+
controllerGeneration: process.env.FORGE_RUNTIME_INCARNATION_GENERATION?.trim()
|
|
577
|
+
|| process.env.FORGE_WRITER_GENERATION?.trim()
|
|
426
578
|
|| process.env.FORGE_ACTIVE_RUNTIME_REVISION?.trim()
|
|
427
579
|
|| 'unbound',
|
|
428
580
|
},
|
|
@@ -455,13 +607,24 @@ async function withAssistantPluginResourceLeases<T>(
|
|
|
455
607
|
retainForReconciliation = isAssistantPluginError(error) && error.effectOutcome === 'outcome_unknown';
|
|
456
608
|
throw error;
|
|
457
609
|
} finally {
|
|
458
|
-
if (!retainForReconciliation)
|
|
610
|
+
if (!retainForReconciliation) {
|
|
611
|
+
try {
|
|
612
|
+
releaseExactExecutionLeases(controllerHome, scope.scopeKey, ownerJobId, expected, { visibility: 'ephemeral' });
|
|
613
|
+
} catch (error) {
|
|
614
|
+
// A successful provider effect can rotate Runtime authority (for example
|
|
615
|
+
// a fenced Runtime cutover). The old sidecar must not release leases
|
|
616
|
+
// after it is fenced, but that cleanup denial must not overwrite the
|
|
617
|
+
// already-known provider result. The canonical successor Runtime can
|
|
618
|
+
// reclaim this ephemeral lease from its fenced owner immediately.
|
|
619
|
+
if (!isRuntimeWriteFenceError(error)) throw error;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
459
622
|
}
|
|
460
623
|
}
|
|
461
624
|
|
|
462
|
-
function semanticKey(
|
|
625
|
+
function semanticKey(scope: PluginActionScopeContext, pluginId: string, actionId: string, args: Record<string, unknown>): string {
|
|
463
626
|
const digest = createHash('sha256').update(JSON.stringify(canonical(args))).digest('hex').slice(0, 20);
|
|
464
|
-
return `plugin-action:${
|
|
627
|
+
return `plugin-action:${scope.scopeKey}:${pluginId}:${actionId}:${digest}`;
|
|
465
628
|
}
|
|
466
629
|
|
|
467
630
|
function validatePrimitive(type: string, value: unknown): boolean {
|
|
@@ -582,7 +745,7 @@ function authorizationTargetMatches(
|
|
|
582
745
|
|
|
583
746
|
async function resolveAutomatedWriteAuthorization(input: {
|
|
584
747
|
controllerHome: string;
|
|
585
|
-
|
|
748
|
+
scope: PluginActionScopeContext;
|
|
586
749
|
adapter: AssistantPluginAdapter;
|
|
587
750
|
manifest: AssistantPluginManifest;
|
|
588
751
|
action: AssistantPluginActionDescriptor;
|
|
@@ -610,14 +773,14 @@ async function resolveAutomatedWriteAuthorization(input: {
|
|
|
610
773
|
}
|
|
611
774
|
const targetContext = await input.adapter.resolveAuthorizationContext({
|
|
612
775
|
controllerHome: input.controllerHome,
|
|
613
|
-
|
|
614
|
-
repoRoot: input.repository.canonicalRoot,
|
|
776
|
+
...pluginActionExecutionFields(input.scope),
|
|
615
777
|
pluginId: input.manifest.pluginId,
|
|
616
778
|
actionId: input.action.actionId,
|
|
617
779
|
requestId: input.requestId,
|
|
618
780
|
args: input.args,
|
|
619
781
|
origin: input.origin,
|
|
620
782
|
jobId: input.jobId,
|
|
783
|
+
authorizationGrantRefs: refs,
|
|
621
784
|
});
|
|
622
785
|
if (!targetContext) {
|
|
623
786
|
throw new AssistantPluginError(
|
|
@@ -630,7 +793,7 @@ async function resolveAutomatedWriteAuthorization(input: {
|
|
|
630
793
|
const grant = refs
|
|
631
794
|
.map((ref) => findActivePluginCapabilityAuthorizationById(input.controllerHome, ref))
|
|
632
795
|
.find((candidate) => candidate
|
|
633
|
-
&& candidate.repoId === input.
|
|
796
|
+
&& candidate.repoId === input.scope.grantRepoId
|
|
634
797
|
&& candidate.pluginId === input.manifest.pluginId
|
|
635
798
|
&& candidate.capabilityId === capabilityId
|
|
636
799
|
&& authorizationTargetMatches(candidate.target, targetContext.target)
|
|
@@ -819,25 +982,39 @@ export function removeAssistantPluginManifestProjection(
|
|
|
819
982
|
}
|
|
820
983
|
|
|
821
984
|
export function isDirectPluginReadAction(action: AssistantPluginActionDescriptor): boolean {
|
|
822
|
-
return action.
|
|
985
|
+
return action.executionMode !== 'direct_non_persistent'
|
|
986
|
+
&& action.readOnly === true
|
|
823
987
|
&& action.risk === 'readonly'
|
|
824
988
|
&& action.confirmation === 'none'
|
|
825
989
|
&& action.idempotent === true;
|
|
826
990
|
}
|
|
827
991
|
|
|
828
992
|
export function isDirectNonPersistentPluginAction(action: AssistantPluginActionDescriptor): boolean {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
993
|
+
if (
|
|
994
|
+
action.executionMode !== 'direct_non_persistent'
|
|
995
|
+
|| action.remoteEffectWorkCompletion !== undefined
|
|
996
|
+
|| action.resourceClaims.length !== 0
|
|
997
|
+
) {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Direct non-persistent actions are provider-local operations that must never
|
|
1002
|
+
// enter the durable effect/receipt path. They are intentionally limited to
|
|
1003
|
+
// either a pure provider read or a provider-local workspace effect. Remote
|
|
1004
|
+
// writes/destructive effects still require the normal durable application
|
|
1005
|
+
// path and its reconciliation authority.
|
|
1006
|
+
if (action.readOnly) {
|
|
1007
|
+
return action.risk === 'readonly'
|
|
1008
|
+
&& (action.confirmation === 'none' || action.confirmation === 'authorization')
|
|
1009
|
+
&& action.idempotent === true;
|
|
1010
|
+
}
|
|
1011
|
+
return action.risk === 'workspace_write'
|
|
1012
|
+
&& (action.confirmation === 'authorization' || action.confirmation === 'none');
|
|
836
1013
|
}
|
|
837
1014
|
|
|
838
|
-
|
|
1015
|
+
async function executePluginDirectNonPersistentInScope(
|
|
839
1016
|
controllerHome: string,
|
|
840
|
-
|
|
1017
|
+
scope: PluginActionScopeContext,
|
|
841
1018
|
request: AssistantPluginActionRequest,
|
|
842
1019
|
): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown> }> {
|
|
843
1020
|
const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
|
|
@@ -845,7 +1022,9 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
845
1022
|
if (PLUGIN_ADAPTERS.get(request.pluginId) !== adapter) {
|
|
846
1023
|
throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_FIRST_PARTY_REQUIRED: ${request.pluginId}/${request.actionId}`);
|
|
847
1024
|
}
|
|
848
|
-
const manifestLookup =
|
|
1025
|
+
const manifestLookup = scope.kind === 'controller'
|
|
1026
|
+
? getControllerPluginManifestForExecution(controllerHome, request.pluginId, adapter)
|
|
1027
|
+
: getAssistantPluginManifestForExecution(controllerHome, scope.repository!, request.pluginId, adapter);
|
|
849
1028
|
const manifest = manifestLookup.manifest;
|
|
850
1029
|
const action = actionForManifest(manifest, request.actionId);
|
|
851
1030
|
if (!manifest.enabled && action.actionId !== 'configure') {
|
|
@@ -860,7 +1039,7 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
860
1039
|
if (request.workId || request.workRepoId) {
|
|
861
1040
|
throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_WORK_ATTRIBUTION_FORBIDDEN: ${request.pluginId}/${request.actionId}`);
|
|
862
1041
|
}
|
|
863
|
-
if (request.confirmAuthorization !== true) {
|
|
1042
|
+
if (action.confirmation === 'authorization' && request.confirmAuthorization !== true) {
|
|
864
1043
|
throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_AUTHORIZATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirm_authorization=true for this invocation`);
|
|
865
1044
|
}
|
|
866
1045
|
const normalizedArgs = validateActionArguments(action, request.args ?? {});
|
|
@@ -868,7 +1047,7 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
868
1047
|
if (requiresAutomatedWriteAuthorization(request.origin)) {
|
|
869
1048
|
await resolveAutomatedWriteAuthorization({
|
|
870
1049
|
controllerHome,
|
|
871
|
-
|
|
1050
|
+
scope,
|
|
872
1051
|
adapter,
|
|
873
1052
|
manifest,
|
|
874
1053
|
action,
|
|
@@ -880,8 +1059,7 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
880
1059
|
}
|
|
881
1060
|
const result = await adapter.executeAction({
|
|
882
1061
|
controllerHome,
|
|
883
|
-
|
|
884
|
-
repoRoot: repository.canonicalRoot,
|
|
1062
|
+
...pluginActionExecutionFields(scope),
|
|
885
1063
|
pluginId: request.pluginId,
|
|
886
1064
|
actionId: request.actionId,
|
|
887
1065
|
requestId: request.requestId,
|
|
@@ -895,12 +1073,29 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
895
1073
|
return { manifest, action, result };
|
|
896
1074
|
}
|
|
897
1075
|
|
|
898
|
-
export
|
|
1076
|
+
export function executeAssistantPluginDirectNonPersistent(
|
|
899
1077
|
controllerHome: string,
|
|
900
1078
|
repository: RepositoryRecord,
|
|
901
1079
|
request: AssistantPluginActionRequest,
|
|
902
|
-
)
|
|
903
|
-
|
|
1080
|
+
) {
|
|
1081
|
+
return executePluginDirectNonPersistentInScope(controllerHome, repositoryPluginActionScope(repository), request);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
export function executeControllerPluginDirectNonPersistent(
|
|
1085
|
+
controllerHome: string,
|
|
1086
|
+
request: AssistantPluginActionRequest,
|
|
1087
|
+
) {
|
|
1088
|
+
return executePluginDirectNonPersistentInScope(controllerHome, controllerPluginActionScope(controllerHome), request);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function executePluginReadDirectInScope(
|
|
1092
|
+
controllerHome: string,
|
|
1093
|
+
scope: PluginActionScopeContext,
|
|
1094
|
+
request: AssistantPluginActionRequest,
|
|
1095
|
+
): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown>; receipt: PluginActionReceipt }> {
|
|
1096
|
+
const manifest = scope.kind === 'controller'
|
|
1097
|
+
? getControllerPluginManifest(controllerHome, request.pluginId)
|
|
1098
|
+
: getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
|
|
904
1099
|
const action = actionForManifest(manifest, request.actionId);
|
|
905
1100
|
if (!manifest.enabled && action.actionId !== 'configure') {
|
|
906
1101
|
throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
|
|
@@ -910,24 +1105,68 @@ export async function executeAssistantPluginReadDirect(
|
|
|
910
1105
|
}
|
|
911
1106
|
const normalizedArgs = validateActionArguments(action, request.args ?? {});
|
|
912
1107
|
enforceConfirmation(action, { ...request, args: normalizedArgs });
|
|
913
|
-
const result = await
|
|
1108
|
+
const result = await executePluginActionInScope(scope, {
|
|
914
1109
|
controllerHome,
|
|
915
|
-
|
|
916
|
-
repoRoot: repository.canonicalRoot,
|
|
1110
|
+
...pluginActionExecutionFields(scope),
|
|
917
1111
|
pluginId: request.pluginId,
|
|
918
1112
|
actionId: request.actionId,
|
|
919
1113
|
requestId: request.requestId,
|
|
920
1114
|
args: normalizedArgs,
|
|
921
1115
|
origin: request.origin,
|
|
922
1116
|
});
|
|
923
|
-
|
|
1117
|
+
const createdAt = new Date().toISOString();
|
|
1118
|
+
const resultDigest = createHash('sha256').update(JSON.stringify(result)).digest('hex');
|
|
1119
|
+
const observationSemanticKey = semanticKey(scope, request.pluginId, request.actionId, normalizedArgs);
|
|
1120
|
+
const observationActor = request.origin?.actor?.trim() ?? '';
|
|
1121
|
+
// Observation evidence is content-addressed so repeated identical reads do not create
|
|
1122
|
+
// an unbounded receipt trail. This remains provenance only, never replay/effect state.
|
|
1123
|
+
const receiptId = `PLG-OBS-${createHash('sha256')
|
|
1124
|
+
.update(`${observationSemanticKey}:${resultDigest}:${observationActor}`)
|
|
1125
|
+
.digest('hex').slice(0, 24)}`;
|
|
1126
|
+
const receipt: PluginActionReceipt = {
|
|
1127
|
+
schemaVersion: 1,
|
|
1128
|
+
receiptId,
|
|
1129
|
+
requestId: request.requestId,
|
|
1130
|
+
scopeKey: scope.scopeKey,
|
|
1131
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1132
|
+
pluginId: request.pluginId,
|
|
1133
|
+
actionId: request.actionId,
|
|
1134
|
+
semanticKey: observationSemanticKey,
|
|
1135
|
+
status: 'succeeded',
|
|
1136
|
+
createdAt,
|
|
1137
|
+
...(request.origin ? { origin: request.origin } : {}),
|
|
1138
|
+
observationOnly: true,
|
|
1139
|
+
resultDigest,
|
|
1140
|
+
};
|
|
1141
|
+
// Direct reads stay on the inline fast path. Persist only a compact observation
|
|
1142
|
+
// receipt, never provider result content and never request replay state.
|
|
1143
|
+
writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
|
|
1144
|
+
return { manifest, action, result, receipt };
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export function executeAssistantPluginReadDirect(
|
|
1148
|
+
controllerHome: string,
|
|
1149
|
+
repository: RepositoryRecord,
|
|
1150
|
+
request: AssistantPluginActionRequest,
|
|
1151
|
+
) {
|
|
1152
|
+
return executePluginReadDirectInScope(controllerHome, repositoryPluginActionScope(repository), request);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
export function executeControllerPluginReadDirect(
|
|
1156
|
+
controllerHome: string,
|
|
1157
|
+
request: AssistantPluginActionRequest,
|
|
1158
|
+
) {
|
|
1159
|
+
return executePluginReadDirectInScope(controllerHome, controllerPluginActionScope(controllerHome), request);
|
|
924
1160
|
}
|
|
925
1161
|
|
|
926
1162
|
export interface PluginActionReceipt {
|
|
927
1163
|
schemaVersion: 1;
|
|
928
1164
|
receiptId: string;
|
|
929
1165
|
requestId: string;
|
|
930
|
-
|
|
1166
|
+
/** Canonical plugin-action receipt scope: `instance` for controller plugins, repository id otherwise. */
|
|
1167
|
+
scopeKey: string;
|
|
1168
|
+
/** Repository provenance only; absent for controller-scoped plugin actions. */
|
|
1169
|
+
repoId?: string;
|
|
931
1170
|
/** Work authority repository when it differs from controller/provider receipt scope. */
|
|
932
1171
|
workRepoId?: string;
|
|
933
1172
|
pluginId: string;
|
|
@@ -942,23 +1181,34 @@ export interface PluginActionReceipt {
|
|
|
942
1181
|
authorization?: PluginActionAuthorizationEvidence;
|
|
943
1182
|
result?: Record<string, unknown>;
|
|
944
1183
|
error?: { code: string; message: string };
|
|
1184
|
+
/** True for a compact direct-read observation; it is evidence, never an effect-completion authority. */
|
|
1185
|
+
observationOnly?: boolean;
|
|
1186
|
+
/** SHA-256 of the direct-read result. Result content is intentionally not persisted for observation-only receipts. */
|
|
1187
|
+
resultDigest?: string;
|
|
945
1188
|
}
|
|
946
1189
|
|
|
947
1190
|
interface PluginActionRequestIndex {
|
|
948
1191
|
requestId: string;
|
|
949
|
-
|
|
1192
|
+
scopeKey: string;
|
|
1193
|
+
repoId?: string;
|
|
950
1194
|
workRepoId?: string;
|
|
951
1195
|
receiptId: string;
|
|
952
1196
|
semanticKey: string;
|
|
953
1197
|
createdAt: string;
|
|
954
1198
|
}
|
|
955
1199
|
|
|
956
|
-
function pluginActionReceiptRoot(controllerHome: string,
|
|
957
|
-
const root =
|
|
1200
|
+
function pluginActionReceiptRoot(controllerHome: string, scopeKey: string): string {
|
|
1201
|
+
const root = scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY
|
|
1202
|
+
? join(controllerSystemRoot(controllerHome), 'plugin-action-receipts')
|
|
1203
|
+
: join(repositoryControllerRoot(controllerHome, scopeKey), 'plugin-action-receipts');
|
|
958
1204
|
mkdirSync(root, { recursive: true });
|
|
959
1205
|
return root;
|
|
960
1206
|
}
|
|
961
1207
|
|
|
1208
|
+
function legacyControllerPluginActionReceiptPath(controllerHome: string, receiptId: string): string {
|
|
1209
|
+
return join(repositoryControllerRoot(controllerHome, CONTROLLER_SCOPE_REPO_ID), 'plugin-action-receipts', `${sanitizeFileComponent(receiptId)}.json`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
962
1212
|
function pluginActionRequestPath(controllerHome: string, requestId: string): string {
|
|
963
1213
|
const hash = createHash('sha256').update(requestId).digest('hex');
|
|
964
1214
|
const root = join(ensureControllerHome(controllerHome), 'indexes', 'plugin-actions', 'requests');
|
|
@@ -966,14 +1216,30 @@ function pluginActionRequestPath(controllerHome: string, requestId: string): str
|
|
|
966
1216
|
return join(root, `${hash}.json`);
|
|
967
1217
|
}
|
|
968
1218
|
|
|
969
|
-
function pluginActionReceiptPath(controllerHome: string,
|
|
970
|
-
return join(pluginActionReceiptRoot(controllerHome,
|
|
1219
|
+
function pluginActionReceiptPath(controllerHome: string, scopeKey: string, receiptId: string): string {
|
|
1220
|
+
return join(pluginActionReceiptRoot(controllerHome, scopeKey), `${sanitizeFileComponent(receiptId)}.json`);
|
|
971
1221
|
}
|
|
972
1222
|
|
|
973
|
-
function
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
1223
|
+
function normalizePluginActionReceipt(receipt: PluginActionReceipt): PluginActionReceipt {
|
|
1224
|
+
const scopeKey = receipt.scopeKey?.trim()
|
|
1225
|
+
|| (receipt.repoId === CONTROLLER_SCOPE_REPO_ID ? CONTROLLER_PLUGIN_SCOPE_KEY : receipt.repoId?.trim())
|
|
1226
|
+
|| '';
|
|
1227
|
+
if (!scopeKey) throw new Error('PLUGIN_RECEIPT_SCOPE_CORRUPT');
|
|
1228
|
+
return {
|
|
1229
|
+
...receipt,
|
|
1230
|
+
scopeKey,
|
|
1231
|
+
...(scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY ? { repoId: undefined } : { repoId: receipt.repoId ?? scopeKey }),
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function readPluginActionReceipt(controllerHome: string, scopeKey: string, receiptId: string): PluginActionReceipt | undefined {
|
|
1236
|
+
const path = pluginActionReceiptPath(controllerHome, scopeKey, receiptId);
|
|
1237
|
+
if (existsSync(path)) return normalizePluginActionReceipt(readJsonFile<PluginActionReceipt>(path));
|
|
1238
|
+
if (scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY || scopeKey === CONTROLLER_SCOPE_REPO_ID) {
|
|
1239
|
+
const legacyPath = legacyControllerPluginActionReceiptPath(controllerHome, receiptId);
|
|
1240
|
+
if (existsSync(legacyPath)) return normalizePluginActionReceipt(readJsonFile<PluginActionReceipt>(legacyPath));
|
|
1241
|
+
}
|
|
1242
|
+
return undefined;
|
|
977
1243
|
}
|
|
978
1244
|
|
|
979
1245
|
export function findPluginActionReceipt(
|
|
@@ -981,6 +1247,12 @@ export function findPluginActionReceipt(
|
|
|
981
1247
|
receiptId: string,
|
|
982
1248
|
): PluginActionReceipt | undefined {
|
|
983
1249
|
const home = ensureControllerHome(controllerHome);
|
|
1250
|
+
// Controller-scoped plugins (for example Local System) live under system/, not
|
|
1251
|
+
// repositories/. They are first-class evidence producers and must be searched too.
|
|
1252
|
+
const controllerReceipt = readPluginActionReceipt(home, CONTROLLER_PLUGIN_SCOPE_KEY, receiptId)
|
|
1253
|
+
// One-way read compatibility for pre-cutover controller receipts only.
|
|
1254
|
+
?? readPluginActionReceipt(home, CONTROLLER_SCOPE_REPO_ID, receiptId);
|
|
1255
|
+
if (controllerReceipt) return controllerReceipt;
|
|
984
1256
|
const repositoriesRoot = join(home, 'repositories');
|
|
985
1257
|
try {
|
|
986
1258
|
for (const repoId of readdirSync(repositoriesRoot)) {
|
|
@@ -999,7 +1271,9 @@ export function readPluginActionReceiptForRequest(controllerHome: string, reques
|
|
|
999
1271
|
if (!existsSync(path)) return undefined;
|
|
1000
1272
|
const index = readJsonFile<PluginActionRequestIndex>(path);
|
|
1001
1273
|
if (index.requestId !== requestId) throw new Error('PLUGIN_RECEIPT_REQUEST_MISMATCH');
|
|
1002
|
-
const
|
|
1274
|
+
const scopeKey = index.scopeKey?.trim() || index.repoId?.trim();
|
|
1275
|
+
if (!scopeKey) throw new Error('PLUGIN_RECEIPT_REQUEST_SCOPE_CORRUPT');
|
|
1276
|
+
const receipt = readPluginActionReceipt(controllerHome, scopeKey, index.receiptId);
|
|
1003
1277
|
if (!receipt || receipt.requestId !== requestId || receipt.semanticKey !== index.semanticKey) throw new Error('PLUGIN_RECEIPT_LOST');
|
|
1004
1278
|
return receipt;
|
|
1005
1279
|
}
|
|
@@ -1011,86 +1285,20 @@ export function compatibilityPluginJobFromReceipt(
|
|
|
1011
1285
|
return compatibilityJobFromReceipt(receipt, checkoutId);
|
|
1012
1286
|
}
|
|
1013
1287
|
|
|
1014
|
-
|
|
1015
|
-
'authorize_target',
|
|
1016
|
-
'revoke_target',
|
|
1017
|
-
'create_directory',
|
|
1018
|
-
'write_text',
|
|
1019
|
-
'delete_file',
|
|
1020
|
-
'delete_empty_directory',
|
|
1021
|
-
'initialize_git',
|
|
1022
|
-
'execute_project_script',
|
|
1023
|
-
'copy_file',
|
|
1024
|
-
'move_file',
|
|
1025
|
-
'rename_file',
|
|
1026
|
-
'open_application',
|
|
1027
|
-
'reveal_in_finder',
|
|
1028
|
-
'open_file',
|
|
1029
|
-
]);
|
|
1030
|
-
|
|
1031
|
-
function localSystemActionRequiresWork(
|
|
1032
|
-
repository: RepositoryRecord,
|
|
1033
|
-
pluginId: string,
|
|
1034
|
-
actionId: string,
|
|
1035
|
-
args: Record<string, unknown>,
|
|
1036
|
-
): boolean {
|
|
1037
|
-
if (repository.repoId !== CONTROLLER_SCOPE_REPO_ID || pluginId !== 'local_system') return false;
|
|
1038
|
-
if (LOCAL_SYSTEM_MUTATION_ACTIONS.has(actionId)) return true;
|
|
1039
|
-
if (actionId !== 'execute_command' || !Array.isArray(args.command)) return false;
|
|
1040
|
-
return classifyRepositoryCommand(args.command.map(String)).risk !== 'readonly';
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
function workRiskForPluginAction(action: AssistantPluginActionDescriptor): WorkRisk {
|
|
1044
|
-
if (action.risk === 'readonly') return 'readonly';
|
|
1045
|
-
if (action.risk === 'workspace_write') return 'medium';
|
|
1046
|
-
if (action.risk === 'remote_write') return 'high';
|
|
1047
|
-
return 'destructive';
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
function localEffectTarget(
|
|
1051
|
-
args: Record<string, unknown>,
|
|
1052
|
-
result: Record<string, unknown>,
|
|
1053
|
-
): LocalEffectCompletionReceipt['target'] {
|
|
1054
|
-
const inner = result.result && typeof result.result === 'object' && !Array.isArray(result.result)
|
|
1055
|
-
? result.result as Record<string, unknown>
|
|
1056
|
-
: result;
|
|
1057
|
-
const target = inner.target && typeof inner.target === 'object' && !Array.isArray(inner.target)
|
|
1058
|
-
? inner.target as Record<string, unknown>
|
|
1059
|
-
: undefined;
|
|
1060
|
-
const id = String(
|
|
1061
|
-
target?.workspaceId
|
|
1062
|
-
?? inner.workspaceId
|
|
1063
|
-
?? args.target_key
|
|
1064
|
-
?? args.destination_target_key
|
|
1065
|
-
?? args.source_target_key
|
|
1066
|
-
?? 'controller-local',
|
|
1067
|
-
);
|
|
1068
|
-
const identityFingerprint = typeof target?.identityFingerprint === 'string'
|
|
1069
|
-
? target.identityFingerprint
|
|
1070
|
-
: typeof inner.identityFingerprint === 'string'
|
|
1071
|
-
? inner.identityFingerprint
|
|
1072
|
-
: undefined;
|
|
1073
|
-
return {
|
|
1074
|
-
kind: id.startsWith('workspace_') || args.target_key || args.destination_target_key || args.source_target_key
|
|
1075
|
-
? 'workspace_target'
|
|
1076
|
-
: 'controller_local',
|
|
1077
|
-
id,
|
|
1078
|
-
...(identityFingerprint ? { identityFingerprint } : {}),
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
function workAttributionRepoId(repository: RepositoryRecord, request: AssistantPluginActionRequest): string {
|
|
1288
|
+
function workAttributionRepoId(scope: PluginActionScopeContext, request: AssistantPluginActionRequest): string {
|
|
1083
1289
|
const explicit = request.workRepoId?.trim();
|
|
1084
1290
|
if (!request.workId?.trim()) {
|
|
1085
1291
|
if (explicit) throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_WITHOUT_WORK: ${explicit}`);
|
|
1086
|
-
return repository
|
|
1292
|
+
return scope.repository?.repoId ?? '';
|
|
1087
1293
|
}
|
|
1088
|
-
|
|
1294
|
+
if (explicit) return explicit;
|
|
1295
|
+
if (scope.repository) return scope.repository.repoId;
|
|
1296
|
+
throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_REQUIRED: controller-scoped action ${request.pluginId}/${request.actionId} must name the Work repository explicitly`);
|
|
1089
1297
|
}
|
|
1090
1298
|
|
|
1091
1299
|
function remoteEffectWorkForPluginAction(
|
|
1092
1300
|
controllerHome: string,
|
|
1093
|
-
|
|
1301
|
+
scope: PluginActionScopeContext,
|
|
1094
1302
|
action: AssistantPluginActionDescriptor,
|
|
1095
1303
|
request: AssistantPluginActionRequest,
|
|
1096
1304
|
receiptId?: string,
|
|
@@ -1100,7 +1308,7 @@ function remoteEffectWorkForPluginAction(
|
|
|
1100
1308
|
if (action.risk !== 'remote_write') {
|
|
1101
1309
|
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_REQUIRES_REMOTE_WRITE: ${action.actionId}`);
|
|
1102
1310
|
}
|
|
1103
|
-
const workRepoId = workAttributionRepoId(
|
|
1311
|
+
const workRepoId = workAttributionRepoId(scope, request);
|
|
1104
1312
|
const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
|
|
1105
1313
|
if (!work) throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_NOT_FOUND: ${workId}`);
|
|
1106
1314
|
if (work.workKind !== 'remote_effect') {
|
|
@@ -1109,49 +1317,49 @@ function remoteEffectWorkForPluginAction(
|
|
|
1109
1317
|
if (work.checks.length > 0) {
|
|
1110
1318
|
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CHECKS_PRESENT: ${workId} declares repository checks and cannot be completed by a plugin receipt alone`);
|
|
1111
1319
|
}
|
|
1112
|
-
if (
|
|
1320
|
+
if (isTerminalSemanticWorkState(work.semanticState)) {
|
|
1113
1321
|
const receiptAlreadyBound = Boolean(receiptId) && (
|
|
1114
1322
|
work.completionReceipt?.receiptId === receiptId
|
|
1115
1323
|
|| work.evidenceRefs.some((evidence) => evidence.evidenceId === receiptId)
|
|
1116
1324
|
);
|
|
1117
|
-
if (work.
|
|
1118
|
-
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_TERMINAL: ${workId} is ${work.
|
|
1325
|
+
if (work.semanticState === 'completed' && receiptAlreadyBound) return work;
|
|
1326
|
+
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_TERMINAL: ${workId} is ${work.semanticState}`);
|
|
1119
1327
|
}
|
|
1120
1328
|
return work;
|
|
1121
1329
|
}
|
|
1122
1330
|
|
|
1123
1331
|
function attributedWorkForPluginAction(
|
|
1124
1332
|
controllerHome: string,
|
|
1125
|
-
|
|
1333
|
+
scope: PluginActionScopeContext,
|
|
1126
1334
|
action: AssistantPluginActionDescriptor,
|
|
1127
1335
|
request: AssistantPluginActionRequest,
|
|
1128
1336
|
): WorkContract | undefined {
|
|
1129
1337
|
const workId = request.workId?.trim();
|
|
1130
1338
|
if (!workId || action.risk === 'remote_write') return undefined;
|
|
1131
|
-
const workRepoId = workAttributionRepoId(
|
|
1339
|
+
const workRepoId = workAttributionRepoId(scope, request);
|
|
1132
1340
|
const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
|
|
1133
1341
|
if (!work) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${workId}`);
|
|
1134
|
-
if (
|
|
1135
|
-
throw new Error(`WORK_PLUGIN_ATTRIBUTION_TERMINAL: ${workId} is ${work.
|
|
1342
|
+
if (isTerminalSemanticWorkState(work.semanticState)) {
|
|
1343
|
+
throw new Error(`WORK_PLUGIN_ATTRIBUTION_TERMINAL: ${workId} is ${work.semanticState}`);
|
|
1136
1344
|
}
|
|
1137
1345
|
return work;
|
|
1138
1346
|
}
|
|
1139
1347
|
|
|
1140
1348
|
function bindLocalEffectReceiptToAttributedWork(
|
|
1141
1349
|
controllerHome: string,
|
|
1142
|
-
|
|
1350
|
+
scope: PluginActionScopeContext,
|
|
1143
1351
|
action: AssistantPluginActionDescriptor,
|
|
1144
1352
|
request: AssistantPluginActionRequest,
|
|
1145
1353
|
receipt: PluginActionReceipt,
|
|
1146
1354
|
work?: WorkContract,
|
|
1147
1355
|
): WorkContract | undefined {
|
|
1148
1356
|
if (!request.workId || receipt.status !== 'succeeded') return work;
|
|
1149
|
-
const workRepoId = workAttributionRepoId(
|
|
1357
|
+
const workRepoId = workAttributionRepoId(scope, request);
|
|
1150
1358
|
const attributed = work ?? getWorkContract({ controllerHome, repoId: workRepoId }, request.workId.trim());
|
|
1151
1359
|
if (!attributed) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${request.workId.trim()}`);
|
|
1152
1360
|
if (attributed.workKind !== 'local_effect' || action.readOnly || action.risk === 'readonly') return attributed;
|
|
1153
1361
|
if (attributed.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) return attributed;
|
|
1154
|
-
if (
|
|
1362
|
+
if (isTerminalSemanticWorkState(attributed.semanticState)) return attributed;
|
|
1155
1363
|
return appendWorkEvidence({ controllerHome, repoId: workRepoId }, attributed.workId, {
|
|
1156
1364
|
evidenceId: receipt.receiptId,
|
|
1157
1365
|
title: 'typed local plugin effect completed',
|
|
@@ -1160,7 +1368,7 @@ function bindLocalEffectReceiptToAttributedWork(
|
|
|
1160
1368
|
});
|
|
1161
1369
|
}
|
|
1162
1370
|
|
|
1163
|
-
export function
|
|
1371
|
+
export function recordRemoteEffectWorkActionReceipt(
|
|
1164
1372
|
controllerHome: string,
|
|
1165
1373
|
repoId: string,
|
|
1166
1374
|
workId: string,
|
|
@@ -1170,14 +1378,12 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
|
|
|
1170
1378
|
if (work.workKind !== 'remote_effect') {
|
|
1171
1379
|
throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_KIND_MISMATCH: ${workId} is ${work.workKind}, expected remote_effect`);
|
|
1172
1380
|
}
|
|
1173
|
-
if (work.
|
|
1174
|
-
if (isTerminalWorkContractStatus(work.status)) {
|
|
1175
|
-
throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_TERMINAL: ${workId} is ${work.status}`);
|
|
1176
|
-
}
|
|
1381
|
+
if (work.completionReceipt?.source === 'remote_effect') return work;
|
|
1177
1382
|
const receipt = work.evidenceRefs
|
|
1178
1383
|
.map((evidence) => {
|
|
1179
1384
|
if (!evidence.evidenceId) return undefined;
|
|
1180
1385
|
return readPluginActionReceipt(controllerHome, repoId, evidence.evidenceId)
|
|
1386
|
+
?? readPluginActionReceipt(controllerHome, CONTROLLER_PLUGIN_SCOPE_KEY, evidence.evidenceId)
|
|
1181
1387
|
?? readPluginActionReceipt(controllerHome, CONTROLLER_SCOPE_REPO_ID, evidence.evidenceId);
|
|
1182
1388
|
})
|
|
1183
1389
|
.find((candidate) => candidate?.workId === workId && candidate.status === 'succeeded');
|
|
@@ -1207,17 +1413,17 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
|
|
|
1207
1413
|
|
|
1208
1414
|
function bindRemoteEffectReceiptToWork(
|
|
1209
1415
|
controllerHome: string,
|
|
1210
|
-
|
|
1416
|
+
scope: PluginActionScopeContext,
|
|
1211
1417
|
action: AssistantPluginActionDescriptor,
|
|
1212
1418
|
request: AssistantPluginActionRequest,
|
|
1213
1419
|
receipt: PluginActionReceipt,
|
|
1214
1420
|
): WorkContract | undefined {
|
|
1215
1421
|
if (!request.workId) return undefined;
|
|
1216
|
-
if (receipt.status !== 'succeeded') return remoteEffectWorkForPluginAction(controllerHome,
|
|
1217
|
-
const work = remoteEffectWorkForPluginAction(controllerHome,
|
|
1422
|
+
if (receipt.status !== 'succeeded') return remoteEffectWorkForPluginAction(controllerHome, scope, action, request);
|
|
1423
|
+
const work = remoteEffectWorkForPluginAction(controllerHome, scope, action, request, receipt.receiptId);
|
|
1218
1424
|
if (!work) return undefined;
|
|
1219
|
-
if (work.
|
|
1220
|
-
const workRepoId = workAttributionRepoId(
|
|
1425
|
+
if (work.completionReceipt?.source === 'remote_effect') return work;
|
|
1426
|
+
const workRepoId = workAttributionRepoId(scope, request);
|
|
1221
1427
|
let updated = work;
|
|
1222
1428
|
if (!work.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) {
|
|
1223
1429
|
updated = appendWorkEvidence({ controllerHome, repoId: workRepoId }, work.workId, {
|
|
@@ -1254,7 +1460,7 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
|
|
|
1254
1460
|
schemaVersion: 1,
|
|
1255
1461
|
revision: 1,
|
|
1256
1462
|
jobId: receipt.receiptId,
|
|
1257
|
-
repoId: receipt.repoId,
|
|
1463
|
+
repoId: receipt.repoId ?? receipt.scopeKey,
|
|
1258
1464
|
checkoutId,
|
|
1259
1465
|
type: 'plugin-action',
|
|
1260
1466
|
status: receipt.effectOutcome === 'outcome_unknown'
|
|
@@ -1286,9 +1492,9 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
|
|
|
1286
1492
|
* Deterministic plugin action path: validate → authorize/confirm → invoke adapter → receipt.
|
|
1287
1493
|
* Does not create ExecutionJobs or call a model.
|
|
1288
1494
|
*/
|
|
1289
|
-
|
|
1495
|
+
async function submitPluginAction(
|
|
1290
1496
|
controllerHome: string,
|
|
1291
|
-
|
|
1497
|
+
scope: PluginActionScopeContext,
|
|
1292
1498
|
request: AssistantPluginActionRequest,
|
|
1293
1499
|
): Promise<{
|
|
1294
1500
|
manifest: AssistantPluginManifest;
|
|
@@ -1302,7 +1508,9 @@ export async function submitAssistantPluginAction(
|
|
|
1302
1508
|
}> {
|
|
1303
1509
|
const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
|
|
1304
1510
|
if (!adapter) throw new Error(`PLUGIN_NOT_FOUND: ${request.pluginId}`);
|
|
1305
|
-
const manifest =
|
|
1511
|
+
const manifest = scope.kind === 'controller'
|
|
1512
|
+
? getControllerPluginManifest(controllerHome, request.pluginId)
|
|
1513
|
+
: getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
|
|
1306
1514
|
const action = actionForManifest(manifest, request.actionId);
|
|
1307
1515
|
if (!manifest.enabled && action.actionId !== 'configure') {
|
|
1308
1516
|
throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
|
|
@@ -1313,36 +1521,38 @@ export async function submitAssistantPluginAction(
|
|
|
1313
1521
|
const normalizedArgs = validateActionArguments(action, request.args ?? {});
|
|
1314
1522
|
enforceConfirmation(action, { ...request, args: normalizedArgs });
|
|
1315
1523
|
|
|
1316
|
-
const key = semanticKey(
|
|
1524
|
+
const key = semanticKey(scope, request.pluginId, request.actionId, normalizedArgs);
|
|
1317
1525
|
const requestPath = pluginActionRequestPath(controllerHome, request.requestId);
|
|
1318
1526
|
if (existsSync(requestPath)) {
|
|
1319
1527
|
const index = readJsonFile<PluginActionRequestIndex>(requestPath);
|
|
1320
1528
|
if (index.semanticKey !== key) {
|
|
1321
1529
|
throw new Error(`REQUEST_ID_CONFLICT: ${request.requestId} already belongs to ${index.semanticKey}`);
|
|
1322
1530
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1531
|
+
const existingScopeKey = index.scopeKey?.trim() || index.repoId?.trim();
|
|
1532
|
+
if (!existingScopeKey) throw new Error(`PLUGIN_RECEIPT_REQUEST_SCOPE_CORRUPT: ${request.requestId}`);
|
|
1533
|
+
if (existingScopeKey !== scope.scopeKey) {
|
|
1534
|
+
throw new Error(`REQUEST_ID_SCOPE_CONFLICT: ${request.requestId} already belongs to ${existingScopeKey}`);
|
|
1325
1535
|
}
|
|
1326
|
-
const existing = readPluginActionReceipt(controllerHome,
|
|
1536
|
+
const existing = readPluginActionReceipt(controllerHome, existingScopeKey, index.receiptId);
|
|
1327
1537
|
if (!existing) {
|
|
1328
1538
|
throw new Error(`PLUGIN_RECEIPT_LOST: ${request.requestId}`);
|
|
1329
1539
|
}
|
|
1330
1540
|
if (request.workId && existing.workId !== request.workId) {
|
|
1331
1541
|
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CONFLICT: ${request.requestId} already belongs to Work ${existing.workId ?? 'none'}`);
|
|
1332
1542
|
}
|
|
1333
|
-
const requestedWorkRepoId = request.workId ? workAttributionRepoId(
|
|
1334
|
-
if (requestedWorkRepoId &&
|
|
1335
|
-
throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_CONFLICT: ${request.requestId} already belongs to Work repository ${index.workRepoId ??
|
|
1543
|
+
const requestedWorkRepoId = request.workId ? workAttributionRepoId(scope, request) : undefined;
|
|
1544
|
+
if (requestedWorkRepoId && index.workRepoId !== requestedWorkRepoId) {
|
|
1545
|
+
throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_CONFLICT: ${request.requestId} already belongs to Work repository ${index.workRepoId ?? 'none'}`);
|
|
1336
1546
|
}
|
|
1337
1547
|
if (request.workId && action.risk === 'remote_write') {
|
|
1338
|
-
bindRemoteEffectReceiptToWork(controllerHome,
|
|
1548
|
+
bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, existing);
|
|
1339
1549
|
} else if (request.workId) {
|
|
1340
|
-
bindLocalEffectReceiptToAttributedWork(controllerHome,
|
|
1550
|
+
bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, existing);
|
|
1341
1551
|
}
|
|
1342
1552
|
return {
|
|
1343
1553
|
manifest,
|
|
1344
1554
|
action,
|
|
1345
|
-
job: compatibilityJobFromReceipt(existing,
|
|
1555
|
+
job: compatibilityJobFromReceipt(existing, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
|
|
1346
1556
|
deduplicated: true,
|
|
1347
1557
|
result: existing.result,
|
|
1348
1558
|
receipt: existing,
|
|
@@ -1360,7 +1570,7 @@ export async function submitAssistantPluginAction(
|
|
|
1360
1570
|
if (requiresAutomatedWriteAuthorization(request.origin)) {
|
|
1361
1571
|
activeAuthorizationGrant = await resolveAutomatedWriteAuthorization({
|
|
1362
1572
|
controllerHome,
|
|
1363
|
-
|
|
1573
|
+
scope,
|
|
1364
1574
|
adapter,
|
|
1365
1575
|
manifest,
|
|
1366
1576
|
action,
|
|
@@ -1380,77 +1590,33 @@ export async function submitAssistantPluginAction(
|
|
|
1380
1590
|
};
|
|
1381
1591
|
}
|
|
1382
1592
|
}
|
|
1383
|
-
const requiresLocalEffectWork = localSystemActionRequiresWork(
|
|
1384
|
-
repository,
|
|
1385
|
-
request.pluginId,
|
|
1386
|
-
request.actionId,
|
|
1387
|
-
normalizedArgs,
|
|
1388
|
-
);
|
|
1389
1593
|
const boundRemoteWork = action.risk === 'remote_write'
|
|
1390
|
-
? remoteEffectWorkForPluginAction(controllerHome,
|
|
1594
|
+
? remoteEffectWorkForPluginAction(controllerHome, scope, action, request)
|
|
1391
1595
|
: undefined;
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
const
|
|
1396
|
-
?
|
|
1397
|
-
requestId: request.requestId,
|
|
1398
|
-
repoId: repository.repoId,
|
|
1399
|
-
semanticKey: `local-effect:${key}`,
|
|
1400
|
-
operation: {
|
|
1401
|
-
name: `plugin:${request.pluginId}/${request.actionId}`,
|
|
1402
|
-
semanticKey: key,
|
|
1403
|
-
argumentHash: createHash('sha256').update(JSON.stringify(normalizedArgs)).digest('hex'),
|
|
1404
|
-
mode: 'mutating',
|
|
1405
|
-
idempotent: action.idempotent,
|
|
1406
|
-
replayable: action.idempotent,
|
|
1407
|
-
resourceClaims: claimsForAssistantPluginAction(action, repository, manifest.pluginId),
|
|
1408
|
-
},
|
|
1409
|
-
objective: `Execute bounded controller-local effect ${request.pluginId}/${request.actionId}`,
|
|
1410
|
-
mode: 'direct_control',
|
|
1411
|
-
requestedBy: 'chatgpt',
|
|
1412
|
-
principalId: request.origin.actor,
|
|
1413
|
-
controllerInstanceId: process.env.FORGE_RUNTIME_INSTANCE_ID?.trim()
|
|
1414
|
-
|| process.env.FORGE_WRITER_INSTANCE_ID?.trim()
|
|
1415
|
-
|| process.env.FORGE_DAEMON_INSTANCE_ID?.trim(),
|
|
1416
|
-
workKind: 'local_effect',
|
|
1417
|
-
risk: workRiskForPluginAction(action),
|
|
1418
|
-
acceptanceCriteria: ['The requested local effect completes within its authorized Target Grant boundary.'],
|
|
1419
|
-
constraints: {
|
|
1420
|
-
requireHandoffOnAmbiguity: true,
|
|
1421
|
-
allowDestructive: action.risk === 'destructive'
|
|
1422
|
-
&& request.confirmAuthorization === true
|
|
1423
|
-
&& request.confirmationText === action.requiredConfirmationText,
|
|
1424
|
-
},
|
|
1425
|
-
}).contract
|
|
1596
|
+
// Capability execution never creates semantic Work implicitly. Existing Work
|
|
1597
|
+
// attribution is opt-in through work_id; otherwise the plugin receipt is the
|
|
1598
|
+
// durable effect record.
|
|
1599
|
+
const attributedWork = action.risk !== 'remote_write'
|
|
1600
|
+
? attributedWorkForPluginAction(controllerHome, scope, action, request)
|
|
1426
1601
|
: undefined;
|
|
1427
|
-
if (
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1602
|
+
if (scope.repository) {
|
|
1603
|
+
appendRuntimeEvent(controllerHome, {
|
|
1604
|
+
repoId: scope.repository.repoId,
|
|
1605
|
+
entityType: 'plugin',
|
|
1606
|
+
entityId: manifest.pluginId,
|
|
1607
|
+
eventType: 'plugin_action_requested',
|
|
1608
|
+
requestId: request.requestId,
|
|
1609
|
+
revision: manifest.revision,
|
|
1610
|
+
data: {
|
|
1611
|
+
actionId: action.actionId,
|
|
1612
|
+
receiptId,
|
|
1613
|
+
risk: action.risk,
|
|
1614
|
+
confirmation: action.confirmation,
|
|
1615
|
+
capabilityId,
|
|
1616
|
+
authorizationReuseSupported: action.confirmation === 'authorization' && Boolean(adapter.resolveAuthorizationContext),
|
|
1435
1617
|
},
|
|
1436
|
-
);
|
|
1618
|
+
});
|
|
1437
1619
|
}
|
|
1438
|
-
appendRuntimeEvent(controllerHome, {
|
|
1439
|
-
repoId: repository.repoId,
|
|
1440
|
-
entityType: 'plugin',
|
|
1441
|
-
entityId: manifest.pluginId,
|
|
1442
|
-
eventType: 'plugin_action_requested',
|
|
1443
|
-
requestId: request.requestId,
|
|
1444
|
-
revision: manifest.revision,
|
|
1445
|
-
data: {
|
|
1446
|
-
actionId: action.actionId,
|
|
1447
|
-
receiptId,
|
|
1448
|
-
risk: action.risk,
|
|
1449
|
-
confirmation: action.confirmation,
|
|
1450
|
-
capabilityId,
|
|
1451
|
-
authorizationReuseSupported: action.confirmation === 'authorization' && Boolean(adapter.resolveAuthorizationContext),
|
|
1452
|
-
},
|
|
1453
|
-
});
|
|
1454
1620
|
|
|
1455
1621
|
try {
|
|
1456
1622
|
if (action.confirmation === 'authorization'
|
|
@@ -1458,8 +1624,7 @@ export async function submitAssistantPluginAction(
|
|
|
1458
1624
|
&& originMayEstablishCapabilityAuthorization(request.origin)) {
|
|
1459
1625
|
authorizationContext = await adapter.resolveAuthorizationContext({
|
|
1460
1626
|
controllerHome,
|
|
1461
|
-
|
|
1462
|
-
repoRoot: repository.canonicalRoot,
|
|
1627
|
+
...pluginActionExecutionFields(scope),
|
|
1463
1628
|
pluginId: request.pluginId,
|
|
1464
1629
|
actionId: request.actionId,
|
|
1465
1630
|
requestId: request.requestId,
|
|
@@ -1479,7 +1644,7 @@ export async function submitAssistantPluginAction(
|
|
|
1479
1644
|
try {
|
|
1480
1645
|
activeAuthorizationGrant = findActivePluginCapabilityAuthorization(controllerHome, {
|
|
1481
1646
|
ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
|
|
1482
|
-
repoId:
|
|
1647
|
+
repoId: scope.grantRepoId,
|
|
1483
1648
|
pluginId: request.pluginId,
|
|
1484
1649
|
capabilityId,
|
|
1485
1650
|
target: authorizationContext.target,
|
|
@@ -1506,13 +1671,12 @@ export async function submitAssistantPluginAction(
|
|
|
1506
1671
|
|
|
1507
1672
|
const result = await withAssistantPluginResourceLeases(
|
|
1508
1673
|
controllerHome,
|
|
1509
|
-
|
|
1674
|
+
scope,
|
|
1510
1675
|
action,
|
|
1511
1676
|
request,
|
|
1512
|
-
() =>
|
|
1677
|
+
() => executePluginActionInScope(scope, {
|
|
1513
1678
|
controllerHome,
|
|
1514
|
-
|
|
1515
|
-
repoRoot: repository.canonicalRoot,
|
|
1679
|
+
...pluginActionExecutionFields(scope),
|
|
1516
1680
|
pluginId: request.pluginId,
|
|
1517
1681
|
actionId: request.actionId,
|
|
1518
1682
|
requestId: request.requestId,
|
|
@@ -1527,6 +1691,31 @@ export async function submitAssistantPluginAction(
|
|
|
1527
1691
|
: undefined,
|
|
1528
1692
|
}),
|
|
1529
1693
|
);
|
|
1694
|
+
// create_session materializes the exact provider/session identity during
|
|
1695
|
+
// execution, so its authorization target cannot always be resolved before
|
|
1696
|
+
// the action runs. Resolve that postcondition once after a successful
|
|
1697
|
+
// interactive bootstrap, then persist the same exact-target grant path as
|
|
1698
|
+
// every other authorization-class Browser action. Scheduled execution
|
|
1699
|
+
// still remains grant-only because this branch is interactive-origin only.
|
|
1700
|
+
if (action.actionId === 'create_session'
|
|
1701
|
+
&& action.confirmation === 'authorization'
|
|
1702
|
+
&& !authorizationContext
|
|
1703
|
+
&& originMayEstablishCapabilityAuthorization(request.origin)
|
|
1704
|
+
&& adapter.resolveAuthorizationContext) {
|
|
1705
|
+
authorizationContext = await adapter.resolveAuthorizationContext({
|
|
1706
|
+
controllerHome,
|
|
1707
|
+
...pluginActionExecutionFields(scope),
|
|
1708
|
+
pluginId: request.pluginId,
|
|
1709
|
+
actionId: request.actionId,
|
|
1710
|
+
requestId: request.requestId,
|
|
1711
|
+
args: normalizedArgs,
|
|
1712
|
+
origin: request.origin,
|
|
1713
|
+
jobId: receiptId,
|
|
1714
|
+
timeoutMs: request.timeoutMs,
|
|
1715
|
+
signal: request.signal,
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1530
1719
|
if (action.confirmation === 'authorization'
|
|
1531
1720
|
&& authorizationContext
|
|
1532
1721
|
&& !activeAuthorizationGrant
|
|
@@ -1534,7 +1723,7 @@ export async function submitAssistantPluginAction(
|
|
|
1534
1723
|
try {
|
|
1535
1724
|
const grant = recordPluginCapabilityAuthorization(controllerHome, {
|
|
1536
1725
|
ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
|
|
1537
|
-
repoId:
|
|
1726
|
+
repoId: scope.grantRepoId,
|
|
1538
1727
|
pluginId: request.pluginId,
|
|
1539
1728
|
capabilityId,
|
|
1540
1729
|
target: authorizationContext.target,
|
|
@@ -1568,42 +1757,8 @@ export async function submitAssistantPluginAction(
|
|
|
1568
1757
|
work: {
|
|
1569
1758
|
workId: boundRemoteWork.workId,
|
|
1570
1759
|
workKind: 'remote_effect',
|
|
1760
|
+
semanticState: 'open' as const,
|
|
1571
1761
|
...(terminalRemoteEffect ? { completionOutcome: 'completed_remote' as const } : {}),
|
|
1572
|
-
status: terminalRemoteEffect ? 'completed' : 'running',
|
|
1573
|
-
},
|
|
1574
|
-
};
|
|
1575
|
-
}
|
|
1576
|
-
if (acceptedWork) {
|
|
1577
|
-
const recordedAt = new Date().toISOString();
|
|
1578
|
-
const target = localEffectTarget(normalizedArgs, result);
|
|
1579
|
-
appendWorkEvidence({ controllerHome, repoId: repository.repoId }, acceptedWork.workId, {
|
|
1580
|
-
title: 'controller-local effect completed',
|
|
1581
|
-
summary: `${request.pluginId}/${request.actionId} completed for ${target.id}.`,
|
|
1582
|
-
detailLevel: 'summary',
|
|
1583
|
-
});
|
|
1584
|
-
recordWorkCompletionReceipt(
|
|
1585
|
-
{ controllerHome, repoId: repository.repoId },
|
|
1586
|
-
acceptedWork.workId,
|
|
1587
|
-
{
|
|
1588
|
-
schemaVersion: 1,
|
|
1589
|
-
receiptId: `LFX-${Date.now()}-${createHash('sha256').update(`${request.requestId}:${acceptedWork.workId}`).digest('hex').slice(0, 8)}`,
|
|
1590
|
-
source: 'local_effect',
|
|
1591
|
-
workId: acceptedWork.workId,
|
|
1592
|
-
operation: `${request.pluginId}/${request.actionId}`,
|
|
1593
|
-
target,
|
|
1594
|
-
changed: true,
|
|
1595
|
-
recordedAt,
|
|
1596
|
-
},
|
|
1597
|
-
'completed_local',
|
|
1598
|
-
'local_effect',
|
|
1599
|
-
);
|
|
1600
|
-
resultWithLineage = {
|
|
1601
|
-
...result,
|
|
1602
|
-
work: {
|
|
1603
|
-
workId: acceptedWork.workId,
|
|
1604
|
-
workKind: 'local_effect',
|
|
1605
|
-
completionOutcome: 'completed_local',
|
|
1606
|
-
status: 'completed',
|
|
1607
1762
|
},
|
|
1608
1763
|
};
|
|
1609
1764
|
}
|
|
@@ -1611,48 +1766,50 @@ export async function submitAssistantPluginAction(
|
|
|
1611
1766
|
schemaVersion: 1,
|
|
1612
1767
|
receiptId,
|
|
1613
1768
|
requestId: request.requestId,
|
|
1614
|
-
|
|
1615
|
-
...(
|
|
1769
|
+
scopeKey: scope.scopeKey,
|
|
1770
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1771
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
|
|
1616
1772
|
pluginId: request.pluginId,
|
|
1617
1773
|
actionId: request.actionId,
|
|
1618
1774
|
semanticKey: key,
|
|
1619
1775
|
status: 'succeeded',
|
|
1620
1776
|
createdAt,
|
|
1621
|
-
...(
|
|
1622
|
-
? { workId:
|
|
1623
|
-
:
|
|
1624
|
-
? { workId:
|
|
1625
|
-
:
|
|
1626
|
-
? { workId: attributedWork.workId }
|
|
1627
|
-
: {}),
|
|
1777
|
+
...(boundRemoteWork
|
|
1778
|
+
? { workId: boundRemoteWork.workId }
|
|
1779
|
+
: attributedWork
|
|
1780
|
+
? { workId: attributedWork.workId }
|
|
1781
|
+
: {}),
|
|
1628
1782
|
origin: request.origin,
|
|
1629
1783
|
authorization,
|
|
1630
1784
|
result: resultWithLineage,
|
|
1631
1785
|
};
|
|
1632
|
-
writeJsonAtomic(pluginActionReceiptPath(controllerHome,
|
|
1786
|
+
writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
|
|
1633
1787
|
writeJsonAtomic(requestPath, {
|
|
1634
1788
|
requestId: request.requestId,
|
|
1635
|
-
|
|
1636
|
-
...(
|
|
1789
|
+
scopeKey: scope.scopeKey,
|
|
1790
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1791
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
|
|
1637
1792
|
receiptId,
|
|
1638
1793
|
semanticKey: key,
|
|
1639
1794
|
createdAt,
|
|
1640
1795
|
} satisfies PluginActionRequestIndex);
|
|
1641
1796
|
if (boundRemoteWork) {
|
|
1642
|
-
bindRemoteEffectReceiptToWork(controllerHome,
|
|
1797
|
+
bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, receipt);
|
|
1643
1798
|
} else if (attributedWork) {
|
|
1644
|
-
bindLocalEffectReceiptToAttributedWork(controllerHome,
|
|
1799
|
+
bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, receipt, attributedWork);
|
|
1645
1800
|
}
|
|
1646
|
-
const nextManifest =
|
|
1801
|
+
const nextManifest = scope.kind === 'controller'
|
|
1802
|
+
? getControllerPluginManifest(controllerHome, request.pluginId)
|
|
1803
|
+
: getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
|
|
1647
1804
|
return {
|
|
1648
1805
|
manifest: nextManifest,
|
|
1649
1806
|
action,
|
|
1650
|
-
job: compatibilityJobFromReceipt(receipt,
|
|
1807
|
+
job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
|
|
1651
1808
|
deduplicated: false,
|
|
1652
1809
|
result: resultWithLineage,
|
|
1653
1810
|
receipt,
|
|
1654
1811
|
authorization,
|
|
1655
|
-
workId:
|
|
1812
|
+
workId: boundRemoteWork?.workId ?? attributedWork?.workId,
|
|
1656
1813
|
};
|
|
1657
1814
|
} catch (error) {
|
|
1658
1815
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1667,65 +1824,41 @@ export async function submitAssistantPluginAction(
|
|
|
1667
1824
|
? { outcome: 'outcome_unknown', error: { code, message } }
|
|
1668
1825
|
: undefined;
|
|
1669
1826
|
if (boundRemoteWork) {
|
|
1670
|
-
appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(
|
|
1827
|
+
appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(scope, request) }, boundRemoteWork.workId, {
|
|
1671
1828
|
title: outcomeUnknown ? 'typed remote plugin effect outcome unknown' : 'typed remote plugin effect failed',
|
|
1672
1829
|
summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
|
|
1673
1830
|
detailLevel: 'summary',
|
|
1674
1831
|
});
|
|
1675
1832
|
}
|
|
1676
|
-
if (acceptedWork) {
|
|
1677
|
-
if (outcomeUnknown) {
|
|
1678
|
-
appendWorkEvidence({ controllerHome, repoId: repository.repoId }, acceptedWork.workId, {
|
|
1679
|
-
title: 'controller-local effect outcome unknown',
|
|
1680
|
-
summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
|
|
1681
|
-
detailLevel: 'summary',
|
|
1682
|
-
});
|
|
1683
|
-
} else {
|
|
1684
|
-
const current = getWorkContract({ controllerHome, repoId: repository.repoId }, acceptedWork.workId);
|
|
1685
|
-
failWorkContract(
|
|
1686
|
-
{ controllerHome, repoId: repository.repoId },
|
|
1687
|
-
acceptedWork.workId,
|
|
1688
|
-
{
|
|
1689
|
-
phase: current?.phase ?? 'implementation',
|
|
1690
|
-
summary: `Controller-local plugin action failed: ${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
|
|
1691
|
-
evidenceRefs: [{
|
|
1692
|
-
title: 'controller-local effect failed',
|
|
1693
|
-
summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
|
|
1694
|
-
detailLevel: 'summary',
|
|
1695
|
-
}, ...(current?.evidenceRefs ?? [])],
|
|
1696
|
-
},
|
|
1697
|
-
);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
1833
|
const receipt: PluginActionReceipt = {
|
|
1701
1834
|
schemaVersion: 1,
|
|
1702
1835
|
receiptId,
|
|
1703
1836
|
requestId: request.requestId,
|
|
1704
|
-
|
|
1705
|
-
...(
|
|
1837
|
+
scopeKey: scope.scopeKey,
|
|
1838
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1839
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
|
|
1706
1840
|
pluginId: request.pluginId,
|
|
1707
1841
|
actionId: request.actionId,
|
|
1708
1842
|
semanticKey: key,
|
|
1709
1843
|
status: 'failed',
|
|
1710
1844
|
effectOutcome,
|
|
1711
1845
|
createdAt,
|
|
1712
|
-
...(
|
|
1713
|
-
? { workId:
|
|
1714
|
-
:
|
|
1715
|
-
? { workId:
|
|
1716
|
-
:
|
|
1717
|
-
? { workId: attributedWork.workId }
|
|
1718
|
-
: {}),
|
|
1846
|
+
...(boundRemoteWork
|
|
1847
|
+
? { workId: boundRemoteWork.workId }
|
|
1848
|
+
: attributedWork
|
|
1849
|
+
? { workId: attributedWork.workId }
|
|
1850
|
+
: {}),
|
|
1719
1851
|
origin: request.origin,
|
|
1720
1852
|
authorization,
|
|
1721
1853
|
...(outcomeResult ? { result: outcomeResult } : {}),
|
|
1722
1854
|
error: { code, message },
|
|
1723
1855
|
};
|
|
1724
|
-
writeJsonAtomic(pluginActionReceiptPath(controllerHome,
|
|
1856
|
+
writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
|
|
1725
1857
|
writeJsonAtomic(requestPath, {
|
|
1726
1858
|
requestId: request.requestId,
|
|
1727
|
-
|
|
1728
|
-
...(
|
|
1859
|
+
scopeKey: scope.scopeKey,
|
|
1860
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1861
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
|
|
1729
1862
|
receiptId,
|
|
1730
1863
|
semanticKey: key,
|
|
1731
1864
|
createdAt,
|
|
@@ -1734,40 +1867,42 @@ export async function submitAssistantPluginAction(
|
|
|
1734
1867
|
return {
|
|
1735
1868
|
manifest,
|
|
1736
1869
|
action,
|
|
1737
|
-
job: compatibilityJobFromReceipt(receipt,
|
|
1870
|
+
job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
|
|
1738
1871
|
deduplicated: false,
|
|
1739
1872
|
result: outcomeResult,
|
|
1740
1873
|
receipt,
|
|
1741
1874
|
authorization,
|
|
1742
|
-
workId:
|
|
1875
|
+
workId: boundRemoteWork?.workId ?? attributedWork?.workId,
|
|
1743
1876
|
};
|
|
1744
1877
|
}
|
|
1745
1878
|
throw error;
|
|
1746
1879
|
}
|
|
1747
1880
|
}
|
|
1748
1881
|
|
|
1749
|
-
export
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
repoId: repository.repoId,
|
|
1756
|
-
repoRoot: repository.canonicalRoot,
|
|
1757
|
-
});
|
|
1882
|
+
export function submitAssistantPluginAction(
|
|
1883
|
+
controllerHome: string,
|
|
1884
|
+
repository: RepositoryRecord,
|
|
1885
|
+
request: AssistantPluginActionRequest,
|
|
1886
|
+
) {
|
|
1887
|
+
return submitPluginAction(controllerHome, repositoryPluginActionScope(repository), request);
|
|
1758
1888
|
}
|
|
1759
1889
|
|
|
1760
|
-
export
|
|
1890
|
+
export function submitControllerPluginAction(
|
|
1891
|
+
controllerHome: string,
|
|
1892
|
+
request: AssistantPluginActionRequest,
|
|
1893
|
+
) {
|
|
1894
|
+
return submitPluginAction(controllerHome, controllerPluginActionScope(controllerHome), request);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
async function executePluginActionInScope(
|
|
1898
|
+
scope: PluginActionScopeContext,
|
|
1761
1899
|
input: AssistantPluginActionExecutionInput,
|
|
1762
1900
|
): Promise<Record<string, unknown>> {
|
|
1763
1901
|
const adapter = resolvePluginAdapter(input.controllerHome, input.pluginId);
|
|
1764
1902
|
if (!adapter) throw new Error(`PLUGIN_NOT_FOUND: ${input.pluginId}`);
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
activeCheckoutId: 'active',
|
|
1769
|
-
} as RepositoryRecord;
|
|
1770
|
-
const manifestLookup = getAssistantPluginManifestForExecution(input.controllerHome, repository, input.pluginId, adapter);
|
|
1903
|
+
const manifestLookup = scope.kind === 'controller'
|
|
1904
|
+
? getControllerPluginManifestForExecution(input.controllerHome, input.pluginId, adapter)
|
|
1905
|
+
: getAssistantPluginManifestForExecution(input.controllerHome, scope.repository!, input.pluginId, adapter);
|
|
1771
1906
|
const manifest = manifestLookup.manifest;
|
|
1772
1907
|
const action = actionForManifest(manifest, input.actionId);
|
|
1773
1908
|
if (action.executionMode === 'direct_non_persistent') {
|
|
@@ -1777,7 +1912,7 @@ export async function executeAssistantPluginAction(
|
|
|
1777
1912
|
if (requiresAutomatedWriteAuthorization(input.origin)) {
|
|
1778
1913
|
await resolveAutomatedWriteAuthorization({
|
|
1779
1914
|
controllerHome: input.controllerHome,
|
|
1780
|
-
|
|
1915
|
+
scope,
|
|
1781
1916
|
adapter,
|
|
1782
1917
|
manifest,
|
|
1783
1918
|
action,
|
|
@@ -1791,38 +1926,53 @@ export async function executeAssistantPluginAction(
|
|
|
1791
1926
|
try {
|
|
1792
1927
|
const result = await adapter.executeAction({
|
|
1793
1928
|
...input,
|
|
1929
|
+
...pluginActionExecutionFields(scope),
|
|
1794
1930
|
args: normalizedArgs,
|
|
1795
1931
|
providerIdentityPrevalidated: manifestLookup.providerIdentityPrevalidated,
|
|
1796
1932
|
});
|
|
1797
1933
|
for (const affectedPluginId of adapter.affectedPluginIdsAfterAction?.(input.actionId, result) ?? []) {
|
|
1798
1934
|
if (!affectedPluginId || affectedPluginId === input.pluginId) continue;
|
|
1799
|
-
invalidateAssistantPluginManifestCache(input.controllerHome, repository.repoId, affectedPluginId);
|
|
1800
1935
|
const affectedAdapter = resolvePluginAdapter(input.controllerHome, affectedPluginId);
|
|
1801
|
-
if (
|
|
1802
|
-
|
|
1936
|
+
if (scope.kind === 'controller') {
|
|
1937
|
+
invalidateAssistantPluginManifestCache(input.controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, affectedPluginId);
|
|
1938
|
+
if (affectedAdapter && adapterMatchesController(affectedAdapter)) {
|
|
1939
|
+
syncControllerPluginManifest(input.controllerHome, affectedPluginId);
|
|
1940
|
+
} else {
|
|
1941
|
+
removeControllerPluginManifestProjection(input.controllerHome, affectedPluginId);
|
|
1942
|
+
}
|
|
1803
1943
|
} else {
|
|
1804
|
-
|
|
1944
|
+
const repository = scope.repository!;
|
|
1945
|
+
invalidateAssistantPluginManifestCache(input.controllerHome, repository.repoId, affectedPluginId);
|
|
1946
|
+
if (affectedAdapter && adapterMatchesRepository(affectedAdapter, repository)) {
|
|
1947
|
+
syncAssistantPluginManifest(input.controllerHome, repository, affectedPluginId);
|
|
1948
|
+
} else {
|
|
1949
|
+
rmSync(manifestPath(input.controllerHome, repository.repoId, affectedPluginId), { force: true });
|
|
1950
|
+
}
|
|
1805
1951
|
}
|
|
1806
1952
|
}
|
|
1807
1953
|
const refreshManifest = adapter.shouldRefreshManifestAfterAction?.(input.actionId) ?? true;
|
|
1808
1954
|
const nextManifest = refreshManifest
|
|
1809
|
-
?
|
|
1955
|
+
? scope.kind === 'controller'
|
|
1956
|
+
? syncControllerPluginManifest(input.controllerHome, input.pluginId).manifest
|
|
1957
|
+
: syncAssistantPluginManifest(input.controllerHome, scope.repository!, input.pluginId).manifest
|
|
1810
1958
|
: manifest;
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1959
|
+
if (scope.repository) {
|
|
1960
|
+
appendRuntimeEvent(input.controllerHome, {
|
|
1961
|
+
repoId: scope.repository.repoId,
|
|
1962
|
+
entityType: 'plugin',
|
|
1963
|
+
entityId: input.pluginId,
|
|
1964
|
+
eventType: 'plugin_action_succeeded',
|
|
1965
|
+
requestId: input.requestId,
|
|
1966
|
+
revision: nextManifest.revision,
|
|
1967
|
+
data: {
|
|
1968
|
+
actionId: input.actionId,
|
|
1969
|
+
jobId: input.jobId,
|
|
1970
|
+
resultKeys: Object.keys(result).slice(0, 20),
|
|
1971
|
+
lifecycleState: nextManifest.lifecycle.state,
|
|
1972
|
+
healthState: nextManifest.health.state,
|
|
1973
|
+
},
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1826
1976
|
return {
|
|
1827
1977
|
schemaVersion: 1,
|
|
1828
1978
|
plugin: {
|
|
@@ -1850,25 +2000,49 @@ export async function executeAssistantPluginAction(
|
|
|
1850
2000
|
actionId: input.actionId,
|
|
1851
2001
|
},
|
|
1852
2002
|
});
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2003
|
+
const nextManifest = scope.kind === 'controller'
|
|
2004
|
+
? syncControllerPluginManifest(input.controllerHome, input.pluginId).manifest
|
|
2005
|
+
: syncAssistantPluginManifest(input.controllerHome, scope.repository!, input.pluginId).manifest;
|
|
2006
|
+
if (scope.repository) {
|
|
2007
|
+
appendRuntimeEvent(input.controllerHome, {
|
|
2008
|
+
repoId: scope.repository.repoId,
|
|
2009
|
+
entityType: 'plugin',
|
|
2010
|
+
entityId: input.pluginId,
|
|
2011
|
+
eventType: 'plugin_action_failed',
|
|
2012
|
+
requestId: input.requestId,
|
|
2013
|
+
revision: nextManifest.revision,
|
|
2014
|
+
data: {
|
|
2015
|
+
actionId: input.actionId,
|
|
2016
|
+
jobId: input.jobId,
|
|
2017
|
+
code: pluginError.code,
|
|
2018
|
+
retryable: pluginError.retryable,
|
|
2019
|
+
},
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
1869
2022
|
throw new AssistantPluginError(pluginError.code, pluginError.message.replace(/^[^:]+:\s*/, ''), {
|
|
1870
2023
|
retryable: pluginError.retryable,
|
|
1871
2024
|
details: pluginError.details,
|
|
1872
2025
|
});
|
|
1873
2026
|
}
|
|
1874
2027
|
}
|
|
2028
|
+
|
|
2029
|
+
export function executeControllerScopedPluginAction(
|
|
2030
|
+
input: Omit<AssistantPluginActionExecutionInput, 'repoId' | 'repoRoot'>,
|
|
2031
|
+
): Promise<Record<string, unknown>> {
|
|
2032
|
+
const scope = controllerPluginActionScope(input.controllerHome);
|
|
2033
|
+
return executePluginActionInScope(scope, {
|
|
2034
|
+
...input,
|
|
2035
|
+
...pluginActionExecutionFields(scope),
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
export function executeAssistantPluginAction(
|
|
2040
|
+
input: AssistantPluginActionExecutionInput,
|
|
2041
|
+
): Promise<Record<string, unknown>> {
|
|
2042
|
+
const repository = {
|
|
2043
|
+
repoId: input.repoId,
|
|
2044
|
+
canonicalRoot: input.repoRoot,
|
|
2045
|
+
activeCheckoutId: 'active',
|
|
2046
|
+
} as RepositoryRecord;
|
|
2047
|
+
return executePluginActionInScope(repositoryPluginActionScope(repository), input);
|
|
2048
|
+
}
|