@moretea-labs/forge 1.8.0 → 1.8.2
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 +73 -7
- package/CHANGELOG.md +34 -0
- package/CLAUDE.md +17 -2
- package/CONTRIBUTING.md +3 -2
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-host.ts +7 -1
- package/adapters/chatgpt/browser-delivery-runtime.ts +531 -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 +229 -2
- package/adapters/chatgpt/work-conversation-binding-store.ts +99 -54
- package/adapters/computer/chatgpt-extension-target.ts +708 -0
- package/adapters/computer/chrome-extension/background.js +541 -0
- package/adapters/computer/chrome-extension/content.js +402 -0
- package/adapters/computer/chrome-extension/core.js +24 -0
- package/adapters/computer/chrome-extension/manifest.json +32 -0
- package/adapters/computer/desktop-operator-discovery.ts +11 -31
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +178 -24
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -7
- package/adapters/mcp/instructions.ts +6 -5
- package/adapters/mcp/multi-repository.ts +10 -47
- package/adapters/mcp/oauth.ts +73 -0
- package/adapters/mcp/paths.ts +4 -1
- 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 +490 -113
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +83 -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 +14 -54
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +132 -47
- package/adapters/mcp/runtime-gateway/process-tools.ts +173 -23
- 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 +19 -507
- package/adapters/mcp/runtime-gateway/routing-policy.ts +506 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +15 -66
- package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +123 -178
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +11 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +37 -1
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +416 -270
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +140 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +262 -3123
- 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 +229 -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 +320 -0
- package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +180 -0
- package/adapters/mcp/server.ts +91 -26
- package/adapters/mcp/setup.ts +5 -13
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +703 -3092
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +5 -10
- package/adapters/mcp/tool-mapping/repository-tools.ts +170 -213
- package/adapters/mcp/toolset-names.ts +11 -37
- package/adapters/mcp/toolset.ts +19 -92
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +377 -0
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +200 -683
- 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/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/plugin-registry.v1.json +6 -4
- package/assets/reference-configs/agentic-development-flow.md +26 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/global-working-rules.md +19 -0
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +205 -51
- 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/20260904-ios-device-migration-baseline.md +42 -0
- 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 +234 -0
- package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +74 -0
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +342 -0
- package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +6 -5
- package/docs/architecture/domains/workflow-engine.md +6 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +3 -3
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +16 -4
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/controller-performance-and-502.md +17 -0
- package/docs/operations/controller-reliability-runbook.md +2 -2
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -52
- package/docs/operations/releasing.md +2 -0
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +15 -7
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +25 -13
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +25 -25
- 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 +348 -97
- 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 +87 -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/eligibility.ts +4 -1
- 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 +166 -143
- package/packages/kernel/work/api/index.ts +3 -2
- package/packages/kernel/work/application/work-service.ts +18 -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 +34 -65
- package/packages/kernel/work/domain/types.ts +57 -51
- package/packages/kernel/work/infrastructure/work-contract-store.ts +847 -729
- package/packages/kernel/work/ports/work-contract-store.ts +5 -2
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- 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 +153 -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/assemble-template.ts +11 -72
- package/scripts/canary-controller-semantic-wait.ts +12 -24
- package/scripts/check-mcp-compatibility.ts +94 -201
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +921 -239
- package/scripts/forge-local-recovery-helper.mjs +93 -15
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +6 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +1 -3
- package/scripts/run-bounded-command.mjs +5 -0
- package/scripts/run-governed-gate.ts +2 -1
- package/scripts/run-mobile-monitor-bridge.ts +0 -1
- package/scripts/smoke-control-plane-failure-domain.ts +94 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +12 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- 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 +95 -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/sync-generated-authority.mjs +0 -1
- package/scripts/verify-forge-runtime.sh +2 -1
- package/src/cli/agent-jobs/executor-health.ts +12 -35
- package/src/cli/agent-jobs/integration.ts +20 -51
- package/src/cli/agent-jobs/job-manager.ts +7 -3
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- 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/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +137 -135
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/runtime.ts +3 -3
- package/src/cli/commands/setup-execution.ts +41 -0
- 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 +5 -44
- package/src/cli/controller/completion-target.ts +3 -72
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +83 -102
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +107 -84
- 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 +25 -52
- 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 +42 -10
- package/src/cli/editing/edit-session.ts +18 -12
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +19 -14
- package/src/cli/local-bridge/console-view-models.ts +4 -13
- package/src/cli/local-bridge/facade-api.ts +64 -193
- 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-classifier.ts +0 -9
- package/src/cli/repositories/command-executor.ts +29 -23
- package/src/cli/repositories/command-scope.ts +37 -18
- 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/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +79 -12
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +101 -5
- package/src/cli/repository/inspector.ts +185 -343
- package/src/cli/repository/session-cache.ts +92 -6
- package/src/effects/process-runner.ts +17 -1
- 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/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +16 -0
- package/src/runtime/context/semantic-navigation.ts +7 -35
- 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 +215 -12
- 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 +616 -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 +197 -110
- package/src/runtime/control-plane/execution/work-handle-store.ts +72 -44
- 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 +60 -34
- package/src/runtime/control-plane/execution/work-preparation-service.ts +231 -267
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- 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 +201 -199
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +33 -85
- package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +6 -2
- package/src/runtime/control-plane/facade/capability-registry.ts +364 -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 +9 -9
- package/src/runtime/control-plane/generated-cache-retention.ts +206 -59
- 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 +256 -117
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -12
- 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/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +29 -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 +295 -107
- 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/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- 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 +82 -30
- 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 +55 -18
- package/src/runtime/execution/process-runtime/store.ts +30 -2
- package/src/runtime/execution/process-runtime/types.ts +6 -1
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +3 -8
- package/src/runtime/maintenance/cleanup.ts +41 -5
- 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 +97 -29
- package/src/runtime/plugins/app-store-connect-adapter.ts +290 -18
- package/src/runtime/plugins/browser-adapter.ts +1124 -333
- package/src/runtime/plugins/browser-automation-service.ts +23 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +431 -137
- package/src/runtime/plugins/browser-manifest-surface.ts +47 -12
- package/src/runtime/plugins/browser-native-session-service.ts +63 -13
- package/src/runtime/plugins/browser-registration.ts +18 -0
- package/src/runtime/plugins/browser-runtime.ts +40 -36
- 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 +228 -0
- package/src/runtime/plugins/computer-registration.ts +298 -20
- 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/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +189 -3
- package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
- package/src/runtime/plugins/google-shared.ts +24 -12
- package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +46 -41
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/lightweight-action.ts +208 -2
- package/src/runtime/plugins/local-system-adapter.ts +12 -100
- 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 +682 -435
- package/src/runtime/plugins/types.ts +24 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/projections/invalidation.ts +7 -3
- package/src/runtime/projections/materialized-view.ts +80 -36
- package/src/runtime/recovery/actions.ts +4 -4
- package/src/runtime/recovery/capability-action-application.ts +46 -45
- package/src/runtime/recovery/lifecycle-inventory.ts +30 -40
- package/src/runtime/recovery/maintenance-executor.ts +374 -123
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/recovery/store.ts +29 -11
- package/src/runtime/release/release-coordinator.ts +312 -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 +254 -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 +227 -29
- package/src/runtime/root/package-runtime-release.ts +117 -30
- package/src/runtime/root/package-runtime-service.ts +24 -24
- package/src/runtime/root/release-execution-canary.ts +57 -12
- package/src/runtime/root/release-loader.ts +90 -8
- package/src/runtime/root/release-manifest.ts +80 -40
- package/src/runtime/root/release-materialize.ts +372 -106
- package/src/runtime/root/release-store.ts +395 -36
- package/src/runtime/root/runtime-lane.ts +191 -0
- package/src/runtime/root/runtime.ts +160 -29
- package/src/runtime/root/scheduled-controller-composition.ts +29 -3
- package/src/runtime/root/service-runner.ts +2 -2
- package/src/runtime/root/service.ts +126 -44
- package/src/runtime/root/status.ts +48 -0
- package/src/runtime/root/types.ts +32 -9
- package/src/runtime/root/workflow-supervisor-composition.ts +681 -0
- package/src/runtime/root/workflow-supervisor-runtime.ts +564 -0
- package/src/runtime/root/write-fence.ts +50 -2
- package/src/runtime/safe-tooling/google-credential-store.ts +67 -22
- package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +5 -2
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
- package/src/runtime/shared/json-files.ts +8 -3
- 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 +3792 -417
- package/src/runtime/standalone-recovery/entry.ts +507 -45
- 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 +12 -2
- 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 +11 -3
- package/src/runtime/workflow/schedules/engine.ts +166 -178
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +95 -11
- 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 +218 -0
- package/supervisor/control-plane.ts +963 -0
- package/supervisor/entry.ts +15 -0
- package/supervisor/forge-validators.ts +147 -0
- package/supervisor/index.ts +5 -0
- package/supervisor/native-browser-adapter.ts +1151 -0
- package/supervisor/native-messaging/host.ts +58 -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 +250 -0
- package/supervisor/server.ts +412 -0
- package/supervisor/store.ts +1870 -0
- package/supervisor/tsconfig.json +8 -0
- package/supervisor/types.ts +261 -0
- package/SKILL.md +0 -427
- 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/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- 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-skill-version.ts +0 -5
- package/scripts/check-tarball-install-smoke.sh +0 -150
- package/scripts/ensure-codegraph.sh +0 -16
- 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/run-skill-hook.ts +0 -244
- package/scripts/stage-runtime-release.ts +0 -44
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- 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/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- 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
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
|
@@ -1,10 +1,11 @@
|
|
|
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';
|
|
5
|
-
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, rmSync } from 'fs';
|
|
4
|
+
import { CONTROLLER_SCOPE_REPO_ID, FORGE_INSTANCE_SCOPE_KEY, controllerSystemRoot, ensureControllerHome, repositoryControllerRoot } from '../../cli/repositories/controller-home';
|
|
5
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'fs';
|
|
6
6
|
import type { ExecutionJob, ResourceClaimSpec } from '../execution/jobs/types';
|
|
7
7
|
import { appendRuntimeEvent } from '../evidence/event-ledger';
|
|
8
|
+
import { redactSensitiveText, redactSensitiveValue } from '../evidence/sensitive-output';
|
|
8
9
|
import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../shared/json-files';
|
|
9
10
|
import { createFirstPartyPluginAdapterMap } from './first-party-registry';
|
|
10
11
|
import { reconcileFirstPartyExternalPluginRegistration, reconcileFirstPartyExternalPluginRegistrations } from './first-party-external-registration';
|
|
@@ -20,17 +21,14 @@ import {
|
|
|
20
21
|
} from './capability-authorization-grants';
|
|
21
22
|
import { markControllerContextProjectionDirty } from '../projections/controller-context';
|
|
22
23
|
import { acquireExecutionLeases, releaseExactExecutionLeases } from '../resources/leases/store';
|
|
23
|
-
import {
|
|
24
|
+
import { isRuntimeWriteFenceError } from '../root/write-fence';
|
|
24
25
|
import {
|
|
25
|
-
acceptSubmittedWorkContract,
|
|
26
|
-
activateWorkContract,
|
|
27
26
|
appendWorkEvidence,
|
|
28
|
-
failWorkContract,
|
|
29
27
|
getWorkContract,
|
|
30
28
|
recordWorkCompletionReceipt,
|
|
31
29
|
updateWorkContract,
|
|
32
30
|
} from '../../../packages/kernel/work/api/index';
|
|
33
|
-
import {
|
|
31
|
+
import { isTerminalSemanticWorkState, type RemoteEffectCompletionReceipt, type WorkContract } from '../control-plane/facade/types';
|
|
34
32
|
import type {
|
|
35
33
|
AssistantPluginAdapter,
|
|
36
34
|
AssistantPluginActionDescriptor,
|
|
@@ -75,65 +73,191 @@ function now(): string {
|
|
|
75
73
|
return new Date().toISOString();
|
|
76
74
|
}
|
|
77
75
|
|
|
78
|
-
|
|
76
|
+
const CONTROLLER_PLUGIN_CACHE_SCOPE = 'controller';
|
|
77
|
+
const CONTROLLER_PLUGIN_GRANT_SCOPE = 'controller:global';
|
|
78
|
+
const CONTROLLER_PLUGIN_SCOPE_KEY = FORGE_INSTANCE_SCOPE_KEY;
|
|
79
|
+
|
|
80
|
+
interface PluginActionScopeContext {
|
|
81
|
+
kind: 'controller' | 'repository';
|
|
82
|
+
scopeKey: string;
|
|
83
|
+
grantRepoId: string;
|
|
84
|
+
root: string;
|
|
85
|
+
checkoutId?: string;
|
|
86
|
+
repository?: RepositoryRecord;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function repositoryPluginActionScope(repository: RepositoryRecord): PluginActionScopeContext {
|
|
90
|
+
return {
|
|
91
|
+
kind: 'repository',
|
|
92
|
+
scopeKey: repository.repoId,
|
|
93
|
+
grantRepoId: repository.repoId,
|
|
94
|
+
root: repository.canonicalRoot,
|
|
95
|
+
checkoutId: repository.activeCheckoutId,
|
|
96
|
+
repository,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function controllerPluginActionScope(controllerHome: string): PluginActionScopeContext {
|
|
101
|
+
return {
|
|
102
|
+
kind: 'controller',
|
|
103
|
+
scopeKey: CONTROLLER_PLUGIN_SCOPE_KEY,
|
|
104
|
+
grantRepoId: CONTROLLER_PLUGIN_GRANT_SCOPE,
|
|
105
|
+
root: controllerSystemRoot(controllerHome),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function pluginActionExecutionFields(scope: PluginActionScopeContext): { repoId: string; repoRoot: string } {
|
|
110
|
+
// Adapter fields are a compatibility projection. controller:global identifies
|
|
111
|
+
// the Grant/config domain; it is not a Repository Registry identity.
|
|
112
|
+
return { repoId: scope.grantRepoId, repoRoot: scope.root };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function controllerPluginsRoot(controllerHome: string): string {
|
|
116
|
+
return join(controllerSystemRoot(controllerHome), 'plugins');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function controllerManifestPath(controllerHome: string, pluginId: string): string {
|
|
120
|
+
return join(controllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function controllerIndexPath(controllerHome: string): string {
|
|
124
|
+
return join(controllerPluginsRoot(controllerHome), 'index.json');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function adapterMatchesController(adapter: AssistantPluginAdapter): boolean {
|
|
128
|
+
const scope = adapter.scope ?? 'repository';
|
|
129
|
+
return scope === 'controller' || scope === 'controller_with_repository_overlay';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function adapterMatchesRepository(adapter: AssistantPluginAdapter, _repository: RepositoryRecord): boolean {
|
|
133
|
+
const scope = adapter.scope ?? 'repository';
|
|
134
|
+
return scope === 'repository' || scope === 'controller_with_repository_overlay';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function computeControllerManifest(
|
|
138
|
+
controllerHome: string,
|
|
139
|
+
pluginId: string,
|
|
140
|
+
resolvedAdapter?: AssistantPluginAdapter,
|
|
141
|
+
): AssistantPluginManifest {
|
|
142
|
+
const adapter = resolvedAdapter ?? resolvePluginAdapter(controllerHome, pluginId);
|
|
143
|
+
if (!adapter || !adapterMatchesController(adapter)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
|
|
144
|
+
const previous = readControllerStoredPluginManifest(controllerHome, pluginId);
|
|
79
145
|
const root = controllerSystemRoot(controllerHome);
|
|
80
|
-
const
|
|
146
|
+
const built = adapter.buildManifest(previous?.revision ?? 0, previous?.updatedAt, root, {
|
|
147
|
+
controllerHome,
|
|
148
|
+
repoId: CONTROLLER_PLUGIN_GRANT_SCOPE,
|
|
149
|
+
repoRoot: root,
|
|
150
|
+
controllerScoped: true,
|
|
151
|
+
});
|
|
152
|
+
const changed = !previous || fingerprintManifest(previous) !== fingerprintManifest(built);
|
|
81
153
|
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',
|
|
154
|
+
...built,
|
|
155
|
+
revision: previous ? (changed ? previous.revision + 1 : previous.revision) : 1,
|
|
156
|
+
updatedAt: changed ? now() : previous?.updatedAt ?? built.updatedAt,
|
|
97
157
|
};
|
|
98
158
|
}
|
|
99
159
|
|
|
100
|
-
/**
|
|
101
|
-
* Controller-scoped product facade. The synthetic RepositoryRecord remains an
|
|
102
|
-
* internal storage/projection compatibility shape only; callers need no repo.
|
|
103
|
-
*/
|
|
104
160
|
export function readControllerStoredPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest | undefined {
|
|
105
|
-
|
|
161
|
+
try {
|
|
162
|
+
const canonicalPath = controllerManifestPath(controllerHome, pluginId);
|
|
163
|
+
if (existsSync(canonicalPath)) return readJsonFile<AssistantPluginManifest>(canonicalPath);
|
|
164
|
+
const legacyPath = join(legacyControllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
|
|
165
|
+
return existsSync(legacyPath) ? readJsonFile<AssistantPluginManifest>(legacyPath) : undefined;
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function listControllerPluginIds(controllerHome: string): string[] {
|
|
172
|
+
return listPluginAdapters(controllerHome).filter(adapterMatchesController).map((adapter) => adapter.pluginId).sort();
|
|
106
173
|
}
|
|
107
174
|
|
|
108
175
|
export function listControllerPluginManifests(
|
|
109
176
|
controllerHome: string,
|
|
110
177
|
options: ListAssistantPluginManifestsOptions = {},
|
|
111
178
|
): AssistantPluginManifest[] {
|
|
112
|
-
|
|
179
|
+
const preferStored = options.preferStored === true && options.forceRefresh !== true;
|
|
180
|
+
const fallbackToLive = options.fallbackToLive !== false || !preferStored;
|
|
181
|
+
const cacheKey = listCacheKey(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, preferStored, fallbackToLive);
|
|
182
|
+
if (options.forceRefresh !== true) {
|
|
183
|
+
const cached = readPluginManifestCache(pluginManifestListCache, cacheKey, preferStored ? PLUGIN_MANIFEST_STORED_CACHE_TTL_MS : PLUGIN_MANIFEST_LIVE_CACHE_TTL_MS);
|
|
184
|
+
if (cached) return cached;
|
|
185
|
+
}
|
|
186
|
+
const manifests = listControllerPluginIds(controllerHome).map((pluginId) => {
|
|
187
|
+
if (preferStored) {
|
|
188
|
+
const stored = readControllerStoredPluginManifest(controllerHome, pluginId);
|
|
189
|
+
if (stored) return stored;
|
|
190
|
+
if (!fallbackToLive) return undefined;
|
|
191
|
+
}
|
|
192
|
+
return computeControllerManifest(controllerHome, pluginId);
|
|
193
|
+
}).filter((manifest): manifest is AssistantPluginManifest => Boolean(manifest)).sort((left, right) => left.pluginId.localeCompare(right.pluginId));
|
|
194
|
+
primePluginManifestItemCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifests, preferStored);
|
|
195
|
+
return writePluginManifestCache(pluginManifestListCache, cacheKey, manifests);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getControllerPluginManifestForExecution(
|
|
199
|
+
controllerHome: string,
|
|
200
|
+
pluginId: string,
|
|
201
|
+
adapter: AssistantPluginAdapter,
|
|
202
|
+
): { manifest: AssistantPluginManifest; providerIdentityPrevalidated: boolean } {
|
|
203
|
+
const cacheKey = itemCacheKey(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId, false);
|
|
204
|
+
const cached = readPluginManifestCache(pluginManifestItemCache, cacheKey);
|
|
205
|
+
const wrap = (manifest: AssistantPluginManifest) => ({
|
|
206
|
+
manifest,
|
|
207
|
+
providerIdentityPrevalidated: manifest.enabled && manifest.health.probed && manifest.health.ready,
|
|
208
|
+
});
|
|
209
|
+
if (cached) return wrap(cached);
|
|
210
|
+
return wrap(writePluginManifestCache(pluginManifestItemCache, cacheKey, computeControllerManifest(controllerHome, pluginId, adapter)));
|
|
113
211
|
}
|
|
114
212
|
|
|
115
213
|
export function getControllerPluginManifest(controllerHome: string, pluginId: string): AssistantPluginManifest {
|
|
116
|
-
|
|
214
|
+
const adapter = resolvePluginAdapter(controllerHome, pluginId);
|
|
215
|
+
if (!adapter || !adapterMatchesController(adapter)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
|
|
216
|
+
return getControllerPluginManifestForExecution(controllerHome, pluginId, adapter).manifest;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function writeControllerRegistry(controllerHome: string, manifests: AssistantPluginManifest[]): AssistantPluginRegistryIndex {
|
|
220
|
+
const index: AssistantPluginRegistryIndex = {
|
|
221
|
+
schemaVersion: 1,
|
|
222
|
+
updatedAt: now(),
|
|
223
|
+
plugins: manifests.map((manifest) => ({
|
|
224
|
+
...pluginIndexEntry(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest),
|
|
225
|
+
manifestPath: controllerManifestPath(controllerHome, manifest.pluginId),
|
|
226
|
+
})).sort((left, right) => left.pluginId.localeCompare(right.pluginId)),
|
|
227
|
+
};
|
|
228
|
+
writeJsonAtomic(controllerIndexPath(controllerHome), index);
|
|
229
|
+
return index;
|
|
117
230
|
}
|
|
118
231
|
|
|
119
232
|
export function syncControllerPluginRegistry(controllerHome: string) {
|
|
120
233
|
reconcileFirstPartyExternalPluginRegistrations(controllerHome);
|
|
121
|
-
|
|
234
|
+
invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE);
|
|
235
|
+
const manifests = listControllerPluginManifests(controllerHome, { forceRefresh: true });
|
|
236
|
+
for (const manifest of manifests) writeJsonAtomic(controllerManifestPath(controllerHome, manifest.pluginId), manifest);
|
|
237
|
+
return { manifests, index: writeControllerRegistry(controllerHome, manifests) };
|
|
122
238
|
}
|
|
123
239
|
|
|
124
240
|
export function syncControllerPluginManifest(controllerHome: string, pluginId: string) {
|
|
125
241
|
reconcileFirstPartyExternalPluginRegistration(controllerHome, pluginId);
|
|
126
|
-
|
|
242
|
+
invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId);
|
|
243
|
+
const manifest = computeControllerManifest(controllerHome, pluginId);
|
|
244
|
+
writeJsonAtomic(controllerManifestPath(controllerHome, pluginId), manifest);
|
|
245
|
+
cacheAssistantPluginManifest(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest, false);
|
|
246
|
+
cacheAssistantPluginManifest(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, manifest, true);
|
|
247
|
+
const manifests = listControllerPluginIds(controllerHome).map((candidate) => candidate === pluginId
|
|
248
|
+
? manifest
|
|
249
|
+
: readControllerStoredPluginManifest(controllerHome, candidate) ?? cachedManifestForRepository(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, candidate))
|
|
250
|
+
.filter((entry): entry is AssistantPluginManifest => Boolean(entry)).sort((left, right) => left.pluginId.localeCompare(right.pluginId));
|
|
251
|
+
return { manifest, index: writeControllerRegistry(controllerHome, manifests) };
|
|
127
252
|
}
|
|
128
253
|
|
|
129
254
|
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';
|
|
255
|
+
invalidateAssistantPluginManifestCache(controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, pluginId);
|
|
256
|
+
rmSync(controllerManifestPath(controllerHome, pluginId), { force: true });
|
|
257
|
+
const current = readJsonFile<AssistantPluginRegistryIndex>(controllerIndexPath(controllerHome), { schemaVersion: 1, updatedAt: now(), plugins: [] });
|
|
258
|
+
const next = { ...current, updatedAt: now(), plugins: current.plugins.filter((entry) => entry.pluginId !== pluginId) };
|
|
259
|
+
writeJsonAtomic(controllerIndexPath(controllerHome), next);
|
|
260
|
+
return next;
|
|
137
261
|
}
|
|
138
262
|
|
|
139
263
|
function cloneCacheValue<T>(value: T): T {
|
|
@@ -226,7 +350,13 @@ export function clearAssistantPluginManifestCacheForTest(): void {
|
|
|
226
350
|
}
|
|
227
351
|
|
|
228
352
|
function pluginsRoot(controllerHome: string, repoId: string): string {
|
|
229
|
-
return
|
|
353
|
+
return repoId === CONTROLLER_SCOPE_REPO_ID
|
|
354
|
+
? join(controllerSystemRoot(controllerHome), 'plugins')
|
|
355
|
+
: join(repositoryControllerRoot(controllerHome, repoId), 'plugins');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function legacyControllerPluginsRoot(controllerHome: string): string {
|
|
359
|
+
return join(repositoryControllerRoot(controllerHome, CONTROLLER_SCOPE_REPO_ID), 'plugins');
|
|
230
360
|
}
|
|
231
361
|
|
|
232
362
|
function manifestPath(controllerHome: string, repoId: string, pluginId: string): string {
|
|
@@ -271,8 +401,14 @@ function pluginIndexEntry(controllerHome: string, repoId: string, manifest: Assi
|
|
|
271
401
|
}
|
|
272
402
|
|
|
273
403
|
function readStoredManifest(controllerHome: string, repoId: string, pluginId: string): AssistantPluginManifest | undefined {
|
|
404
|
+
const canonicalPath = manifestPath(controllerHome, repoId, pluginId);
|
|
274
405
|
try {
|
|
275
|
-
return readJsonFile<AssistantPluginManifest>(
|
|
406
|
+
if (existsSync(canonicalPath)) return readJsonFile<AssistantPluginManifest>(canonicalPath);
|
|
407
|
+
if (repoId === CONTROLLER_SCOPE_REPO_ID) {
|
|
408
|
+
const legacyPath = join(legacyControllerPluginsRoot(controllerHome), 'manifests', `${sanitizeFileComponent(pluginId)}.json`);
|
|
409
|
+
if (existsSync(legacyPath)) return readJsonFile<AssistantPluginManifest>(legacyPath);
|
|
410
|
+
}
|
|
411
|
+
return undefined;
|
|
276
412
|
} catch {
|
|
277
413
|
return undefined;
|
|
278
414
|
}
|
|
@@ -309,6 +445,20 @@ export function migrateRepositoryPluginConfigLegacyFiles(
|
|
|
309
445
|
join(repository.canonicalRoot, '.repo-harness', 'plugins'),
|
|
310
446
|
];
|
|
311
447
|
mkdirSync(targetRoot, { recursive: true });
|
|
448
|
+
// Upgrade an already-adopted v1/v2 Controller Home config in place. The
|
|
449
|
+
// adapter owns schema semantics; this store remains the sole file writer.
|
|
450
|
+
const browserTarget = repositoryPluginConfigPath({ controllerHome, repoId: repository.repoId }, 'browser');
|
|
451
|
+
const browserNormalizer = PLUGIN_ADAPTERS.get('browser')?.normalizeLegacyConfig;
|
|
452
|
+
if (!browserNormalizer) throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: browser schema owner missing');
|
|
453
|
+
if (existsSync(browserTarget)) {
|
|
454
|
+
const stat = lstatSync(browserTarget);
|
|
455
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('PLUGIN_CONFIG_AUTHORITY_PATH_INVALID: browser config');
|
|
456
|
+
let current: unknown;
|
|
457
|
+
try { current = JSON.parse(readFileSync(browserTarget, 'utf8')); }
|
|
458
|
+
catch { throw new Error('PLUGIN_CONFIG_AUTHORITY_INVALID: browser config JSON'); }
|
|
459
|
+
const normalized = browserNormalizer(current);
|
|
460
|
+
if ((current as { schemaVersion?: unknown }).schemaVersion !== 3) writeJsonAtomic(browserTarget, normalized);
|
|
461
|
+
}
|
|
312
462
|
for (const legacyRoot of legacyRoots) {
|
|
313
463
|
if (!existsSync(legacyRoot)) continue;
|
|
314
464
|
const stat = lstatSync(legacyRoot);
|
|
@@ -326,7 +476,16 @@ export function migrateRepositoryPluginConfigLegacyFiles(
|
|
|
326
476
|
const sourceStat = lstatSync(source);
|
|
327
477
|
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) continue;
|
|
328
478
|
const target = repositoryPluginConfigPath({ controllerHome, repoId: repository.repoId }, pluginId);
|
|
329
|
-
if (!existsSync(target))
|
|
479
|
+
if (!existsSync(target)) {
|
|
480
|
+
if (pluginId === 'browser') {
|
|
481
|
+
let parsed: unknown;
|
|
482
|
+
try { parsed = JSON.parse(readFileSync(source, 'utf8')); }
|
|
483
|
+
catch { throw new Error('PLUGIN_CONFIG_MIGRATION_INVALID: legacy browser JSON'); }
|
|
484
|
+
writeJsonAtomic(target, browserNormalizer(parsed));
|
|
485
|
+
} else {
|
|
486
|
+
cpSync(source, target, { force: false, errorOnExist: false });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
330
489
|
rmSync(source, { force: true });
|
|
331
490
|
}
|
|
332
491
|
if (readdirSync(legacyRoot).length === 0) rmSync(legacyRoot, { recursive: true, force: true });
|
|
@@ -340,7 +499,6 @@ function computeManifest(
|
|
|
340
499
|
pluginId: string,
|
|
341
500
|
resolvedAdapter?: AssistantPluginAdapter,
|
|
342
501
|
): AssistantPluginManifest {
|
|
343
|
-
migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
|
|
344
502
|
const adapter = resolvedAdapter ?? resolvePluginAdapter(controllerHome, pluginId);
|
|
345
503
|
if (!adapter || !adapterMatchesRepository(adapter, repository)) throw new Error(`PLUGIN_NOT_FOUND: ${pluginId}`);
|
|
346
504
|
const previous = readStoredManifest(controllerHome, repository.repoId, pluginId);
|
|
@@ -348,7 +506,7 @@ function computeManifest(
|
|
|
348
506
|
controllerHome,
|
|
349
507
|
repoId: repository.repoId,
|
|
350
508
|
repoRoot: repository.canonicalRoot,
|
|
351
|
-
controllerScoped:
|
|
509
|
+
controllerScoped: false,
|
|
352
510
|
});
|
|
353
511
|
const changed = !previous || fingerprintManifest(previous) !== fingerprintManifest(built);
|
|
354
512
|
return {
|
|
@@ -370,59 +528,76 @@ function writeRegistry(controllerHome: string, repoId: string, manifests: Assist
|
|
|
370
528
|
return index;
|
|
371
529
|
}
|
|
372
530
|
|
|
373
|
-
|
|
531
|
+
function claimsForPluginActionScope(
|
|
374
532
|
action: AssistantPluginActionDescriptor,
|
|
375
|
-
|
|
533
|
+
scope: PluginActionScopeContext,
|
|
376
534
|
trustedPluginId: string,
|
|
377
535
|
): ResourceClaimSpec[] {
|
|
378
|
-
const controllerScoped = repository.repoId === CONTROLLER_SCOPE_REPO_ID;
|
|
379
536
|
const pluginId = trustedPluginId.trim().replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
380
537
|
if (!pluginId) throw new Error('PLUGIN_ID_REQUIRED_FOR_RESOURCE_CLAIMS');
|
|
381
538
|
return action.resourceClaims.map((claim) => ({
|
|
382
539
|
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
540
|
? `provider-state:${pluginId}`
|
|
386
|
-
:
|
|
541
|
+
: scope.kind === 'controller'
|
|
387
542
|
? `controller-system:${claim.resource}`
|
|
388
543
|
: claim.resource === 'workspace'
|
|
389
|
-
? `workspace:${
|
|
544
|
+
? `workspace:${scope.checkoutId}`
|
|
390
545
|
: claim.resource === 'git-refs'
|
|
391
|
-
? `git-refs:${
|
|
392
|
-
: `repo-state:${
|
|
546
|
+
? `git-refs:${scope.scopeKey}`
|
|
547
|
+
: `repo-state:${scope.scopeKey}`,
|
|
393
548
|
mode: claim.mode,
|
|
394
549
|
}));
|
|
395
550
|
}
|
|
396
551
|
|
|
552
|
+
export function claimsForAssistantPluginAction(
|
|
553
|
+
action: AssistantPluginActionDescriptor,
|
|
554
|
+
repository: RepositoryRecord,
|
|
555
|
+
trustedPluginId: string,
|
|
556
|
+
): ResourceClaimSpec[] {
|
|
557
|
+
return claimsForPluginActionScope(action, repositoryPluginActionScope(repository), trustedPluginId);
|
|
558
|
+
}
|
|
559
|
+
|
|
397
560
|
async function withAssistantPluginResourceLeases<T>(
|
|
398
561
|
controllerHome: string,
|
|
399
|
-
|
|
562
|
+
scope: PluginActionScopeContext,
|
|
400
563
|
action: AssistantPluginActionDescriptor,
|
|
401
564
|
request: AssistantPluginActionRequest,
|
|
402
565
|
operation: () => Promise<T>,
|
|
403
566
|
): Promise<T> {
|
|
404
|
-
const claims =
|
|
567
|
+
const claims = claimsForPluginActionScope(action, scope, request.pluginId).map((claim) => ({
|
|
405
568
|
...claim,
|
|
406
|
-
repoId:
|
|
407
|
-
checkoutId:
|
|
569
|
+
repoId: scope.scopeKey,
|
|
570
|
+
...(scope.checkoutId ? { checkoutId: scope.checkoutId } : {}),
|
|
408
571
|
}));
|
|
409
572
|
if (claims.length === 0) return operation();
|
|
410
573
|
|
|
411
|
-
|
|
574
|
+
// requestId is the semantic idempotency key, but before its receipt/index is
|
|
575
|
+
// persisted two concurrent replays can enter this function. Give each live
|
|
576
|
+
// invocation a distinct lease owner so the later replay contends on the
|
|
577
|
+
// first invocation's declared resource instead of silently accumulating a
|
|
578
|
+
// second same-owner lease and then failing exact release with LEASE_SET_MISMATCH.
|
|
579
|
+
const ownerJobId = `plugin:${request.requestId}:${randomUUID()}`;
|
|
412
580
|
const timeoutMs = Math.max(5_000, Math.min(10 * 60_000, request.timeoutMs ?? action.defaultTimeoutMs));
|
|
413
|
-
const acquisition = acquireExecutionLeases(controllerHome,
|
|
581
|
+
const acquisition = acquireExecutionLeases(controllerHome, scope.scopeKey, ownerJobId, claims, {
|
|
414
582
|
ttlMs: timeoutMs + 60_000,
|
|
583
|
+
// This lease exists only to fence concurrent invocations of one external
|
|
584
|
+
// effect. It is not Scheduler/recovery authority, so derived projection,
|
|
585
|
+
// event-ledger, and wake side effects must never participate in the effect
|
|
586
|
+
// outcome. The lease file + fencing token remain cross-process durable for
|
|
587
|
+
// the bounded invocation and expire if its process dies.
|
|
588
|
+
visibility: 'ephemeral',
|
|
415
589
|
ownerIdentity: {
|
|
416
|
-
repositoryId:
|
|
417
|
-
checkoutId:
|
|
418
|
-
worktreeId:
|
|
590
|
+
repositoryId: scope.scopeKey,
|
|
591
|
+
checkoutId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
|
|
592
|
+
worktreeId: scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY,
|
|
419
593
|
branch: 'workflow-plugin-action',
|
|
420
594
|
principalId: request.origin.actor?.trim() || 'workflow-plugin-action',
|
|
421
595
|
controllerInstanceId: process.env.FORGE_RUNTIME_INSTANCE_ID?.trim()
|
|
422
596
|
|| process.env.FORGE_WRITER_INSTANCE_ID?.trim()
|
|
423
597
|
|| process.env.FORGE_DAEMON_INSTANCE_ID?.trim()
|
|
424
598
|
|| `process:${process.pid}`,
|
|
425
|
-
controllerGeneration: process.env.
|
|
599
|
+
controllerGeneration: process.env.FORGE_RUNTIME_INCARNATION_GENERATION?.trim()
|
|
600
|
+
|| process.env.FORGE_WRITER_GENERATION?.trim()
|
|
426
601
|
|| process.env.FORGE_ACTIVE_RUNTIME_REVISION?.trim()
|
|
427
602
|
|| 'unbound',
|
|
428
603
|
},
|
|
@@ -455,13 +630,24 @@ async function withAssistantPluginResourceLeases<T>(
|
|
|
455
630
|
retainForReconciliation = isAssistantPluginError(error) && error.effectOutcome === 'outcome_unknown';
|
|
456
631
|
throw error;
|
|
457
632
|
} finally {
|
|
458
|
-
if (!retainForReconciliation)
|
|
633
|
+
if (!retainForReconciliation) {
|
|
634
|
+
try {
|
|
635
|
+
releaseExactExecutionLeases(controllerHome, scope.scopeKey, ownerJobId, expected, { visibility: 'ephemeral' });
|
|
636
|
+
} catch (error) {
|
|
637
|
+
// A successful provider effect can rotate Runtime authority (for example
|
|
638
|
+
// a fenced Runtime cutover). The old sidecar must not release leases
|
|
639
|
+
// after it is fenced, but that cleanup denial must not overwrite the
|
|
640
|
+
// already-known provider result. The canonical successor Runtime can
|
|
641
|
+
// reclaim this ephemeral lease from its fenced owner immediately.
|
|
642
|
+
if (!isRuntimeWriteFenceError(error)) throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
459
645
|
}
|
|
460
646
|
}
|
|
461
647
|
|
|
462
|
-
function semanticKey(
|
|
648
|
+
function semanticKey(scope: PluginActionScopeContext, pluginId: string, actionId: string, args: Record<string, unknown>): string {
|
|
463
649
|
const digest = createHash('sha256').update(JSON.stringify(canonical(args))).digest('hex').slice(0, 20);
|
|
464
|
-
return `plugin-action:${
|
|
650
|
+
return `plugin-action:${scope.scopeKey}:${pluginId}:${actionId}:${digest}`;
|
|
465
651
|
}
|
|
466
652
|
|
|
467
653
|
function validatePrimitive(type: string, value: unknown): boolean {
|
|
@@ -515,7 +701,18 @@ function enforceConfirmation(action: AssistantPluginActionDescriptor, request: A
|
|
|
515
701
|
// Ordinary authorization is delegated to the host AI/tool permission model.
|
|
516
702
|
// Forge keeps only the explicit strong-confirmation boundary for
|
|
517
703
|
// destructive or irreversible plugin operations.
|
|
518
|
-
if (action.confirmation === 'none'
|
|
704
|
+
if (action.confirmation === 'none') return;
|
|
705
|
+
if (action.confirmation === 'authorization') {
|
|
706
|
+
if (action.requiredConfirmationText) {
|
|
707
|
+
if (request.confirmAuthorization !== true) {
|
|
708
|
+
throw new Error(`PLUGIN_CONFIRMATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirmAuthorization=true`);
|
|
709
|
+
}
|
|
710
|
+
if (request.confirmationText !== action.requiredConfirmationText) {
|
|
711
|
+
throw new Error(`PLUGIN_CONFIRMATION_TEXT_REQUIRED: provide confirmationText=${action.requiredConfirmationText}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
519
716
|
if (action.confirmation === 'strong_confirmation') {
|
|
520
717
|
if (request.confirmAuthorization !== true) {
|
|
521
718
|
throw new Error(`PLUGIN_CONFIRMATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirmAuthorization=true`);
|
|
@@ -582,7 +779,7 @@ function authorizationTargetMatches(
|
|
|
582
779
|
|
|
583
780
|
async function resolveAutomatedWriteAuthorization(input: {
|
|
584
781
|
controllerHome: string;
|
|
585
|
-
|
|
782
|
+
scope: PluginActionScopeContext;
|
|
586
783
|
adapter: AssistantPluginAdapter;
|
|
587
784
|
manifest: AssistantPluginManifest;
|
|
588
785
|
action: AssistantPluginActionDescriptor;
|
|
@@ -610,14 +807,14 @@ async function resolveAutomatedWriteAuthorization(input: {
|
|
|
610
807
|
}
|
|
611
808
|
const targetContext = await input.adapter.resolveAuthorizationContext({
|
|
612
809
|
controllerHome: input.controllerHome,
|
|
613
|
-
|
|
614
|
-
repoRoot: input.repository.canonicalRoot,
|
|
810
|
+
...pluginActionExecutionFields(input.scope),
|
|
615
811
|
pluginId: input.manifest.pluginId,
|
|
616
812
|
actionId: input.action.actionId,
|
|
617
813
|
requestId: input.requestId,
|
|
618
814
|
args: input.args,
|
|
619
815
|
origin: input.origin,
|
|
620
816
|
jobId: input.jobId,
|
|
817
|
+
authorizationGrantRefs: refs,
|
|
621
818
|
});
|
|
622
819
|
if (!targetContext) {
|
|
623
820
|
throw new AssistantPluginError(
|
|
@@ -630,7 +827,7 @@ async function resolveAutomatedWriteAuthorization(input: {
|
|
|
630
827
|
const grant = refs
|
|
631
828
|
.map((ref) => findActivePluginCapabilityAuthorizationById(input.controllerHome, ref))
|
|
632
829
|
.find((candidate) => candidate
|
|
633
|
-
&& candidate.repoId === input.
|
|
830
|
+
&& candidate.repoId === input.scope.grantRepoId
|
|
634
831
|
&& candidate.pluginId === input.manifest.pluginId
|
|
635
832
|
&& candidate.capabilityId === capabilityId
|
|
636
833
|
&& authorizationTargetMatches(candidate.target, targetContext.target)
|
|
@@ -759,6 +956,7 @@ export function syncAssistantPluginRegistry(
|
|
|
759
956
|
repository: RepositoryRecord,
|
|
760
957
|
): { manifests: AssistantPluginManifest[]; index: AssistantPluginRegistryIndex } {
|
|
761
958
|
invalidateAssistantPluginManifestCache(controllerHome, repository.repoId);
|
|
959
|
+
migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
|
|
762
960
|
const manifests = listAssistantPluginManifests(controllerHome, repository, { forceRefresh: true });
|
|
763
961
|
for (const manifest of manifests) {
|
|
764
962
|
writeJsonAtomic(manifestPath(controllerHome, repository.repoId, manifest.pluginId), manifest);
|
|
@@ -776,6 +974,7 @@ export function syncAssistantPluginManifest(
|
|
|
776
974
|
pluginId: string,
|
|
777
975
|
): { manifest: AssistantPluginManifest; index: AssistantPluginRegistryIndex } {
|
|
778
976
|
invalidateAssistantPluginManifestCache(controllerHome, repository.repoId, pluginId);
|
|
977
|
+
migrateRepositoryPluginConfigLegacyFiles(controllerHome, repository);
|
|
779
978
|
const manifest = computeManifest(controllerHome, repository, pluginId);
|
|
780
979
|
writeJsonAtomic(manifestPath(controllerHome, repository.repoId, manifest.pluginId), manifest);
|
|
781
980
|
markControllerContextProjectionDirty(repository.canonicalRoot, `plugin:${pluginId}:synced`);
|
|
@@ -819,25 +1018,39 @@ export function removeAssistantPluginManifestProjection(
|
|
|
819
1018
|
}
|
|
820
1019
|
|
|
821
1020
|
export function isDirectPluginReadAction(action: AssistantPluginActionDescriptor): boolean {
|
|
822
|
-
return action.
|
|
1021
|
+
return action.executionMode !== 'direct_non_persistent'
|
|
1022
|
+
&& action.readOnly === true
|
|
823
1023
|
&& action.risk === 'readonly'
|
|
824
1024
|
&& action.confirmation === 'none'
|
|
825
1025
|
&& action.idempotent === true;
|
|
826
1026
|
}
|
|
827
1027
|
|
|
828
1028
|
export function isDirectNonPersistentPluginAction(action: AssistantPluginActionDescriptor): boolean {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1029
|
+
if (
|
|
1030
|
+
action.executionMode !== 'direct_non_persistent'
|
|
1031
|
+
|| action.remoteEffectWorkCompletion !== undefined
|
|
1032
|
+
|| action.resourceClaims.length !== 0
|
|
1033
|
+
) {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Direct non-persistent actions are provider-local operations that must never
|
|
1038
|
+
// enter the durable effect/receipt path. They are intentionally limited to
|
|
1039
|
+
// either a pure provider read or a provider-local workspace effect. Remote
|
|
1040
|
+
// writes/destructive effects still require the normal durable application
|
|
1041
|
+
// path and its reconciliation authority.
|
|
1042
|
+
if (action.readOnly) {
|
|
1043
|
+
return action.risk === 'readonly'
|
|
1044
|
+
&& (action.confirmation === 'none' || action.confirmation === 'authorization')
|
|
1045
|
+
&& action.idempotent === true;
|
|
1046
|
+
}
|
|
1047
|
+
return action.risk === 'workspace_write'
|
|
1048
|
+
&& (action.confirmation === 'authorization' || action.confirmation === 'none');
|
|
836
1049
|
}
|
|
837
1050
|
|
|
838
|
-
|
|
1051
|
+
async function executePluginDirectNonPersistentInScope(
|
|
839
1052
|
controllerHome: string,
|
|
840
|
-
|
|
1053
|
+
scope: PluginActionScopeContext,
|
|
841
1054
|
request: AssistantPluginActionRequest,
|
|
842
1055
|
): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown> }> {
|
|
843
1056
|
const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
|
|
@@ -845,7 +1058,9 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
845
1058
|
if (PLUGIN_ADAPTERS.get(request.pluginId) !== adapter) {
|
|
846
1059
|
throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_FIRST_PARTY_REQUIRED: ${request.pluginId}/${request.actionId}`);
|
|
847
1060
|
}
|
|
848
|
-
const manifestLookup =
|
|
1061
|
+
const manifestLookup = scope.kind === 'controller'
|
|
1062
|
+
? getControllerPluginManifestForExecution(controllerHome, request.pluginId, adapter)
|
|
1063
|
+
: getAssistantPluginManifestForExecution(controllerHome, scope.repository!, request.pluginId, adapter);
|
|
849
1064
|
const manifest = manifestLookup.manifest;
|
|
850
1065
|
const action = actionForManifest(manifest, request.actionId);
|
|
851
1066
|
if (!manifest.enabled && action.actionId !== 'configure') {
|
|
@@ -860,7 +1075,7 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
860
1075
|
if (request.workId || request.workRepoId) {
|
|
861
1076
|
throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_WORK_ATTRIBUTION_FORBIDDEN: ${request.pluginId}/${request.actionId}`);
|
|
862
1077
|
}
|
|
863
|
-
if (request.confirmAuthorization !== true) {
|
|
1078
|
+
if (action.confirmation === 'authorization' && request.confirmAuthorization !== true) {
|
|
864
1079
|
throw new Error(`PLUGIN_DIRECT_NON_PERSISTENT_AUTHORIZATION_REQUIRED: ${request.pluginId}/${request.actionId} requires confirm_authorization=true for this invocation`);
|
|
865
1080
|
}
|
|
866
1081
|
const normalizedArgs = validateActionArguments(action, request.args ?? {});
|
|
@@ -868,7 +1083,7 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
868
1083
|
if (requiresAutomatedWriteAuthorization(request.origin)) {
|
|
869
1084
|
await resolveAutomatedWriteAuthorization({
|
|
870
1085
|
controllerHome,
|
|
871
|
-
|
|
1086
|
+
scope,
|
|
872
1087
|
adapter,
|
|
873
1088
|
manifest,
|
|
874
1089
|
action,
|
|
@@ -880,8 +1095,7 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
880
1095
|
}
|
|
881
1096
|
const result = await adapter.executeAction({
|
|
882
1097
|
controllerHome,
|
|
883
|
-
|
|
884
|
-
repoRoot: repository.canonicalRoot,
|
|
1098
|
+
...pluginActionExecutionFields(scope),
|
|
885
1099
|
pluginId: request.pluginId,
|
|
886
1100
|
actionId: request.actionId,
|
|
887
1101
|
requestId: request.requestId,
|
|
@@ -895,12 +1109,29 @@ export async function executeAssistantPluginDirectNonPersistent(
|
|
|
895
1109
|
return { manifest, action, result };
|
|
896
1110
|
}
|
|
897
1111
|
|
|
898
|
-
export
|
|
1112
|
+
export function executeAssistantPluginDirectNonPersistent(
|
|
899
1113
|
controllerHome: string,
|
|
900
1114
|
repository: RepositoryRecord,
|
|
901
1115
|
request: AssistantPluginActionRequest,
|
|
902
|
-
)
|
|
903
|
-
|
|
1116
|
+
) {
|
|
1117
|
+
return executePluginDirectNonPersistentInScope(controllerHome, repositoryPluginActionScope(repository), request);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
export function executeControllerPluginDirectNonPersistent(
|
|
1121
|
+
controllerHome: string,
|
|
1122
|
+
request: AssistantPluginActionRequest,
|
|
1123
|
+
) {
|
|
1124
|
+
return executePluginDirectNonPersistentInScope(controllerHome, controllerPluginActionScope(controllerHome), request);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async function executePluginReadDirectInScope(
|
|
1128
|
+
controllerHome: string,
|
|
1129
|
+
scope: PluginActionScopeContext,
|
|
1130
|
+
request: AssistantPluginActionRequest,
|
|
1131
|
+
): Promise<{ manifest: AssistantPluginManifest; action: AssistantPluginActionDescriptor; result: Record<string, unknown>; receipt: PluginActionReceipt }> {
|
|
1132
|
+
const manifest = scope.kind === 'controller'
|
|
1133
|
+
? getControllerPluginManifest(controllerHome, request.pluginId)
|
|
1134
|
+
: getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
|
|
904
1135
|
const action = actionForManifest(manifest, request.actionId);
|
|
905
1136
|
if (!manifest.enabled && action.actionId !== 'configure') {
|
|
906
1137
|
throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
|
|
@@ -910,24 +1141,68 @@ export async function executeAssistantPluginReadDirect(
|
|
|
910
1141
|
}
|
|
911
1142
|
const normalizedArgs = validateActionArguments(action, request.args ?? {});
|
|
912
1143
|
enforceConfirmation(action, { ...request, args: normalizedArgs });
|
|
913
|
-
const result = await
|
|
1144
|
+
const result = await executePluginActionInScope(scope, {
|
|
914
1145
|
controllerHome,
|
|
915
|
-
|
|
916
|
-
repoRoot: repository.canonicalRoot,
|
|
1146
|
+
...pluginActionExecutionFields(scope),
|
|
917
1147
|
pluginId: request.pluginId,
|
|
918
1148
|
actionId: request.actionId,
|
|
919
1149
|
requestId: request.requestId,
|
|
920
1150
|
args: normalizedArgs,
|
|
921
1151
|
origin: request.origin,
|
|
922
1152
|
});
|
|
923
|
-
|
|
1153
|
+
const createdAt = new Date().toISOString();
|
|
1154
|
+
const resultDigest = createHash('sha256').update(JSON.stringify(result)).digest('hex');
|
|
1155
|
+
const observationSemanticKey = semanticKey(scope, request.pluginId, request.actionId, normalizedArgs);
|
|
1156
|
+
const observationActor = request.origin?.actor?.trim() ?? '';
|
|
1157
|
+
// Observation evidence is content-addressed so repeated identical reads do not create
|
|
1158
|
+
// an unbounded receipt trail. This remains provenance only, never replay/effect state.
|
|
1159
|
+
const receiptId = `PLG-OBS-${createHash('sha256')
|
|
1160
|
+
.update(`${observationSemanticKey}:${resultDigest}:${observationActor}`)
|
|
1161
|
+
.digest('hex').slice(0, 24)}`;
|
|
1162
|
+
const receipt: PluginActionReceipt = {
|
|
1163
|
+
schemaVersion: 1,
|
|
1164
|
+
receiptId,
|
|
1165
|
+
requestId: request.requestId,
|
|
1166
|
+
scopeKey: scope.scopeKey,
|
|
1167
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1168
|
+
pluginId: request.pluginId,
|
|
1169
|
+
actionId: request.actionId,
|
|
1170
|
+
semanticKey: observationSemanticKey,
|
|
1171
|
+
status: 'succeeded',
|
|
1172
|
+
createdAt,
|
|
1173
|
+
...(request.origin ? { origin: request.origin } : {}),
|
|
1174
|
+
observationOnly: true,
|
|
1175
|
+
resultDigest,
|
|
1176
|
+
};
|
|
1177
|
+
// Direct reads stay on the inline fast path. Persist only a compact observation
|
|
1178
|
+
// receipt, never provider result content and never request replay state.
|
|
1179
|
+
writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
|
|
1180
|
+
return { manifest, action, result, receipt };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export function executeAssistantPluginReadDirect(
|
|
1184
|
+
controllerHome: string,
|
|
1185
|
+
repository: RepositoryRecord,
|
|
1186
|
+
request: AssistantPluginActionRequest,
|
|
1187
|
+
) {
|
|
1188
|
+
return executePluginReadDirectInScope(controllerHome, repositoryPluginActionScope(repository), request);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
export function executeControllerPluginReadDirect(
|
|
1192
|
+
controllerHome: string,
|
|
1193
|
+
request: AssistantPluginActionRequest,
|
|
1194
|
+
) {
|
|
1195
|
+
return executePluginReadDirectInScope(controllerHome, controllerPluginActionScope(controllerHome), request);
|
|
924
1196
|
}
|
|
925
1197
|
|
|
926
1198
|
export interface PluginActionReceipt {
|
|
927
1199
|
schemaVersion: 1;
|
|
928
1200
|
receiptId: string;
|
|
929
1201
|
requestId: string;
|
|
930
|
-
|
|
1202
|
+
/** Canonical plugin-action receipt scope: `instance` for controller plugins, repository id otherwise. */
|
|
1203
|
+
scopeKey: string;
|
|
1204
|
+
/** Repository provenance only; absent for controller-scoped plugin actions. */
|
|
1205
|
+
repoId?: string;
|
|
931
1206
|
/** Work authority repository when it differs from controller/provider receipt scope. */
|
|
932
1207
|
workRepoId?: string;
|
|
933
1208
|
pluginId: string;
|
|
@@ -941,24 +1216,35 @@ export interface PluginActionReceipt {
|
|
|
941
1216
|
origin?: { surface: string; actor?: string; correlationId?: string };
|
|
942
1217
|
authorization?: PluginActionAuthorizationEvidence;
|
|
943
1218
|
result?: Record<string, unknown>;
|
|
944
|
-
error?: { code: string; message: string };
|
|
1219
|
+
error?: { code: string; message: string; retryable?: boolean; details?: Record<string, unknown> };
|
|
1220
|
+
/** True for a compact direct-read observation; it is evidence, never an effect-completion authority. */
|
|
1221
|
+
observationOnly?: boolean;
|
|
1222
|
+
/** SHA-256 of the direct-read result. Result content is intentionally not persisted for observation-only receipts. */
|
|
1223
|
+
resultDigest?: string;
|
|
945
1224
|
}
|
|
946
1225
|
|
|
947
1226
|
interface PluginActionRequestIndex {
|
|
948
1227
|
requestId: string;
|
|
949
|
-
|
|
1228
|
+
scopeKey: string;
|
|
1229
|
+
repoId?: string;
|
|
950
1230
|
workRepoId?: string;
|
|
951
1231
|
receiptId: string;
|
|
952
1232
|
semanticKey: string;
|
|
953
1233
|
createdAt: string;
|
|
954
1234
|
}
|
|
955
1235
|
|
|
956
|
-
function pluginActionReceiptRoot(controllerHome: string,
|
|
957
|
-
const root =
|
|
1236
|
+
function pluginActionReceiptRoot(controllerHome: string, scopeKey: string): string {
|
|
1237
|
+
const root = scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY
|
|
1238
|
+
? join(controllerSystemRoot(controllerHome), 'plugin-action-receipts')
|
|
1239
|
+
: join(repositoryControllerRoot(controllerHome, scopeKey), 'plugin-action-receipts');
|
|
958
1240
|
mkdirSync(root, { recursive: true });
|
|
959
1241
|
return root;
|
|
960
1242
|
}
|
|
961
1243
|
|
|
1244
|
+
function legacyControllerPluginActionReceiptPath(controllerHome: string, receiptId: string): string {
|
|
1245
|
+
return join(repositoryControllerRoot(controllerHome, CONTROLLER_SCOPE_REPO_ID), 'plugin-action-receipts', `${sanitizeFileComponent(receiptId)}.json`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
962
1248
|
function pluginActionRequestPath(controllerHome: string, requestId: string): string {
|
|
963
1249
|
const hash = createHash('sha256').update(requestId).digest('hex');
|
|
964
1250
|
const root = join(ensureControllerHome(controllerHome), 'indexes', 'plugin-actions', 'requests');
|
|
@@ -966,14 +1252,30 @@ function pluginActionRequestPath(controllerHome: string, requestId: string): str
|
|
|
966
1252
|
return join(root, `${hash}.json`);
|
|
967
1253
|
}
|
|
968
1254
|
|
|
969
|
-
function pluginActionReceiptPath(controllerHome: string,
|
|
970
|
-
return join(pluginActionReceiptRoot(controllerHome,
|
|
1255
|
+
function pluginActionReceiptPath(controllerHome: string, scopeKey: string, receiptId: string): string {
|
|
1256
|
+
return join(pluginActionReceiptRoot(controllerHome, scopeKey), `${sanitizeFileComponent(receiptId)}.json`);
|
|
971
1257
|
}
|
|
972
1258
|
|
|
973
|
-
function
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
1259
|
+
function normalizePluginActionReceipt(receipt: PluginActionReceipt): PluginActionReceipt {
|
|
1260
|
+
const scopeKey = receipt.scopeKey?.trim()
|
|
1261
|
+
|| (receipt.repoId === CONTROLLER_SCOPE_REPO_ID ? CONTROLLER_PLUGIN_SCOPE_KEY : receipt.repoId?.trim())
|
|
1262
|
+
|| '';
|
|
1263
|
+
if (!scopeKey) throw new Error('PLUGIN_RECEIPT_SCOPE_CORRUPT');
|
|
1264
|
+
return {
|
|
1265
|
+
...receipt,
|
|
1266
|
+
scopeKey,
|
|
1267
|
+
...(scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY ? { repoId: undefined } : { repoId: receipt.repoId ?? scopeKey }),
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function readPluginActionReceipt(controllerHome: string, scopeKey: string, receiptId: string): PluginActionReceipt | undefined {
|
|
1272
|
+
const path = pluginActionReceiptPath(controllerHome, scopeKey, receiptId);
|
|
1273
|
+
if (existsSync(path)) return normalizePluginActionReceipt(readJsonFile<PluginActionReceipt>(path));
|
|
1274
|
+
if (scopeKey === CONTROLLER_PLUGIN_SCOPE_KEY || scopeKey === CONTROLLER_SCOPE_REPO_ID) {
|
|
1275
|
+
const legacyPath = legacyControllerPluginActionReceiptPath(controllerHome, receiptId);
|
|
1276
|
+
if (existsSync(legacyPath)) return normalizePluginActionReceipt(readJsonFile<PluginActionReceipt>(legacyPath));
|
|
1277
|
+
}
|
|
1278
|
+
return undefined;
|
|
977
1279
|
}
|
|
978
1280
|
|
|
979
1281
|
export function findPluginActionReceipt(
|
|
@@ -981,6 +1283,12 @@ export function findPluginActionReceipt(
|
|
|
981
1283
|
receiptId: string,
|
|
982
1284
|
): PluginActionReceipt | undefined {
|
|
983
1285
|
const home = ensureControllerHome(controllerHome);
|
|
1286
|
+
// Controller-scoped plugins (for example Local System) live under system/, not
|
|
1287
|
+
// repositories/. They are first-class evidence producers and must be searched too.
|
|
1288
|
+
const controllerReceipt = readPluginActionReceipt(home, CONTROLLER_PLUGIN_SCOPE_KEY, receiptId)
|
|
1289
|
+
// One-way read compatibility for pre-cutover controller receipts only.
|
|
1290
|
+
?? readPluginActionReceipt(home, CONTROLLER_SCOPE_REPO_ID, receiptId);
|
|
1291
|
+
if (controllerReceipt) return controllerReceipt;
|
|
984
1292
|
const repositoriesRoot = join(home, 'repositories');
|
|
985
1293
|
try {
|
|
986
1294
|
for (const repoId of readdirSync(repositoriesRoot)) {
|
|
@@ -999,7 +1307,9 @@ export function readPluginActionReceiptForRequest(controllerHome: string, reques
|
|
|
999
1307
|
if (!existsSync(path)) return undefined;
|
|
1000
1308
|
const index = readJsonFile<PluginActionRequestIndex>(path);
|
|
1001
1309
|
if (index.requestId !== requestId) throw new Error('PLUGIN_RECEIPT_REQUEST_MISMATCH');
|
|
1002
|
-
const
|
|
1310
|
+
const scopeKey = index.scopeKey?.trim() || index.repoId?.trim();
|
|
1311
|
+
if (!scopeKey) throw new Error('PLUGIN_RECEIPT_REQUEST_SCOPE_CORRUPT');
|
|
1312
|
+
const receipt = readPluginActionReceipt(controllerHome, scopeKey, index.receiptId);
|
|
1003
1313
|
if (!receipt || receipt.requestId !== requestId || receipt.semanticKey !== index.semanticKey) throw new Error('PLUGIN_RECEIPT_LOST');
|
|
1004
1314
|
return receipt;
|
|
1005
1315
|
}
|
|
@@ -1011,86 +1321,20 @@ export function compatibilityPluginJobFromReceipt(
|
|
|
1011
1321
|
return compatibilityJobFromReceipt(receipt, checkoutId);
|
|
1012
1322
|
}
|
|
1013
1323
|
|
|
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 {
|
|
1324
|
+
function workAttributionRepoId(scope: PluginActionScopeContext, request: AssistantPluginActionRequest): string {
|
|
1083
1325
|
const explicit = request.workRepoId?.trim();
|
|
1084
1326
|
if (!request.workId?.trim()) {
|
|
1085
1327
|
if (explicit) throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_WITHOUT_WORK: ${explicit}`);
|
|
1086
|
-
return repository
|
|
1328
|
+
return scope.repository?.repoId ?? '';
|
|
1087
1329
|
}
|
|
1088
|
-
|
|
1330
|
+
if (explicit) return explicit;
|
|
1331
|
+
if (scope.repository) return scope.repository.repoId;
|
|
1332
|
+
throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_REQUIRED: controller-scoped action ${request.pluginId}/${request.actionId} must name the Work repository explicitly`);
|
|
1089
1333
|
}
|
|
1090
1334
|
|
|
1091
1335
|
function remoteEffectWorkForPluginAction(
|
|
1092
1336
|
controllerHome: string,
|
|
1093
|
-
|
|
1337
|
+
scope: PluginActionScopeContext,
|
|
1094
1338
|
action: AssistantPluginActionDescriptor,
|
|
1095
1339
|
request: AssistantPluginActionRequest,
|
|
1096
1340
|
receiptId?: string,
|
|
@@ -1100,7 +1344,7 @@ function remoteEffectWorkForPluginAction(
|
|
|
1100
1344
|
if (action.risk !== 'remote_write') {
|
|
1101
1345
|
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_REQUIRES_REMOTE_WRITE: ${action.actionId}`);
|
|
1102
1346
|
}
|
|
1103
|
-
const workRepoId = workAttributionRepoId(
|
|
1347
|
+
const workRepoId = workAttributionRepoId(scope, request);
|
|
1104
1348
|
const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
|
|
1105
1349
|
if (!work) throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_NOT_FOUND: ${workId}`);
|
|
1106
1350
|
if (work.workKind !== 'remote_effect') {
|
|
@@ -1109,49 +1353,64 @@ function remoteEffectWorkForPluginAction(
|
|
|
1109
1353
|
if (work.checks.length > 0) {
|
|
1110
1354
|
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CHECKS_PRESENT: ${workId} declares repository checks and cannot be completed by a plugin receipt alone`);
|
|
1111
1355
|
}
|
|
1112
|
-
if (
|
|
1356
|
+
if (isTerminalSemanticWorkState(work.semanticState)) {
|
|
1113
1357
|
const receiptAlreadyBound = Boolean(receiptId) && (
|
|
1114
1358
|
work.completionReceipt?.receiptId === receiptId
|
|
1115
1359
|
|| work.evidenceRefs.some((evidence) => evidence.evidenceId === receiptId)
|
|
1116
1360
|
);
|
|
1117
|
-
if (work.
|
|
1118
|
-
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_TERMINAL: ${workId} is ${work.
|
|
1361
|
+
if (work.semanticState === 'completed' && receiptAlreadyBound) return work;
|
|
1362
|
+
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_TERMINAL: ${workId} is ${work.semanticState}`);
|
|
1119
1363
|
}
|
|
1120
1364
|
return work;
|
|
1121
1365
|
}
|
|
1122
1366
|
|
|
1123
1367
|
function attributedWorkForPluginAction(
|
|
1124
1368
|
controllerHome: string,
|
|
1125
|
-
|
|
1369
|
+
scope: PluginActionScopeContext,
|
|
1126
1370
|
action: AssistantPluginActionDescriptor,
|
|
1127
1371
|
request: AssistantPluginActionRequest,
|
|
1128
1372
|
): WorkContract | undefined {
|
|
1129
1373
|
const workId = request.workId?.trim();
|
|
1130
1374
|
if (!workId || action.risk === 'remote_write') return undefined;
|
|
1131
|
-
const workRepoId = workAttributionRepoId(
|
|
1375
|
+
const workRepoId = workAttributionRepoId(scope, request);
|
|
1132
1376
|
const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
|
|
1133
1377
|
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.
|
|
1378
|
+
if (isTerminalSemanticWorkState(work.semanticState)) {
|
|
1379
|
+
throw new Error(`WORK_PLUGIN_ATTRIBUTION_TERMINAL: ${workId} is ${work.semanticState}`);
|
|
1136
1380
|
}
|
|
1137
1381
|
return work;
|
|
1138
1382
|
}
|
|
1139
1383
|
|
|
1384
|
+
/** Reuse the canonical Work binding check before a Process request ID is reserved. */
|
|
1385
|
+
export function preflightPluginActionWorkBinding(
|
|
1386
|
+
controllerHome: string,
|
|
1387
|
+
pluginScope: { kind: 'controller' } | { kind: 'repository'; repository: RepositoryRecord },
|
|
1388
|
+
action: AssistantPluginActionDescriptor,
|
|
1389
|
+
request: AssistantPluginActionRequest,
|
|
1390
|
+
): void {
|
|
1391
|
+
if (!request.workId?.trim()) return;
|
|
1392
|
+
const scope = pluginScope.kind === 'controller'
|
|
1393
|
+
? controllerPluginActionScope(controllerHome)
|
|
1394
|
+
: repositoryPluginActionScope(pluginScope.repository);
|
|
1395
|
+
if (action.risk === 'remote_write') remoteEffectWorkForPluginAction(controllerHome, scope, action, request);
|
|
1396
|
+
else attributedWorkForPluginAction(controllerHome, scope, action, request);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1140
1399
|
function bindLocalEffectReceiptToAttributedWork(
|
|
1141
1400
|
controllerHome: string,
|
|
1142
|
-
|
|
1401
|
+
scope: PluginActionScopeContext,
|
|
1143
1402
|
action: AssistantPluginActionDescriptor,
|
|
1144
1403
|
request: AssistantPluginActionRequest,
|
|
1145
1404
|
receipt: PluginActionReceipt,
|
|
1146
1405
|
work?: WorkContract,
|
|
1147
1406
|
): WorkContract | undefined {
|
|
1148
1407
|
if (!request.workId || receipt.status !== 'succeeded') return work;
|
|
1149
|
-
const workRepoId = workAttributionRepoId(
|
|
1408
|
+
const workRepoId = workAttributionRepoId(scope, request);
|
|
1150
1409
|
const attributed = work ?? getWorkContract({ controllerHome, repoId: workRepoId }, request.workId.trim());
|
|
1151
1410
|
if (!attributed) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${request.workId.trim()}`);
|
|
1152
1411
|
if (attributed.workKind !== 'local_effect' || action.readOnly || action.risk === 'readonly') return attributed;
|
|
1153
1412
|
if (attributed.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) return attributed;
|
|
1154
|
-
if (
|
|
1413
|
+
if (isTerminalSemanticWorkState(attributed.semanticState)) return attributed;
|
|
1155
1414
|
return appendWorkEvidence({ controllerHome, repoId: workRepoId }, attributed.workId, {
|
|
1156
1415
|
evidenceId: receipt.receiptId,
|
|
1157
1416
|
title: 'typed local plugin effect completed',
|
|
@@ -1160,7 +1419,7 @@ function bindLocalEffectReceiptToAttributedWork(
|
|
|
1160
1419
|
});
|
|
1161
1420
|
}
|
|
1162
1421
|
|
|
1163
|
-
export function
|
|
1422
|
+
export function recordRemoteEffectWorkActionReceipt(
|
|
1164
1423
|
controllerHome: string,
|
|
1165
1424
|
repoId: string,
|
|
1166
1425
|
workId: string,
|
|
@@ -1170,14 +1429,12 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
|
|
|
1170
1429
|
if (work.workKind !== 'remote_effect') {
|
|
1171
1430
|
throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_KIND_MISMATCH: ${workId} is ${work.workKind}, expected remote_effect`);
|
|
1172
1431
|
}
|
|
1173
|
-
if (work.
|
|
1174
|
-
if (isTerminalWorkContractStatus(work.status)) {
|
|
1175
|
-
throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_TERMINAL: ${workId} is ${work.status}`);
|
|
1176
|
-
}
|
|
1432
|
+
if (work.completionReceipt?.source === 'remote_effect') return work;
|
|
1177
1433
|
const receipt = work.evidenceRefs
|
|
1178
1434
|
.map((evidence) => {
|
|
1179
1435
|
if (!evidence.evidenceId) return undefined;
|
|
1180
1436
|
return readPluginActionReceipt(controllerHome, repoId, evidence.evidenceId)
|
|
1437
|
+
?? readPluginActionReceipt(controllerHome, CONTROLLER_PLUGIN_SCOPE_KEY, evidence.evidenceId)
|
|
1181
1438
|
?? readPluginActionReceipt(controllerHome, CONTROLLER_SCOPE_REPO_ID, evidence.evidenceId);
|
|
1182
1439
|
})
|
|
1183
1440
|
.find((candidate) => candidate?.workId === workId && candidate.status === 'succeeded');
|
|
@@ -1207,17 +1464,17 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
|
|
|
1207
1464
|
|
|
1208
1465
|
function bindRemoteEffectReceiptToWork(
|
|
1209
1466
|
controllerHome: string,
|
|
1210
|
-
|
|
1467
|
+
scope: PluginActionScopeContext,
|
|
1211
1468
|
action: AssistantPluginActionDescriptor,
|
|
1212
1469
|
request: AssistantPluginActionRequest,
|
|
1213
1470
|
receipt: PluginActionReceipt,
|
|
1214
1471
|
): WorkContract | undefined {
|
|
1215
1472
|
if (!request.workId) return undefined;
|
|
1216
|
-
if (receipt.status !== 'succeeded') return remoteEffectWorkForPluginAction(controllerHome,
|
|
1217
|
-
const work = remoteEffectWorkForPluginAction(controllerHome,
|
|
1473
|
+
if (receipt.status !== 'succeeded') return remoteEffectWorkForPluginAction(controllerHome, scope, action, request);
|
|
1474
|
+
const work = remoteEffectWorkForPluginAction(controllerHome, scope, action, request, receipt.receiptId);
|
|
1218
1475
|
if (!work) return undefined;
|
|
1219
|
-
if (work.
|
|
1220
|
-
const workRepoId = workAttributionRepoId(
|
|
1476
|
+
if (work.completionReceipt?.source === 'remote_effect') return work;
|
|
1477
|
+
const workRepoId = workAttributionRepoId(scope, request);
|
|
1221
1478
|
let updated = work;
|
|
1222
1479
|
if (!work.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) {
|
|
1223
1480
|
updated = appendWorkEvidence({ controllerHome, repoId: workRepoId }, work.workId, {
|
|
@@ -1254,7 +1511,7 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
|
|
|
1254
1511
|
schemaVersion: 1,
|
|
1255
1512
|
revision: 1,
|
|
1256
1513
|
jobId: receipt.receiptId,
|
|
1257
|
-
repoId: receipt.repoId,
|
|
1514
|
+
repoId: receipt.repoId ?? receipt.scopeKey,
|
|
1258
1515
|
checkoutId,
|
|
1259
1516
|
type: 'plugin-action',
|
|
1260
1517
|
status: receipt.effectOutcome === 'outcome_unknown'
|
|
@@ -1286,9 +1543,9 @@ function compatibilityJobFromReceipt(receipt: PluginActionReceipt, checkoutId: s
|
|
|
1286
1543
|
* Deterministic plugin action path: validate → authorize/confirm → invoke adapter → receipt.
|
|
1287
1544
|
* Does not create ExecutionJobs or call a model.
|
|
1288
1545
|
*/
|
|
1289
|
-
|
|
1546
|
+
async function submitPluginAction(
|
|
1290
1547
|
controllerHome: string,
|
|
1291
|
-
|
|
1548
|
+
scope: PluginActionScopeContext,
|
|
1292
1549
|
request: AssistantPluginActionRequest,
|
|
1293
1550
|
): Promise<{
|
|
1294
1551
|
manifest: AssistantPluginManifest;
|
|
@@ -1302,7 +1559,9 @@ export async function submitAssistantPluginAction(
|
|
|
1302
1559
|
}> {
|
|
1303
1560
|
const adapter = resolvePluginAdapter(controllerHome, request.pluginId);
|
|
1304
1561
|
if (!adapter) throw new Error(`PLUGIN_NOT_FOUND: ${request.pluginId}`);
|
|
1305
|
-
const manifest =
|
|
1562
|
+
const manifest = scope.kind === 'controller'
|
|
1563
|
+
? getControllerPluginManifest(controllerHome, request.pluginId)
|
|
1564
|
+
: getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
|
|
1306
1565
|
const action = actionForManifest(manifest, request.actionId);
|
|
1307
1566
|
if (!manifest.enabled && action.actionId !== 'configure') {
|
|
1308
1567
|
throw new Error(`PLUGIN_DISABLED: ${request.pluginId} is disabled`);
|
|
@@ -1313,36 +1572,38 @@ export async function submitAssistantPluginAction(
|
|
|
1313
1572
|
const normalizedArgs = validateActionArguments(action, request.args ?? {});
|
|
1314
1573
|
enforceConfirmation(action, { ...request, args: normalizedArgs });
|
|
1315
1574
|
|
|
1316
|
-
const key = semanticKey(
|
|
1575
|
+
const key = semanticKey(scope, request.pluginId, request.actionId, normalizedArgs);
|
|
1317
1576
|
const requestPath = pluginActionRequestPath(controllerHome, request.requestId);
|
|
1318
1577
|
if (existsSync(requestPath)) {
|
|
1319
1578
|
const index = readJsonFile<PluginActionRequestIndex>(requestPath);
|
|
1320
1579
|
if (index.semanticKey !== key) {
|
|
1321
1580
|
throw new Error(`REQUEST_ID_CONFLICT: ${request.requestId} already belongs to ${index.semanticKey}`);
|
|
1322
1581
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1582
|
+
const existingScopeKey = index.scopeKey?.trim() || index.repoId?.trim();
|
|
1583
|
+
if (!existingScopeKey) throw new Error(`PLUGIN_RECEIPT_REQUEST_SCOPE_CORRUPT: ${request.requestId}`);
|
|
1584
|
+
if (existingScopeKey !== scope.scopeKey) {
|
|
1585
|
+
throw new Error(`REQUEST_ID_SCOPE_CONFLICT: ${request.requestId} already belongs to ${existingScopeKey}`);
|
|
1325
1586
|
}
|
|
1326
|
-
const existing = readPluginActionReceipt(controllerHome,
|
|
1587
|
+
const existing = readPluginActionReceipt(controllerHome, existingScopeKey, index.receiptId);
|
|
1327
1588
|
if (!existing) {
|
|
1328
1589
|
throw new Error(`PLUGIN_RECEIPT_LOST: ${request.requestId}`);
|
|
1329
1590
|
}
|
|
1330
1591
|
if (request.workId && existing.workId !== request.workId) {
|
|
1331
1592
|
throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CONFLICT: ${request.requestId} already belongs to Work ${existing.workId ?? 'none'}`);
|
|
1332
1593
|
}
|
|
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 ??
|
|
1594
|
+
const requestedWorkRepoId = request.workId ? workAttributionRepoId(scope, request) : undefined;
|
|
1595
|
+
if (requestedWorkRepoId && index.workRepoId !== requestedWorkRepoId) {
|
|
1596
|
+
throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_CONFLICT: ${request.requestId} already belongs to Work repository ${index.workRepoId ?? 'none'}`);
|
|
1336
1597
|
}
|
|
1337
1598
|
if (request.workId && action.risk === 'remote_write') {
|
|
1338
|
-
bindRemoteEffectReceiptToWork(controllerHome,
|
|
1599
|
+
bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, existing);
|
|
1339
1600
|
} else if (request.workId) {
|
|
1340
|
-
bindLocalEffectReceiptToAttributedWork(controllerHome,
|
|
1601
|
+
bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, existing);
|
|
1341
1602
|
}
|
|
1342
1603
|
return {
|
|
1343
1604
|
manifest,
|
|
1344
1605
|
action,
|
|
1345
|
-
job: compatibilityJobFromReceipt(existing,
|
|
1606
|
+
job: compatibilityJobFromReceipt(existing, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
|
|
1346
1607
|
deduplicated: true,
|
|
1347
1608
|
result: existing.result,
|
|
1348
1609
|
receipt: existing,
|
|
@@ -1360,7 +1621,7 @@ export async function submitAssistantPluginAction(
|
|
|
1360
1621
|
if (requiresAutomatedWriteAuthorization(request.origin)) {
|
|
1361
1622
|
activeAuthorizationGrant = await resolveAutomatedWriteAuthorization({
|
|
1362
1623
|
controllerHome,
|
|
1363
|
-
|
|
1624
|
+
scope,
|
|
1364
1625
|
adapter,
|
|
1365
1626
|
manifest,
|
|
1366
1627
|
action,
|
|
@@ -1380,77 +1641,33 @@ export async function submitAssistantPluginAction(
|
|
|
1380
1641
|
};
|
|
1381
1642
|
}
|
|
1382
1643
|
}
|
|
1383
|
-
const requiresLocalEffectWork = localSystemActionRequiresWork(
|
|
1384
|
-
repository,
|
|
1385
|
-
request.pluginId,
|
|
1386
|
-
request.actionId,
|
|
1387
|
-
normalizedArgs,
|
|
1388
|
-
);
|
|
1389
1644
|
const boundRemoteWork = action.risk === 'remote_write'
|
|
1390
|
-
? remoteEffectWorkForPluginAction(controllerHome,
|
|
1645
|
+
? remoteEffectWorkForPluginAction(controllerHome, scope, action, request)
|
|
1391
1646
|
: undefined;
|
|
1392
|
-
|
|
1393
|
-
|
|
1647
|
+
// Capability execution never creates semantic Work implicitly. Existing Work
|
|
1648
|
+
// attribution is opt-in through work_id; otherwise the plugin receipt is the
|
|
1649
|
+
// durable effect record.
|
|
1650
|
+
const attributedWork = action.risk !== 'remote_write'
|
|
1651
|
+
? attributedWorkForPluginAction(controllerHome, scope, action, request)
|
|
1394
1652
|
: undefined;
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
|
1426
|
-
: undefined;
|
|
1427
|
-
if (acceptedWork) {
|
|
1428
|
-
activateWorkContract(
|
|
1429
|
-
{ controllerHome, repoId: repository.repoId },
|
|
1430
|
-
acceptedWork.workId,
|
|
1431
|
-
{
|
|
1432
|
-
phase: 'implementation',
|
|
1433
|
-
summary: `Controller-local plugin action ${request.pluginId}/${request.actionId} started.`,
|
|
1434
|
-
evidenceState: 'partial',
|
|
1653
|
+
if (scope.repository) {
|
|
1654
|
+
appendRuntimeEvent(controllerHome, {
|
|
1655
|
+
repoId: scope.repository.repoId,
|
|
1656
|
+
entityType: 'plugin',
|
|
1657
|
+
entityId: manifest.pluginId,
|
|
1658
|
+
eventType: 'plugin_action_requested',
|
|
1659
|
+
requestId: request.requestId,
|
|
1660
|
+
revision: manifest.revision,
|
|
1661
|
+
data: {
|
|
1662
|
+
actionId: action.actionId,
|
|
1663
|
+
receiptId,
|
|
1664
|
+
risk: action.risk,
|
|
1665
|
+
confirmation: action.confirmation,
|
|
1666
|
+
capabilityId,
|
|
1667
|
+
authorizationReuseSupported: action.confirmation === 'authorization' && Boolean(adapter.resolveAuthorizationContext),
|
|
1435
1668
|
},
|
|
1436
|
-
);
|
|
1669
|
+
});
|
|
1437
1670
|
}
|
|
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
1671
|
|
|
1455
1672
|
try {
|
|
1456
1673
|
if (action.confirmation === 'authorization'
|
|
@@ -1458,8 +1675,7 @@ export async function submitAssistantPluginAction(
|
|
|
1458
1675
|
&& originMayEstablishCapabilityAuthorization(request.origin)) {
|
|
1459
1676
|
authorizationContext = await adapter.resolveAuthorizationContext({
|
|
1460
1677
|
controllerHome,
|
|
1461
|
-
|
|
1462
|
-
repoRoot: repository.canonicalRoot,
|
|
1678
|
+
...pluginActionExecutionFields(scope),
|
|
1463
1679
|
pluginId: request.pluginId,
|
|
1464
1680
|
actionId: request.actionId,
|
|
1465
1681
|
requestId: request.requestId,
|
|
@@ -1479,7 +1695,7 @@ export async function submitAssistantPluginAction(
|
|
|
1479
1695
|
try {
|
|
1480
1696
|
activeAuthorizationGrant = findActivePluginCapabilityAuthorization(controllerHome, {
|
|
1481
1697
|
ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
|
|
1482
|
-
repoId:
|
|
1698
|
+
repoId: scope.grantRepoId,
|
|
1483
1699
|
pluginId: request.pluginId,
|
|
1484
1700
|
capabilityId,
|
|
1485
1701
|
target: authorizationContext.target,
|
|
@@ -1504,15 +1720,21 @@ export async function submitAssistantPluginAction(
|
|
|
1504
1720
|
}
|
|
1505
1721
|
}
|
|
1506
1722
|
|
|
1723
|
+
if (action.requiresExplicitCapabilityGrant && !activeAuthorizationGrant) {
|
|
1724
|
+
throw new AssistantPluginError(
|
|
1725
|
+
'PLUGIN_CAPABILITY_GRANT_REQUIRED',
|
|
1726
|
+
`${manifest.pluginId}/${action.actionId} requires an explicit reusable capability grant for its exact target. Run the dedicated authorization action first.`,
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1507
1730
|
const result = await withAssistantPluginResourceLeases(
|
|
1508
1731
|
controllerHome,
|
|
1509
|
-
|
|
1732
|
+
scope,
|
|
1510
1733
|
action,
|
|
1511
1734
|
request,
|
|
1512
|
-
() =>
|
|
1735
|
+
() => executePluginActionInScope(scope, {
|
|
1513
1736
|
controllerHome,
|
|
1514
|
-
|
|
1515
|
-
repoRoot: repository.canonicalRoot,
|
|
1737
|
+
...pluginActionExecutionFields(scope),
|
|
1516
1738
|
pluginId: request.pluginId,
|
|
1517
1739
|
actionId: request.actionId,
|
|
1518
1740
|
requestId: request.requestId,
|
|
@@ -1527,6 +1749,31 @@ export async function submitAssistantPluginAction(
|
|
|
1527
1749
|
: undefined,
|
|
1528
1750
|
}),
|
|
1529
1751
|
);
|
|
1752
|
+
// create_session materializes the exact provider/session identity during
|
|
1753
|
+
// execution, so its authorization target cannot always be resolved before
|
|
1754
|
+
// the action runs. Resolve that postcondition once after a successful
|
|
1755
|
+
// interactive bootstrap, then persist the same exact-target grant path as
|
|
1756
|
+
// every other authorization-class Browser action. Scheduled execution
|
|
1757
|
+
// still remains grant-only because this branch is interactive-origin only.
|
|
1758
|
+
if (action.actionId === 'create_session'
|
|
1759
|
+
&& action.confirmation === 'authorization'
|
|
1760
|
+
&& !authorizationContext
|
|
1761
|
+
&& originMayEstablishCapabilityAuthorization(request.origin)
|
|
1762
|
+
&& adapter.resolveAuthorizationContext) {
|
|
1763
|
+
authorizationContext = await adapter.resolveAuthorizationContext({
|
|
1764
|
+
controllerHome,
|
|
1765
|
+
...pluginActionExecutionFields(scope),
|
|
1766
|
+
pluginId: request.pluginId,
|
|
1767
|
+
actionId: request.actionId,
|
|
1768
|
+
requestId: request.requestId,
|
|
1769
|
+
args: normalizedArgs,
|
|
1770
|
+
origin: request.origin,
|
|
1771
|
+
jobId: receiptId,
|
|
1772
|
+
timeoutMs: request.timeoutMs,
|
|
1773
|
+
signal: request.signal,
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1530
1777
|
if (action.confirmation === 'authorization'
|
|
1531
1778
|
&& authorizationContext
|
|
1532
1779
|
&& !activeAuthorizationGrant
|
|
@@ -1534,7 +1781,7 @@ export async function submitAssistantPluginAction(
|
|
|
1534
1781
|
try {
|
|
1535
1782
|
const grant = recordPluginCapabilityAuthorization(controllerHome, {
|
|
1536
1783
|
ownerScope: pluginCapabilityAuthorizationOwnerScope(request.origin),
|
|
1537
|
-
repoId:
|
|
1784
|
+
repoId: scope.grantRepoId,
|
|
1538
1785
|
pluginId: request.pluginId,
|
|
1539
1786
|
capabilityId,
|
|
1540
1787
|
target: authorizationContext.target,
|
|
@@ -1568,42 +1815,8 @@ export async function submitAssistantPluginAction(
|
|
|
1568
1815
|
work: {
|
|
1569
1816
|
workId: boundRemoteWork.workId,
|
|
1570
1817
|
workKind: 'remote_effect',
|
|
1818
|
+
semanticState: 'open' as const,
|
|
1571
1819
|
...(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
1820
|
},
|
|
1608
1821
|
};
|
|
1609
1822
|
}
|
|
@@ -1611,48 +1824,50 @@ export async function submitAssistantPluginAction(
|
|
|
1611
1824
|
schemaVersion: 1,
|
|
1612
1825
|
receiptId,
|
|
1613
1826
|
requestId: request.requestId,
|
|
1614
|
-
|
|
1615
|
-
...(
|
|
1827
|
+
scopeKey: scope.scopeKey,
|
|
1828
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1829
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
|
|
1616
1830
|
pluginId: request.pluginId,
|
|
1617
1831
|
actionId: request.actionId,
|
|
1618
1832
|
semanticKey: key,
|
|
1619
1833
|
status: 'succeeded',
|
|
1620
1834
|
createdAt,
|
|
1621
|
-
...(
|
|
1622
|
-
? { workId:
|
|
1623
|
-
:
|
|
1624
|
-
? { workId:
|
|
1625
|
-
:
|
|
1626
|
-
? { workId: attributedWork.workId }
|
|
1627
|
-
: {}),
|
|
1835
|
+
...(boundRemoteWork
|
|
1836
|
+
? { workId: boundRemoteWork.workId }
|
|
1837
|
+
: attributedWork
|
|
1838
|
+
? { workId: attributedWork.workId }
|
|
1839
|
+
: {}),
|
|
1628
1840
|
origin: request.origin,
|
|
1629
1841
|
authorization,
|
|
1630
1842
|
result: resultWithLineage,
|
|
1631
1843
|
};
|
|
1632
|
-
writeJsonAtomic(pluginActionReceiptPath(controllerHome,
|
|
1844
|
+
writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
|
|
1633
1845
|
writeJsonAtomic(requestPath, {
|
|
1634
1846
|
requestId: request.requestId,
|
|
1635
|
-
|
|
1636
|
-
...(
|
|
1847
|
+
scopeKey: scope.scopeKey,
|
|
1848
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1849
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
|
|
1637
1850
|
receiptId,
|
|
1638
1851
|
semanticKey: key,
|
|
1639
1852
|
createdAt,
|
|
1640
1853
|
} satisfies PluginActionRequestIndex);
|
|
1641
1854
|
if (boundRemoteWork) {
|
|
1642
|
-
bindRemoteEffectReceiptToWork(controllerHome,
|
|
1855
|
+
bindRemoteEffectReceiptToWork(controllerHome, scope, action, request, receipt);
|
|
1643
1856
|
} else if (attributedWork) {
|
|
1644
|
-
bindLocalEffectReceiptToAttributedWork(controllerHome,
|
|
1857
|
+
bindLocalEffectReceiptToAttributedWork(controllerHome, scope, action, request, receipt, attributedWork);
|
|
1645
1858
|
}
|
|
1646
|
-
const nextManifest =
|
|
1859
|
+
const nextManifest = scope.kind === 'controller'
|
|
1860
|
+
? getControllerPluginManifest(controllerHome, request.pluginId)
|
|
1861
|
+
: getAssistantPluginManifest(controllerHome, scope.repository!, request.pluginId);
|
|
1647
1862
|
return {
|
|
1648
1863
|
manifest: nextManifest,
|
|
1649
1864
|
action,
|
|
1650
|
-
job: compatibilityJobFromReceipt(receipt,
|
|
1865
|
+
job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
|
|
1651
1866
|
deduplicated: false,
|
|
1652
1867
|
result: resultWithLineage,
|
|
1653
1868
|
receipt,
|
|
1654
1869
|
authorization,
|
|
1655
|
-
workId:
|
|
1870
|
+
workId: boundRemoteWork?.workId ?? attributedWork?.workId,
|
|
1656
1871
|
};
|
|
1657
1872
|
} catch (error) {
|
|
1658
1873
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1663,69 +1878,60 @@ export async function submitAssistantPluginAction(
|
|
|
1663
1878
|
? error.effectOutcome
|
|
1664
1879
|
: 'failed';
|
|
1665
1880
|
const outcomeUnknown = effectOutcome === 'outcome_unknown';
|
|
1881
|
+
const failure: NonNullable<PluginActionReceipt['error']> = {
|
|
1882
|
+
code,
|
|
1883
|
+
message: redactSensitiveText(message).text,
|
|
1884
|
+
...(isAssistantPluginError(error) ? { retryable: error.retryable } : {}),
|
|
1885
|
+
};
|
|
1886
|
+
// Error evidence stays compact; oversized provider diagnostics must not
|
|
1887
|
+
// turn failure settlement into an unbounded persistence write.
|
|
1888
|
+
if (isAssistantPluginError(error) && error.details) {
|
|
1889
|
+
try {
|
|
1890
|
+
const details = redactSensitiveValue(error.details).value;
|
|
1891
|
+
if (Buffer.byteLength(JSON.stringify(details), 'utf8') <= 8_192) failure.details = details;
|
|
1892
|
+
} catch {
|
|
1893
|
+
// Non-JSON provider diagnostics cannot override the settled failure.
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1666
1896
|
const outcomeResult = outcomeUnknown
|
|
1667
|
-
? { outcome: 'outcome_unknown', error:
|
|
1897
|
+
? { outcome: 'outcome_unknown', error: failure }
|
|
1668
1898
|
: undefined;
|
|
1669
1899
|
if (boundRemoteWork) {
|
|
1670
|
-
appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(
|
|
1900
|
+
appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(scope, request) }, boundRemoteWork.workId, {
|
|
1671
1901
|
title: outcomeUnknown ? 'typed remote plugin effect outcome unknown' : 'typed remote plugin effect failed',
|
|
1672
1902
|
summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
|
|
1673
1903
|
detailLevel: 'summary',
|
|
1674
1904
|
});
|
|
1675
1905
|
}
|
|
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
1906
|
const receipt: PluginActionReceipt = {
|
|
1701
1907
|
schemaVersion: 1,
|
|
1702
1908
|
receiptId,
|
|
1703
1909
|
requestId: request.requestId,
|
|
1704
|
-
|
|
1705
|
-
...(
|
|
1910
|
+
scopeKey: scope.scopeKey,
|
|
1911
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1912
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
|
|
1706
1913
|
pluginId: request.pluginId,
|
|
1707
1914
|
actionId: request.actionId,
|
|
1708
1915
|
semanticKey: key,
|
|
1709
1916
|
status: 'failed',
|
|
1710
1917
|
effectOutcome,
|
|
1711
1918
|
createdAt,
|
|
1712
|
-
...(
|
|
1713
|
-
? { workId:
|
|
1714
|
-
:
|
|
1715
|
-
? { workId:
|
|
1716
|
-
:
|
|
1717
|
-
? { workId: attributedWork.workId }
|
|
1718
|
-
: {}),
|
|
1919
|
+
...(boundRemoteWork
|
|
1920
|
+
? { workId: boundRemoteWork.workId }
|
|
1921
|
+
: attributedWork
|
|
1922
|
+
? { workId: attributedWork.workId }
|
|
1923
|
+
: {}),
|
|
1719
1924
|
origin: request.origin,
|
|
1720
1925
|
authorization,
|
|
1721
1926
|
...(outcomeResult ? { result: outcomeResult } : {}),
|
|
1722
|
-
error:
|
|
1927
|
+
error: failure,
|
|
1723
1928
|
};
|
|
1724
|
-
writeJsonAtomic(pluginActionReceiptPath(controllerHome,
|
|
1929
|
+
writeJsonAtomic(pluginActionReceiptPath(controllerHome, scope.scopeKey, receiptId), receipt);
|
|
1725
1930
|
writeJsonAtomic(requestPath, {
|
|
1726
1931
|
requestId: request.requestId,
|
|
1727
|
-
|
|
1728
|
-
...(
|
|
1932
|
+
scopeKey: scope.scopeKey,
|
|
1933
|
+
...(scope.repository ? { repoId: scope.repository.repoId } : {}),
|
|
1934
|
+
...(request.workId ? { workRepoId: workAttributionRepoId(scope, request) } : {}),
|
|
1729
1935
|
receiptId,
|
|
1730
1936
|
semanticKey: key,
|
|
1731
1937
|
createdAt,
|
|
@@ -1734,40 +1940,42 @@ export async function submitAssistantPluginAction(
|
|
|
1734
1940
|
return {
|
|
1735
1941
|
manifest,
|
|
1736
1942
|
action,
|
|
1737
|
-
job: compatibilityJobFromReceipt(receipt,
|
|
1943
|
+
job: compatibilityJobFromReceipt(receipt, scope.checkoutId ?? CONTROLLER_PLUGIN_SCOPE_KEY),
|
|
1738
1944
|
deduplicated: false,
|
|
1739
1945
|
result: outcomeResult,
|
|
1740
1946
|
receipt,
|
|
1741
1947
|
authorization,
|
|
1742
|
-
workId:
|
|
1948
|
+
workId: boundRemoteWork?.workId ?? attributedWork?.workId,
|
|
1743
1949
|
};
|
|
1744
1950
|
}
|
|
1745
1951
|
throw error;
|
|
1746
1952
|
}
|
|
1747
1953
|
}
|
|
1748
1954
|
|
|
1749
|
-
export
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
repoId: repository.repoId,
|
|
1756
|
-
repoRoot: repository.canonicalRoot,
|
|
1757
|
-
});
|
|
1955
|
+
export function submitAssistantPluginAction(
|
|
1956
|
+
controllerHome: string,
|
|
1957
|
+
repository: RepositoryRecord,
|
|
1958
|
+
request: AssistantPluginActionRequest,
|
|
1959
|
+
) {
|
|
1960
|
+
return submitPluginAction(controllerHome, repositoryPluginActionScope(repository), request);
|
|
1758
1961
|
}
|
|
1759
1962
|
|
|
1760
|
-
export
|
|
1963
|
+
export function submitControllerPluginAction(
|
|
1964
|
+
controllerHome: string,
|
|
1965
|
+
request: AssistantPluginActionRequest,
|
|
1966
|
+
) {
|
|
1967
|
+
return submitPluginAction(controllerHome, controllerPluginActionScope(controllerHome), request);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
async function executePluginActionInScope(
|
|
1971
|
+
scope: PluginActionScopeContext,
|
|
1761
1972
|
input: AssistantPluginActionExecutionInput,
|
|
1762
1973
|
): Promise<Record<string, unknown>> {
|
|
1763
1974
|
const adapter = resolvePluginAdapter(input.controllerHome, input.pluginId);
|
|
1764
1975
|
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);
|
|
1976
|
+
const manifestLookup = scope.kind === 'controller'
|
|
1977
|
+
? getControllerPluginManifestForExecution(input.controllerHome, input.pluginId, adapter)
|
|
1978
|
+
: getAssistantPluginManifestForExecution(input.controllerHome, scope.repository!, input.pluginId, adapter);
|
|
1771
1979
|
const manifest = manifestLookup.manifest;
|
|
1772
1980
|
const action = actionForManifest(manifest, input.actionId);
|
|
1773
1981
|
if (action.executionMode === 'direct_non_persistent') {
|
|
@@ -1777,7 +1985,7 @@ export async function executeAssistantPluginAction(
|
|
|
1777
1985
|
if (requiresAutomatedWriteAuthorization(input.origin)) {
|
|
1778
1986
|
await resolveAutomatedWriteAuthorization({
|
|
1779
1987
|
controllerHome: input.controllerHome,
|
|
1780
|
-
|
|
1988
|
+
scope,
|
|
1781
1989
|
adapter,
|
|
1782
1990
|
manifest,
|
|
1783
1991
|
action,
|
|
@@ -1791,38 +1999,53 @@ export async function executeAssistantPluginAction(
|
|
|
1791
1999
|
try {
|
|
1792
2000
|
const result = await adapter.executeAction({
|
|
1793
2001
|
...input,
|
|
2002
|
+
...pluginActionExecutionFields(scope),
|
|
1794
2003
|
args: normalizedArgs,
|
|
1795
2004
|
providerIdentityPrevalidated: manifestLookup.providerIdentityPrevalidated,
|
|
1796
2005
|
});
|
|
1797
2006
|
for (const affectedPluginId of adapter.affectedPluginIdsAfterAction?.(input.actionId, result) ?? []) {
|
|
1798
2007
|
if (!affectedPluginId || affectedPluginId === input.pluginId) continue;
|
|
1799
|
-
invalidateAssistantPluginManifestCache(input.controllerHome, repository.repoId, affectedPluginId);
|
|
1800
2008
|
const affectedAdapter = resolvePluginAdapter(input.controllerHome, affectedPluginId);
|
|
1801
|
-
if (
|
|
1802
|
-
|
|
2009
|
+
if (scope.kind === 'controller') {
|
|
2010
|
+
invalidateAssistantPluginManifestCache(input.controllerHome, CONTROLLER_PLUGIN_CACHE_SCOPE, affectedPluginId);
|
|
2011
|
+
if (affectedAdapter && adapterMatchesController(affectedAdapter)) {
|
|
2012
|
+
syncControllerPluginManifest(input.controllerHome, affectedPluginId);
|
|
2013
|
+
} else {
|
|
2014
|
+
removeControllerPluginManifestProjection(input.controllerHome, affectedPluginId);
|
|
2015
|
+
}
|
|
1803
2016
|
} else {
|
|
1804
|
-
|
|
2017
|
+
const repository = scope.repository!;
|
|
2018
|
+
invalidateAssistantPluginManifestCache(input.controllerHome, repository.repoId, affectedPluginId);
|
|
2019
|
+
if (affectedAdapter && adapterMatchesRepository(affectedAdapter, repository)) {
|
|
2020
|
+
syncAssistantPluginManifest(input.controllerHome, repository, affectedPluginId);
|
|
2021
|
+
} else {
|
|
2022
|
+
rmSync(manifestPath(input.controllerHome, repository.repoId, affectedPluginId), { force: true });
|
|
2023
|
+
}
|
|
1805
2024
|
}
|
|
1806
2025
|
}
|
|
1807
2026
|
const refreshManifest = adapter.shouldRefreshManifestAfterAction?.(input.actionId) ?? true;
|
|
1808
2027
|
const nextManifest = refreshManifest
|
|
1809
|
-
?
|
|
2028
|
+
? scope.kind === 'controller'
|
|
2029
|
+
? syncControllerPluginManifest(input.controllerHome, input.pluginId).manifest
|
|
2030
|
+
: syncAssistantPluginManifest(input.controllerHome, scope.repository!, input.pluginId).manifest
|
|
1810
2031
|
: manifest;
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2032
|
+
if (scope.repository) {
|
|
2033
|
+
appendRuntimeEvent(input.controllerHome, {
|
|
2034
|
+
repoId: scope.repository.repoId,
|
|
2035
|
+
entityType: 'plugin',
|
|
2036
|
+
entityId: input.pluginId,
|
|
2037
|
+
eventType: 'plugin_action_succeeded',
|
|
2038
|
+
requestId: input.requestId,
|
|
2039
|
+
revision: nextManifest.revision,
|
|
2040
|
+
data: {
|
|
2041
|
+
actionId: input.actionId,
|
|
2042
|
+
jobId: input.jobId,
|
|
2043
|
+
resultKeys: Object.keys(result).slice(0, 20),
|
|
2044
|
+
lifecycleState: nextManifest.lifecycle.state,
|
|
2045
|
+
healthState: nextManifest.health.state,
|
|
2046
|
+
},
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
1826
2049
|
return {
|
|
1827
2050
|
schemaVersion: 1,
|
|
1828
2051
|
plugin: {
|
|
@@ -1850,25 +2073,49 @@ export async function executeAssistantPluginAction(
|
|
|
1850
2073
|
actionId: input.actionId,
|
|
1851
2074
|
},
|
|
1852
2075
|
});
|
|
1853
|
-
const
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
2076
|
+
const nextManifest = scope.kind === 'controller'
|
|
2077
|
+
? syncControllerPluginManifest(input.controllerHome, input.pluginId).manifest
|
|
2078
|
+
: syncAssistantPluginManifest(input.controllerHome, scope.repository!, input.pluginId).manifest;
|
|
2079
|
+
if (scope.repository) {
|
|
2080
|
+
appendRuntimeEvent(input.controllerHome, {
|
|
2081
|
+
repoId: scope.repository.repoId,
|
|
2082
|
+
entityType: 'plugin',
|
|
2083
|
+
entityId: input.pluginId,
|
|
2084
|
+
eventType: 'plugin_action_failed',
|
|
2085
|
+
requestId: input.requestId,
|
|
2086
|
+
revision: nextManifest.revision,
|
|
2087
|
+
data: {
|
|
2088
|
+
actionId: input.actionId,
|
|
2089
|
+
jobId: input.jobId,
|
|
2090
|
+
code: pluginError.code,
|
|
2091
|
+
retryable: pluginError.retryable,
|
|
2092
|
+
},
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
1869
2095
|
throw new AssistantPluginError(pluginError.code, pluginError.message.replace(/^[^:]+:\s*/, ''), {
|
|
1870
2096
|
retryable: pluginError.retryable,
|
|
1871
2097
|
details: pluginError.details,
|
|
1872
2098
|
});
|
|
1873
2099
|
}
|
|
1874
2100
|
}
|
|
2101
|
+
|
|
2102
|
+
export function executeControllerScopedPluginAction(
|
|
2103
|
+
input: Omit<AssistantPluginActionExecutionInput, 'repoId' | 'repoRoot'>,
|
|
2104
|
+
): Promise<Record<string, unknown>> {
|
|
2105
|
+
const scope = controllerPluginActionScope(input.controllerHome);
|
|
2106
|
+
return executePluginActionInScope(scope, {
|
|
2107
|
+
...input,
|
|
2108
|
+
...pluginActionExecutionFields(scope),
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
export function executeAssistantPluginAction(
|
|
2113
|
+
input: AssistantPluginActionExecutionInput,
|
|
2114
|
+
): Promise<Record<string, unknown>> {
|
|
2115
|
+
const repository = {
|
|
2116
|
+
repoId: input.repoId,
|
|
2117
|
+
canonicalRoot: input.repoRoot,
|
|
2118
|
+
activeCheckoutId: 'active',
|
|
2119
|
+
} as RepositoryRecord;
|
|
2120
|
+
return executePluginActionInScope(repositoryPluginActionScope(repository), input);
|
|
2121
|
+
}
|