ws-spec-kit 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +79 -0
- package/dist/adapters/cli/output.d.ts +15 -0
- package/dist/adapters/cli/output.js +21 -0
- package/dist/adapters/cli/output.js.map +1 -0
- package/dist/adapters/cli/workflow.d.ts +25 -0
- package/dist/adapters/cli/workflow.js +131 -0
- package/dist/adapters/cli/workflow.js.map +1 -0
- package/dist/adapters/connectors/git-native.d.ts +23 -0
- package/dist/adapters/connectors/git-native.js +521 -0
- package/dist/adapters/connectors/git-native.js.map +1 -0
- package/dist/adapters/connectors/github-cli.d.ts +23 -0
- package/dist/adapters/connectors/github-cli.js +213 -0
- package/dist/adapters/connectors/github-cli.js.map +1 -0
- package/dist/adapters/connectors/gitlab-cli.d.ts +23 -0
- package/dist/adapters/connectors/gitlab-cli.js +199 -0
- package/dist/adapters/connectors/gitlab-cli.js.map +1 -0
- package/dist/adapters/connectors/lark-cli.d.ts +24 -0
- package/dist/adapters/connectors/lark-cli.js +296 -0
- package/dist/adapters/connectors/lark-cli.js.map +1 -0
- package/dist/adapters/process/redaction.d.ts +2 -0
- package/dist/adapters/process/redaction.js +51 -0
- package/dist/adapters/process/redaction.js.map +1 -0
- package/dist/adapters/process/spawn-json.d.ts +37 -0
- package/dist/adapters/process/spawn-json.js +312 -0
- package/dist/adapters/process/spawn-json.js.map +1 -0
- package/dist/adapters/skills/claude.d.ts +1 -0
- package/dist/adapters/skills/claude.js +3 -0
- package/dist/adapters/skills/claude.js.map +1 -0
- package/dist/adapters/skills/codex.d.ts +1 -0
- package/dist/adapters/skills/codex.js +3 -0
- package/dist/adapters/skills/codex.js.map +1 -0
- package/dist/adapters/skills/cursor.d.ts +1 -0
- package/dist/adapters/skills/cursor.js +3 -0
- package/dist/adapters/skills/cursor.js.map +1 -0
- package/dist/adapters/skills/generic.d.ts +1 -0
- package/dist/adapters/skills/generic.js +2 -0
- package/dist/adapters/skills/generic.js.map +1 -0
- package/dist/adapters/skills/install.d.ts +28 -0
- package/dist/adapters/skills/install.js +571 -0
- package/dist/adapters/skills/install.js.map +1 -0
- package/dist/application/acquire.d.ts +60 -0
- package/dist/application/acquire.js +964 -0
- package/dist/application/acquire.js.map +1 -0
- package/dist/application/application.d.ts +21 -0
- package/dist/application/application.js +31 -0
- package/dist/application/application.js.map +1 -0
- package/dist/application/artifact-reader.d.ts +1 -0
- package/dist/application/artifact-reader.js +150 -0
- package/dist/application/artifact-reader.js.map +1 -0
- package/dist/application/artifact-writer.d.ts +1 -0
- package/dist/application/artifact-writer.js +268 -0
- package/dist/application/artifact-writer.js.map +1 -0
- package/dist/application/artifact.d.ts +28 -0
- package/dist/application/artifact.js +523 -0
- package/dist/application/artifact.js.map +1 -0
- package/dist/application/decide.d.ts +12 -0
- package/dist/application/decide.js +168 -0
- package/dist/application/decide.js.map +1 -0
- package/dist/application/doctor-connectors.d.ts +34 -0
- package/dist/application/doctor-connectors.js +99 -0
- package/dist/application/doctor-connectors.js.map +1 -0
- package/dist/application/external-action.d.ts +164 -0
- package/dist/application/external-action.js +670 -0
- package/dist/application/external-action.js.map +1 -0
- package/dist/application/initial-stages.d.ts +3 -0
- package/dist/application/initial-stages.js +7 -0
- package/dist/application/initial-stages.js.map +1 -0
- package/dist/application/inspect.d.ts +2 -0
- package/dist/application/inspect.js +31 -0
- package/dist/application/inspect.js.map +1 -0
- package/dist/application/profile.d.ts +32 -0
- package/dist/application/profile.js +70 -0
- package/dist/application/profile.js.map +1 -0
- package/dist/application/snapshot.d.ts +75 -0
- package/dist/application/snapshot.js +338 -0
- package/dist/application/snapshot.js.map +1 -0
- package/dist/application/start.d.ts +43 -0
- package/dist/application/start.js +360 -0
- package/dist/application/start.js.map +1 -0
- package/dist/application/state.d.ts +17 -0
- package/dist/application/state.js +94 -0
- package/dist/application/state.js.map +1 -0
- package/dist/application/submit.d.ts +18 -0
- package/dist/application/submit.js +1090 -0
- package/dist/application/submit.js.map +1 -0
- package/dist/cli/commands/core.d.ts +2 -0
- package/dist/cli/commands/core.js +205 -0
- package/dist/cli/commands/core.js.map +1 -0
- package/dist/cli/commands/public-contract.d.ts +12 -0
- package/dist/cli/commands/public-contract.js +47 -0
- package/dist/cli/commands/public-contract.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +23 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/domain/artifacts.d.ts +52 -0
- package/dist/domain/artifacts.js +241 -0
- package/dist/domain/artifacts.js.map +1 -0
- package/dist/domain/digests.d.ts +11 -0
- package/dist/domain/digests.js +106 -0
- package/dist/domain/digests.js.map +1 -0
- package/dist/domain/external-receipt.d.ts +48 -0
- package/dist/domain/external-receipt.js +138 -0
- package/dist/domain/external-receipt.js.map +1 -0
- package/dist/domain/ids.d.ts +7 -0
- package/dist/domain/ids.js +5 -0
- package/dist/domain/ids.js.map +1 -0
- package/dist/domain/repository-path.d.ts +3 -0
- package/dist/domain/repository-path.js +87 -0
- package/dist/domain/repository-path.js.map +1 -0
- package/dist/domain/states.d.ts +38 -0
- package/dist/domain/states.js +69 -0
- package/dist/domain/states.js.map +1 -0
- package/dist/domain/work-package-identity.d.ts +2 -0
- package/dist/domain/work-package-identity.js +10 -0
- package/dist/domain/work-package-identity.js.map +1 -0
- package/dist/domain/workflow.d.ts +58 -0
- package/dist/domain/workflow.js +2 -0
- package/dist/domain/workflow.js.map +1 -0
- package/dist/engine/actor-roles.d.ts +15 -0
- package/dist/engine/actor-roles.js +53 -0
- package/dist/engine/actor-roles.js.map +1 -0
- package/dist/engine/approvals.d.ts +44 -0
- package/dist/engine/approvals.js +239 -0
- package/dist/engine/approvals.js.map +1 -0
- package/dist/engine/archive.d.ts +24 -0
- package/dist/engine/archive.js +442 -0
- package/dist/engine/archive.js.map +1 -0
- package/dist/engine/compiler.d.ts +25 -0
- package/dist/engine/compiler.js +828 -0
- package/dist/engine/compiler.js.map +1 -0
- package/dist/engine/control/condition.d.ts +10 -0
- package/dist/engine/control/condition.js +49 -0
- package/dist/engine/control/condition.js.map +1 -0
- package/dist/engine/control/loop.d.ts +23 -0
- package/dist/engine/control/loop.js +96 -0
- package/dist/engine/control/loop.js.map +1 -0
- package/dist/engine/control/retry.d.ts +15 -0
- package/dist/engine/control/retry.js +54 -0
- package/dist/engine/control/retry.js.map +1 -0
- package/dist/engine/docs-integrity.d.ts +13 -0
- package/dist/engine/docs-integrity.js +34 -0
- package/dist/engine/docs-integrity.js.map +1 -0
- package/dist/engine/expressions/ast.d.ts +18 -0
- package/dist/engine/expressions/ast.js +9 -0
- package/dist/engine/expressions/ast.js.map +1 -0
- package/dist/engine/expressions/evaluate.d.ts +7 -0
- package/dist/engine/expressions/evaluate.js +47 -0
- package/dist/engine/expressions/evaluate.js.map +1 -0
- package/dist/engine/expressions/parser.d.ts +2 -0
- package/dist/engine/expressions/parser.js +173 -0
- package/dist/engine/expressions/parser.js.map +1 -0
- package/dist/engine/external-effects/authorization.d.ts +146 -0
- package/dist/engine/external-effects/authorization.js +222 -0
- package/dist/engine/external-effects/authorization.js.map +1 -0
- package/dist/engine/external-effects/idempotency.d.ts +14 -0
- package/dist/engine/external-effects/idempotency.js +26 -0
- package/dist/engine/external-effects/idempotency.js.map +1 -0
- package/dist/engine/external-effects/reconciliation.d.ts +11 -0
- package/dist/engine/external-effects/reconciliation.js +18 -0
- package/dist/engine/external-effects/reconciliation.js.map +1 -0
- package/dist/engine/results.d.ts +14 -0
- package/dist/engine/results.js +88 -0
- package/dist/engine/results.js.map +1 -0
- package/dist/engine/scheduler.d.ts +60 -0
- package/dist/engine/scheduler.js +225 -0
- package/dist/engine/scheduler.js.map +1 -0
- package/dist/engine/tdd/green-gate.d.ts +6 -0
- package/dist/engine/tdd/green-gate.js +72 -0
- package/dist/engine/tdd/green-gate.js.map +1 -0
- package/dist/engine/tdd/red-gate.d.ts +30 -0
- package/dist/engine/tdd/red-gate.js +531 -0
- package/dist/engine/tdd/red-gate.js.map +1 -0
- package/dist/engine/tdd/types.d.ts +89 -0
- package/dist/engine/tdd/types.js +38 -0
- package/dist/engine/tdd/types.js.map +1 -0
- package/dist/engine/verification.d.ts +56 -0
- package/dist/engine/verification.js +196 -0
- package/dist/engine/verification.js.map +1 -0
- package/dist/policy/profile.d.ts +24 -0
- package/dist/policy/profile.js +23 -0
- package/dist/policy/profile.js.map +1 -0
- package/dist/policy/risk.d.ts +33 -0
- package/dist/policy/risk.js +138 -0
- package/dist/policy/risk.js.map +1 -0
- package/dist/protocol/application.d.ts +196 -0
- package/dist/protocol/application.js +2 -0
- package/dist/protocol/application.js.map +1 -0
- package/dist/protocol/public-contract.d.ts +81 -0
- package/dist/protocol/public-contract.js +231 -0
- package/dist/protocol/public-contract.js.map +1 -0
- package/dist/protocol/work-package.d.ts +55 -0
- package/dist/protocol/work-package.js +2 -0
- package/dist/protocol/work-package.js.map +1 -0
- package/dist/registry/connectors/external-executor.d.ts +4 -0
- package/dist/registry/connectors/external-executor.js +249 -0
- package/dist/registry/connectors/external-executor.js.map +1 -0
- package/dist/registry/connectors/feishu-document.d.ts +21 -0
- package/dist/registry/connectors/feishu-document.js +197 -0
- package/dist/registry/connectors/feishu-document.js.map +1 -0
- package/dist/registry/connectors/git-commit.d.ts +32 -0
- package/dist/registry/connectors/git-commit.js +122 -0
- package/dist/registry/connectors/git-commit.js.map +1 -0
- package/dist/registry/connectors/issue.d.ts +68 -0
- package/dist/registry/connectors/issue.js +301 -0
- package/dist/registry/connectors/issue.js.map +1 -0
- package/dist/registry/connectors/knowledge-publish.d.ts +30 -0
- package/dist/registry/connectors/knowledge-publish.js +157 -0
- package/dist/registry/connectors/knowledge-publish.js.map +1 -0
- package/dist/registry/connectors/local-requirement.d.ts +19 -0
- package/dist/registry/connectors/local-requirement.js +177 -0
- package/dist/registry/connectors/local-requirement.js.map +1 -0
- package/dist/registry/connectors/manifest.d.ts +6 -0
- package/dist/registry/connectors/manifest.js +164 -0
- package/dist/registry/connectors/manifest.js.map +1 -0
- package/dist/registry/connectors/registry.d.ts +11 -0
- package/dist/registry/connectors/registry.js +34 -0
- package/dist/registry/connectors/registry.js.map +1 -0
- package/dist/registry/connectors/requirement-source.d.ts +46 -0
- package/dist/registry/connectors/requirement-source.js +513 -0
- package/dist/registry/connectors/requirement-source.js.map +1 -0
- package/dist/registry/connectors/runtime.d.ts +38 -0
- package/dist/registry/connectors/runtime.js +127 -0
- package/dist/registry/connectors/runtime.js.map +1 -0
- package/dist/registry/connectors/secret-detector.d.ts +19 -0
- package/dist/registry/connectors/secret-detector.js +85 -0
- package/dist/registry/connectors/secret-detector.js.map +1 -0
- package/dist/registry/connectors/semver.d.ts +8 -0
- package/dist/registry/connectors/semver.js +57 -0
- package/dist/registry/connectors/semver.js.map +1 -0
- package/dist/registry/connectors/types.d.ts +45 -0
- package/dist/registry/connectors/types.js +2 -0
- package/dist/registry/connectors/types.js.map +1 -0
- package/dist/registry/executors/registry.d.ts +15 -0
- package/dist/registry/executors/registry.js +68 -0
- package/dist/registry/executors/registry.js.map +1 -0
- package/dist/registry/executors/types.d.ts +19 -0
- package/dist/registry/executors/types.js +2 -0
- package/dist/registry/executors/types.js.map +1 -0
- package/dist/registry/skills/lock.d.ts +3 -0
- package/dist/registry/skills/lock.js +278 -0
- package/dist/registry/skills/lock.js.map +1 -0
- package/dist/registry/skills/resolver.d.ts +16 -0
- package/dist/registry/skills/resolver.js +353 -0
- package/dist/registry/skills/resolver.js.map +1 -0
- package/dist/registry/skills/types.d.ts +75 -0
- package/dist/registry/skills/types.js +9 -0
- package/dist/registry/skills/types.js.map +1 -0
- package/dist/resources/catalog.d.ts +20 -0
- package/dist/resources/catalog.js +111 -0
- package/dist/resources/catalog.js.map +1 -0
- package/dist/schemas/definitions.d.ts +5 -0
- package/dist/schemas/definitions.js +958 -0
- package/dist/schemas/definitions.js.map +1 -0
- package/dist/schemas/generate.d.ts +1 -0
- package/dist/schemas/generate.js +26 -0
- package/dist/schemas/generate.js.map +1 -0
- package/dist/schemas/index.d.ts +11 -0
- package/dist/schemas/index.js +54 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/storage/control-plane.d.ts +136 -0
- package/dist/storage/control-plane.js +688 -0
- package/dist/storage/control-plane.js.map +1 -0
- package/dist/storage/events.d.ts +36 -0
- package/dist/storage/events.js +185 -0
- package/dist/storage/events.js.map +1 -0
- package/dist/storage/external-action-payload.d.ts +33 -0
- package/dist/storage/external-action-payload.js +190 -0
- package/dist/storage/external-action-payload.js.map +1 -0
- package/dist/storage/files.d.ts +13 -0
- package/dist/storage/files.js +45 -0
- package/dist/storage/files.js.map +1 -0
- package/dist/storage/git.d.ts +4 -0
- package/dist/storage/git.js +21 -0
- package/dist/storage/git.js.map +1 -0
- package/dist/storage/project-config.d.ts +11 -0
- package/dist/storage/project-config.js +60 -0
- package/dist/storage/project-config.js.map +1 -0
- package/dist/storage/repository.d.ts +14 -0
- package/dist/storage/repository.js +151 -0
- package/dist/storage/repository.js.map +1 -0
- package/dist/storage/work-items.d.ts +77 -0
- package/dist/storage/work-items.js +373 -0
- package/dist/storage/work-items.js.map +1 -0
- package/dist/storage/workflow-trust.d.ts +38 -0
- package/dist/storage/workflow-trust.js +310 -0
- package/dist/storage/workflow-trust.js.map +1 -0
- package/dist/workflow-package/loader.d.ts +12 -0
- package/dist/workflow-package/loader.js +249 -0
- package/dist/workflow-package/loader.js.map +1 -0
- package/dist/workflow-package/lock.d.ts +6 -0
- package/dist/workflow-package/lock.js +12 -0
- package/dist/workflow-package/lock.js.map +1 -0
- package/dist/workflow-package/path-boundary.d.ts +5 -0
- package/dist/workflow-package/path-boundary.js +38 -0
- package/dist/workflow-package/path-boundary.js.map +1 -0
- package/dist/workflow-package/trust.d.ts +27 -0
- package/dist/workflow-package/trust.js +61 -0
- package/dist/workflow-package/trust.js.map +1 -0
- package/dist/workflow-package/types.d.ts +176 -0
- package/dist/workflow-package/types.js +9 -0
- package/dist/workflow-package/types.js.map +1 -0
- package/dist/workflow-package/workflow-v1.d.ts +3 -0
- package/dist/workflow-package/workflow-v1.js +274 -0
- package/dist/workflow-package/workflow-v1.js.map +1 -0
- package/package.json +40 -0
- package/resources/catalog.yaml +37 -0
- package/resources/connectors/github-cli.yaml +24 -0
- package/resources/connectors/gitlab-cli.yaml +24 -0
- package/resources/connectors/lark-cli.yaml +28 -0
- package/resources/skills/bug-fixing/SKILL.md +16 -0
- package/resources/skills/code-review/SKILL.md +16 -0
- package/resources/skills/documentation-editing/SKILL.md +16 -0
- package/resources/skills/documentation-exploration/SKILL.md +16 -0
- package/resources/skills/documentation-review/SKILL.md +16 -0
- package/resources/skills/requirement-exploration/SKILL.md +16 -0
- package/resources/skills/solution-design/SKILL.md +16 -0
- package/resources/skills/task-planning/SKILL.md +16 -0
- package/resources/skills/tdd-implementation/SKILL.md +16 -0
- package/resources/workflows/documentation-delivery/manifest.yaml +8 -0
- package/resources/workflows/documentation-delivery/profiles/governed.yaml +23 -0
- package/resources/workflows/documentation-delivery/profiles/quick.yaml +23 -0
- package/resources/workflows/documentation-delivery/profiles/standard.yaml +21 -0
- package/resources/workflows/documentation-delivery/workflow.yaml +109 -0
- package/resources/workflows/feature-delivery/manifest.yaml +8 -0
- package/resources/workflows/feature-delivery/profiles/governed.yaml +27 -0
- package/resources/workflows/feature-delivery/profiles/quick.yaml +25 -0
- package/resources/workflows/feature-delivery/profiles/standard.yaml +26 -0
- package/resources/workflows/feature-delivery/workflow.yaml +150 -0
- package/schemas/builtin-agent-action-v1.schema.json +509 -0
- package/schemas/builtin-application-acquire-input-v1.schema.json +25 -0
- package/schemas/builtin-application-artifact-create-input-v1.schema.json +49 -0
- package/schemas/builtin-application-decision-input-v1.schema.json +284 -0
- package/schemas/builtin-application-inspect-input-v1.schema.json +20 -0
- package/schemas/builtin-application-project-config-snapshot-v1.schema.json +289 -0
- package/schemas/builtin-application-project-config-v1.schema.json +293 -0
- package/schemas/builtin-application-start-input-v1.schema.json +92 -0
- package/schemas/builtin-application-submit-input-v1.schema.json +145 -0
- package/schemas/builtin-artifact-v1.schema.json +48 -0
- package/schemas/builtin-evidence-v1.schema.json +65 -0
- package/schemas/builtin-external-action-grant-v1.schema.json +298 -0
- package/schemas/builtin-external-action-request-v1.schema.json +288 -0
- package/schemas/builtin-external-binding-v1.schema.json +59 -0
- package/schemas/builtin-external-receipt-v1.schema.json +109 -0
- package/schemas/builtin-external-write-receipt-v1.schema.json +253 -0
- package/schemas/builtin-source-artifact-v1.schema.json +293 -0
- package/schemas/builtin-submit-result-v1.schema.json +111 -0
- package/schemas/builtin-tdd-cycle-evidence-v1.schema.json +119 -0
- package/schemas/builtin-tdd-node-test-report-v1.schema.json +103 -0
- package/schemas/builtin-tdd-trusted-evidence-v1.schema.json +184 -0
- package/schemas/builtin-work-item-v1.schema.json +201 -0
- package/schemas/builtin-work-package-v1.schema.json +254 -0
- package/schemas/builtin-workflow-selection-v1.schema.json +40 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { computeWorkspaceTreeDigest } from "../domain/digests.js";
|
|
2
|
+
import { assertGrantAuthorizes, createExternalActionGrant, createExternalActionRequest, ExternalAuthorizationError, transitionExternalAction, } from "../engine/external-effects/authorization.js";
|
|
3
|
+
import { canonicalDigest, externalIdempotencyKey, ExternalIdempotencyError } from "../engine/external-effects/idempotency.js";
|
|
4
|
+
import { ControlPlaneError, mutateControlPlane } from "../engine/scheduler.js";
|
|
5
|
+
import { readControlPlane } from "../storage/control-plane.js";
|
|
6
|
+
import { readEvents, withControlPlaneLock } from "../storage/events.js";
|
|
7
|
+
import { persistExternalActionPayload, prepareExternalActionPayload, removeExternalActionPayload, } from "../storage/external-action-payload.js";
|
|
8
|
+
import { loadApplicationState, selectedProfile } from "./state.js";
|
|
9
|
+
import { validate } from "../schemas/index.js";
|
|
10
|
+
import { inspectDecodedCredentialSurface } from "../registry/connectors/secret-detector.js";
|
|
11
|
+
export { ExternalAuthorizationError as ExternalActionError } from "../engine/external-effects/authorization.js";
|
|
12
|
+
export function externalActionRejectionKey(requestId) {
|
|
13
|
+
return `external-rejection:${requestId}`;
|
|
14
|
+
}
|
|
15
|
+
function asExternalError(error) {
|
|
16
|
+
if (error instanceof ExternalAuthorizationError)
|
|
17
|
+
throw error;
|
|
18
|
+
if (error instanceof ExternalIdempotencyError) {
|
|
19
|
+
throw new ExternalAuthorizationError(error.code, error.message.replace(/^WSSPEC_[A-Z_]+:\s*/u, ""));
|
|
20
|
+
}
|
|
21
|
+
if (error instanceof ControlPlaneError && error.code === "WSSPEC_IDEMPOTENCY_CONFLICT") {
|
|
22
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_IDEMPOTENCY_CONFLICT", "外部幂等键已绑定不同请求。");
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
function currentAction(projection, requestId) {
|
|
27
|
+
const action = projection.externalActions[requestId];
|
|
28
|
+
if (action === undefined)
|
|
29
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_REQUEST_NOT_FOUND", "外部动作请求不存在。");
|
|
30
|
+
return action;
|
|
31
|
+
}
|
|
32
|
+
function assertActiveAttempt(projection, input, at) {
|
|
33
|
+
const claim = projection.claims[input.stepId];
|
|
34
|
+
const context = projection.contexts[input.stepId];
|
|
35
|
+
if (claim?.attemptId !== input.attemptId || claim.stageId !== input.stepId
|
|
36
|
+
|| context?.workPackage?.stepId !== input.stepId || context.workPackage.attemptId !== input.attemptId
|
|
37
|
+
|| context.workPackage.lease?.token !== claim.claimToken || new Date(claim.expiresAt) <= at) {
|
|
38
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_ATTEMPT_MISMATCH", "外部动作未绑定当前未过期的活动 Attempt/Lease。");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function assertCurrentExternalActionContext(state, projection, request) {
|
|
42
|
+
const profile = state.snapshot.profiles[projection.profile.selected];
|
|
43
|
+
const workspaceDigest = await computeWorkspaceTreeDigest(state.worktree);
|
|
44
|
+
if (profile === undefined || request.profile !== profile.id
|
|
45
|
+
|| request.profileDigest !== canonicalDigest(profile, "WSSPEC_EXTERNAL_GRANT_MISMATCH")
|
|
46
|
+
|| request.workspaceDigest !== workspaceDigest
|
|
47
|
+
|| request.configDigest !== state.item.execution.configDigest) {
|
|
48
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_GRANT_MISMATCH", "外部动作授权上下文已变化,必须重新准备请求并审批。");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function externalActionApprovalSummary(request) {
|
|
52
|
+
return {
|
|
53
|
+
provider: request.provider,
|
|
54
|
+
action: request.action,
|
|
55
|
+
target: { ...request.target },
|
|
56
|
+
digest: request.payloadDigest,
|
|
57
|
+
sideEffects: [...request.sideEffects],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function actionsReferencePayload(actions, digest) {
|
|
61
|
+
return actions !== undefined && Object.values(actions)
|
|
62
|
+
.some((action) => action.request.payloadArtifactDigest === digest);
|
|
63
|
+
}
|
|
64
|
+
async function removeCreatedPayloadIfUnreferenced(input) {
|
|
65
|
+
await withControlPlaneLock(input.controlPlane, async () => {
|
|
66
|
+
const projection = await readControlPlane(input.root, input.workItemId);
|
|
67
|
+
if (actionsReferencePayload(projection.externalActions, input.digest))
|
|
68
|
+
return;
|
|
69
|
+
const events = await readEvents(input.controlPlane);
|
|
70
|
+
const referencedByEvent = events.some((event) => {
|
|
71
|
+
const result = event.result;
|
|
72
|
+
return actionsReferencePayload(result?.projection?.externalActions, input.digest);
|
|
73
|
+
});
|
|
74
|
+
if (!referencedByEvent)
|
|
75
|
+
await removeExternalActionPayload({ controlPlane: input.controlPlane, digest: input.digest });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function prepareExternalAction(input) {
|
|
79
|
+
let createdPayload;
|
|
80
|
+
try {
|
|
81
|
+
const state = await loadApplicationState(input.root, input.workItemId);
|
|
82
|
+
const profile = selectedProfile(state.snapshot);
|
|
83
|
+
const payloadArtifact = prepareExternalActionPayload({
|
|
84
|
+
workItemId: input.workItemId,
|
|
85
|
+
stepId: input.stepId,
|
|
86
|
+
attemptId: input.attemptId,
|
|
87
|
+
provider: input.provider,
|
|
88
|
+
action: input.action,
|
|
89
|
+
target: input.target,
|
|
90
|
+
payload: input.payload,
|
|
91
|
+
});
|
|
92
|
+
const request = validate("builtin.external-action-request.v1", createExternalActionRequest({
|
|
93
|
+
version: 1,
|
|
94
|
+
workItemId: input.workItemId,
|
|
95
|
+
stepId: input.stepId,
|
|
96
|
+
attemptId: input.attemptId,
|
|
97
|
+
provider: input.provider,
|
|
98
|
+
action: input.action,
|
|
99
|
+
securityClass: input.securityClass,
|
|
100
|
+
target: input.target,
|
|
101
|
+
...(input.externalEffectKind === undefined ? {} : { externalEffectKind: input.externalEffectKind }),
|
|
102
|
+
payload: input.payload,
|
|
103
|
+
expectedContentDigest: input.expectedContentDigest,
|
|
104
|
+
payloadArtifactDigest: payloadArtifact.digest,
|
|
105
|
+
bindingDigest: input.bindingDigest,
|
|
106
|
+
inputDigest: input.inputDigest,
|
|
107
|
+
artifactDigests: input.artifactDigests,
|
|
108
|
+
...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }),
|
|
109
|
+
profile: profile.id,
|
|
110
|
+
profileDigest: canonicalDigest(profile, "WSSPEC_EXTERNAL_REQUEST_INVALID"),
|
|
111
|
+
workspaceDigest: await computeWorkspaceTreeDigest(state.worktree),
|
|
112
|
+
configDigest: state.item.execution.configDigest,
|
|
113
|
+
sideEffects: input.sideEffects,
|
|
114
|
+
createdAt: input.createdAt,
|
|
115
|
+
expiresAt: input.expiresAt,
|
|
116
|
+
}));
|
|
117
|
+
const derivedKey = externalIdempotencyKey(request);
|
|
118
|
+
return await mutateControlPlane({
|
|
119
|
+
cwd: input.root,
|
|
120
|
+
workItemId: input.workItemId,
|
|
121
|
+
eventType: "external-action.prepared",
|
|
122
|
+
idempotencyKey: `external-action:prepare:${request.idempotencyKey}:${request.requestDigest}`,
|
|
123
|
+
actor: input.actor,
|
|
124
|
+
stageId: input.stepId,
|
|
125
|
+
attemptId: input.attemptId,
|
|
126
|
+
operationInput: request,
|
|
127
|
+
mutate: async (projection) => {
|
|
128
|
+
const existingRequestId = projection.externalActionIdempotency[request.idempotencyKey];
|
|
129
|
+
if (existingRequestId !== undefined) {
|
|
130
|
+
const existing = currentAction(projection, existingRequestId);
|
|
131
|
+
if (existing.request.requestDigest !== request.requestDigest) {
|
|
132
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_IDEMPOTENCY_CONFLICT", "外部幂等键已绑定不同请求。");
|
|
133
|
+
}
|
|
134
|
+
return { projection, value: existing };
|
|
135
|
+
}
|
|
136
|
+
const conflicting = Object.values(projection.externalActions).find((candidate) => candidate.request.workItemId === request.workItemId
|
|
137
|
+
&& candidate.request.stepId === request.stepId
|
|
138
|
+
&& candidate.request.attemptId === request.attemptId
|
|
139
|
+
&& candidate.request.provider === request.provider
|
|
140
|
+
&& candidate.request.action === request.action
|
|
141
|
+
&& candidate.request.target.kind === request.target.kind
|
|
142
|
+
&& candidate.request.target.stableId === request.target.stableId);
|
|
143
|
+
if (conflicting !== undefined) {
|
|
144
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_IDEMPOTENCY_CONFLICT", "同一 Attempt 的外部动作已绑定不同 payload。");
|
|
145
|
+
}
|
|
146
|
+
assertActiveAttempt(projection, request, new Date(input.createdAt));
|
|
147
|
+
await assertCurrentExternalActionContext(state, projection, request);
|
|
148
|
+
if (input.idempotencyKey !== undefined && input.idempotencyKey !== derivedKey) {
|
|
149
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_IDEMPOTENCY_INVALID", "外部幂等键不符合规范身份生成规则。");
|
|
150
|
+
}
|
|
151
|
+
const persisted = await persistExternalActionPayload({
|
|
152
|
+
controlPlane: projection.controlPlane,
|
|
153
|
+
artifact: payloadArtifact,
|
|
154
|
+
});
|
|
155
|
+
if (persisted.created)
|
|
156
|
+
createdPayload = { controlPlane: projection.controlPlane, digest: persisted.digest };
|
|
157
|
+
const prepared = { status: "prepared", request };
|
|
158
|
+
return {
|
|
159
|
+
projection: {
|
|
160
|
+
...projection,
|
|
161
|
+
externalActions: { ...projection.externalActions, [request.requestId]: prepared },
|
|
162
|
+
externalActionIdempotency: { ...projection.externalActionIdempotency, [request.idempotencyKey]: request.requestId },
|
|
163
|
+
},
|
|
164
|
+
value: prepared,
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (createdPayload !== undefined) {
|
|
171
|
+
try {
|
|
172
|
+
await removeCreatedPayloadIfUnreferenced({
|
|
173
|
+
root: input.root,
|
|
174
|
+
workItemId: input.workItemId,
|
|
175
|
+
...createdPayload,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Cleanup is conservative: retain the artifact when durable references cannot be ruled out.
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return asExternalError(error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export async function approveExternalAction(input) {
|
|
186
|
+
if (input.terminal.isTTY !== true)
|
|
187
|
+
throw new ExternalAuthorizationError("WSSPEC_INTERACTIVE_TTY_REQUIRED", "外部动作授权必须来自真实交互式 TTY。");
|
|
188
|
+
const applicationState = await loadApplicationState(input.root, input.workItemId);
|
|
189
|
+
const current = currentAction(applicationState.projection, input.requestId);
|
|
190
|
+
if (current.request.requestDigest !== input.expectedRequestDigest) {
|
|
191
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_REQUEST_DIGEST_MISMATCH", "外部动作审批摘要不匹配。");
|
|
192
|
+
}
|
|
193
|
+
if (current.status !== "prepared" && current.status !== "approved") {
|
|
194
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_STATE_TRANSITION_INVALID", "只有 prepared 外部动作可以授权。");
|
|
195
|
+
}
|
|
196
|
+
return mutateControlPlane({
|
|
197
|
+
cwd: input.root,
|
|
198
|
+
workItemId: input.workItemId,
|
|
199
|
+
eventType: "external-action.approved",
|
|
200
|
+
idempotencyKey: `external-action:approve:${input.requestId}:${current.request.requestDigest}`,
|
|
201
|
+
actor: input.actor,
|
|
202
|
+
stageId: current.request.stepId,
|
|
203
|
+
attemptId: current.request.attemptId,
|
|
204
|
+
operationInput: {
|
|
205
|
+
decision: "approved",
|
|
206
|
+
requestDigest: input.expectedRequestDigest,
|
|
207
|
+
actor: input.actor,
|
|
208
|
+
approvalDigest: input.approvalDigest,
|
|
209
|
+
profile: input.profile,
|
|
210
|
+
profileDigest: input.profileDigest,
|
|
211
|
+
workspaceDigest: input.workspaceDigest,
|
|
212
|
+
configDigest: input.configDigest,
|
|
213
|
+
expiresAt: input.expiresAt,
|
|
214
|
+
},
|
|
215
|
+
mutate: async (projection) => {
|
|
216
|
+
const action = currentAction(projection, input.requestId);
|
|
217
|
+
if (action.status !== "prepared")
|
|
218
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_STATE_TRANSITION_INVALID", "只有 prepared 外部动作可以授权。");
|
|
219
|
+
const grant = validate("builtin.external-action-grant.v1", createExternalActionGrant({
|
|
220
|
+
request: action.request,
|
|
221
|
+
actor: input.actor,
|
|
222
|
+
approvalDigest: input.approvalDigest,
|
|
223
|
+
profile: input.profile,
|
|
224
|
+
profileDigest: input.profileDigest,
|
|
225
|
+
workspaceDigest: input.workspaceDigest,
|
|
226
|
+
configDigest: input.configDigest,
|
|
227
|
+
decidedAt: input.decidedAt,
|
|
228
|
+
expiresAt: input.expiresAt,
|
|
229
|
+
}));
|
|
230
|
+
assertActiveAttempt(projection, action.request, new Date(input.decidedAt));
|
|
231
|
+
await assertCurrentExternalActionContext(applicationState, projection, action.request);
|
|
232
|
+
const approved = transitionExternalAction(action, { status: "approved", request: action.request, grant });
|
|
233
|
+
return { projection: { ...projection, externalActions: { ...projection.externalActions, [input.requestId]: approved } }, value: approved };
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
export async function rejectExternalAction(input) {
|
|
238
|
+
if (input.terminal.isTTY !== true)
|
|
239
|
+
throw new ExternalAuthorizationError("WSSPEC_INTERACTIVE_TTY_REQUIRED", "外部动作拒绝决定必须来自真实交互式 TTY。");
|
|
240
|
+
const initial = currentAction(await readControlPlane(input.root, input.workItemId), input.requestId);
|
|
241
|
+
return mutateControlPlane({
|
|
242
|
+
cwd: input.root,
|
|
243
|
+
workItemId: input.workItemId,
|
|
244
|
+
eventType: "external-action.rejected",
|
|
245
|
+
idempotencyKey: `external-action:reject:${input.requestId}:${initial.request.requestDigest}`,
|
|
246
|
+
actor: input.actor,
|
|
247
|
+
stageId: initial.request.stepId,
|
|
248
|
+
attemptId: initial.request.attemptId,
|
|
249
|
+
operationInput: {
|
|
250
|
+
decision: "rejected",
|
|
251
|
+
requestId: input.requestId,
|
|
252
|
+
expectedRequestDigest: input.expectedRequestDigest,
|
|
253
|
+
actor: input.actor,
|
|
254
|
+
},
|
|
255
|
+
mutate: (projection) => {
|
|
256
|
+
const action = currentAction(projection, input.requestId);
|
|
257
|
+
if (action.request.requestDigest !== input.expectedRequestDigest) {
|
|
258
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_REQUEST_DIGEST_MISMATCH", "外部动作拒绝摘要不匹配。");
|
|
259
|
+
}
|
|
260
|
+
if (action.status !== "prepared") {
|
|
261
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_STATE_TRANSITION_INVALID", "只有 prepared 外部动作可以拒绝。");
|
|
262
|
+
}
|
|
263
|
+
assertActiveAttempt(projection, action.request, new Date(input.rejectedAt));
|
|
264
|
+
const rejection = {
|
|
265
|
+
requestId: action.request.requestId,
|
|
266
|
+
requestDigest: action.request.requestDigest,
|
|
267
|
+
actor: input.actor,
|
|
268
|
+
rejectedAt: input.rejectedAt,
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
projection: {
|
|
272
|
+
...projection,
|
|
273
|
+
evidence: { ...projection.evidence, [externalActionRejectionKey(input.requestId)]: rejection },
|
|
274
|
+
},
|
|
275
|
+
value: rejection,
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async function persistAction(input) {
|
|
281
|
+
return mutateControlPlane({
|
|
282
|
+
cwd: input.root,
|
|
283
|
+
workItemId: input.workItemId,
|
|
284
|
+
eventType: input.eventType,
|
|
285
|
+
idempotencyKey: `external-action:${input.suffix}:${input.requestId}`,
|
|
286
|
+
actor: input.actor ?? "external-coordinator",
|
|
287
|
+
operationInput: input.operationInput,
|
|
288
|
+
mutate: async (projection) => {
|
|
289
|
+
const current = currentAction(projection, input.requestId);
|
|
290
|
+
const next = await input.update(current, projection);
|
|
291
|
+
return { projection: { ...projection, externalActions: { ...projection.externalActions, [input.requestId]: next } }, value: next };
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function receipt(request, grant, input) {
|
|
296
|
+
if (input.targetStableId !== request.target.stableId
|
|
297
|
+
|| (request.externalEffectKind === "issue.comment") !== (input.externalEffectId !== undefined)
|
|
298
|
+
|| input.publishedContentDigest !== request.expectedContentDigest
|
|
299
|
+
|| input.readBackContentDigest !== request.expectedContentDigest) {
|
|
300
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_READBACK_MISMATCH", "外部回读的稳定目标或内容摘要不一致。");
|
|
301
|
+
}
|
|
302
|
+
return validate("builtin.external-write-receipt.v1", {
|
|
303
|
+
version: 1,
|
|
304
|
+
kind: "external-write-receipt",
|
|
305
|
+
requestId: request.requestId,
|
|
306
|
+
requestDigest: request.requestDigest,
|
|
307
|
+
grantDigest: grant.grantDigest,
|
|
308
|
+
workItemId: request.workItemId,
|
|
309
|
+
stepId: request.stepId,
|
|
310
|
+
attemptId: request.attemptId,
|
|
311
|
+
provider: request.provider,
|
|
312
|
+
action: request.action,
|
|
313
|
+
target: { ...request.target },
|
|
314
|
+
...(request.externalEffectKind === undefined ? {} : { externalEffectKind: request.externalEffectKind }),
|
|
315
|
+
...(input.externalEffectId === undefined ? {} : { externalEffectId: input.externalEffectId }),
|
|
316
|
+
payloadDigest: request.payloadDigest,
|
|
317
|
+
expectedContentDigest: request.expectedContentDigest,
|
|
318
|
+
bindingDigest: request.bindingDigest,
|
|
319
|
+
inputDigest: request.inputDigest,
|
|
320
|
+
artifactDigests: [...request.artifactDigests],
|
|
321
|
+
idempotencyKey: request.idempotencyKey,
|
|
322
|
+
publishedContentDigest: input.publishedContentDigest,
|
|
323
|
+
readBackContentDigest: input.readBackContentDigest,
|
|
324
|
+
status: "verified",
|
|
325
|
+
verifiedAt: input.verifiedAt,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
async function reconciliationRequired(input) {
|
|
329
|
+
return persistAction({
|
|
330
|
+
...input,
|
|
331
|
+
eventType: "external-action.reconciliation-required",
|
|
332
|
+
suffix: "reconciliation-required",
|
|
333
|
+
operationInput: { reason: "provider outcome unknown after dispatch", requiredAt: input.now },
|
|
334
|
+
update: (current) => {
|
|
335
|
+
if (current.status === "reconciliation_required")
|
|
336
|
+
return current;
|
|
337
|
+
if (current.status !== "executing" || current.dispatch !== "sent_or_unknown") {
|
|
338
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_STATE_TRANSITION_INVALID", "只有可能已发送的 executing 动作需要协调恢复。");
|
|
339
|
+
}
|
|
340
|
+
return transitionExternalAction(current, {
|
|
341
|
+
status: "reconciliation_required",
|
|
342
|
+
request: current.request,
|
|
343
|
+
grant: current.grant,
|
|
344
|
+
reason: "provider outcome unknown after dispatch",
|
|
345
|
+
requiredAt: input.now,
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
export async function executeExternalAction(input) {
|
|
351
|
+
const applicationState = await loadApplicationState(input.root, input.workItemId);
|
|
352
|
+
let current = currentAction(applicationState.projection, input.requestId);
|
|
353
|
+
if (current.status === "verified")
|
|
354
|
+
return current;
|
|
355
|
+
assertActiveAttempt(await readControlPlane(input.root, input.workItemId), current.request, new Date(input.now));
|
|
356
|
+
if (current.status === "reconciliation_required")
|
|
357
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_RECONCILIATION_REQUIRED", "外部动作结果未知,禁止自动重发。");
|
|
358
|
+
if (current.status === "failed" || current.status === "prepared")
|
|
359
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_STATE_TRANSITION_INVALID", "外部动作尚未获得有效 Grant 或已失败。");
|
|
360
|
+
if (canonicalDigest(input.payload) !== current.request.payloadDigest) {
|
|
361
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_PAYLOAD_MISMATCH", "执行 payload 与批准摘要不一致。");
|
|
362
|
+
}
|
|
363
|
+
assertGrantAuthorizes(current.request, current.grant, new Date(input.now));
|
|
364
|
+
const executionOwner = `external-execution-${crypto.randomUUID()}`;
|
|
365
|
+
if (current.status === "approved" || (current.status === "executing" && current.dispatch === "not_sent")) {
|
|
366
|
+
const claimed = await mutateControlPlane({
|
|
367
|
+
cwd: input.root,
|
|
368
|
+
workItemId: input.workItemId,
|
|
369
|
+
eventType: "external-action.executing",
|
|
370
|
+
idempotencyKey: `external-action:execution-owner:${input.requestId}:${executionOwner}`,
|
|
371
|
+
actor: "external-coordinator",
|
|
372
|
+
stageId: current.request.stepId,
|
|
373
|
+
attemptId: current.request.attemptId,
|
|
374
|
+
operationInput: { executionOwner, startedAt: input.now },
|
|
375
|
+
mutate: async (projection) => {
|
|
376
|
+
const action = currentAction(projection, input.requestId);
|
|
377
|
+
if (action.status === "executing" && action.dispatch === "not_sent" && action.executionOwner !== undefined) {
|
|
378
|
+
return { projection, value: { action, owned: false } };
|
|
379
|
+
}
|
|
380
|
+
if (action.status !== "approved" && (action.status !== "executing" || action.dispatch !== "not_sent")) {
|
|
381
|
+
return { projection, value: { action, owned: false } };
|
|
382
|
+
}
|
|
383
|
+
assertActiveAttempt(projection, action.request, new Date(input.now));
|
|
384
|
+
await assertCurrentExternalActionContext(applicationState, projection, action.request);
|
|
385
|
+
const executing = action.status === "approved"
|
|
386
|
+
? transitionExternalAction(action, {
|
|
387
|
+
status: "executing", request: action.request, grant: action.grant, dispatch: "not_sent",
|
|
388
|
+
startedAt: input.now, executionOwner,
|
|
389
|
+
})
|
|
390
|
+
: { ...action, startedAt: input.now, executionOwner };
|
|
391
|
+
return {
|
|
392
|
+
projection: { ...projection, externalActions: { ...projection.externalActions, [input.requestId]: executing } },
|
|
393
|
+
value: { action: executing, owned: true },
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
current = claimed.action;
|
|
398
|
+
if (!claimed.owned)
|
|
399
|
+
return current;
|
|
400
|
+
}
|
|
401
|
+
if (current.status !== "executing" || current.dispatch !== "not_sent" || current.executionOwner !== executionOwner) {
|
|
402
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_RECONCILIATION_REQUIRED", "外部动作可能已发送,禁止自动重发。");
|
|
403
|
+
}
|
|
404
|
+
const executionProjection = await readControlPlane(input.root, input.workItemId);
|
|
405
|
+
assertActiveAttempt(executionProjection, current.request, new Date(input.now));
|
|
406
|
+
await assertCurrentExternalActionContext(applicationState, executionProjection, current.request);
|
|
407
|
+
let dispatched = false;
|
|
408
|
+
const markDispatched = async () => {
|
|
409
|
+
const dispatchedState = await persistAction({
|
|
410
|
+
...input,
|
|
411
|
+
eventType: "external-action.dispatched",
|
|
412
|
+
suffix: "dispatched",
|
|
413
|
+
operationInput: { dispatchedAt: input.now },
|
|
414
|
+
update: (action) => {
|
|
415
|
+
if (action.status !== "executing" || action.dispatch !== "not_sent" || action.executionOwner !== executionOwner) {
|
|
416
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_STATE_TRANSITION_INVALID", "外部动作 dispatch 边界重复或无效。");
|
|
417
|
+
}
|
|
418
|
+
return transitionExternalAction(action, { ...action, dispatch: "sent_or_unknown", dispatchedAt: input.now });
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
dispatched = dispatchedState.status === "executing" && dispatchedState.dispatch === "sent_or_unknown";
|
|
422
|
+
};
|
|
423
|
+
let readBack;
|
|
424
|
+
try {
|
|
425
|
+
readBack = await input.executor.execute({ root: input.root, request: current.request, grant: current.grant, payload: input.payload, markDispatched });
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
const stored = currentAction(await readControlPlane(input.root, input.workItemId), input.requestId);
|
|
429
|
+
if (dispatched || (stored.status === "executing" && stored.dispatch === "sent_or_unknown")) {
|
|
430
|
+
return reconciliationRequired(input);
|
|
431
|
+
}
|
|
432
|
+
await persistAction({
|
|
433
|
+
...input,
|
|
434
|
+
eventType: "external-action.executing",
|
|
435
|
+
suffix: `execution-owner-released:${executionOwner}`,
|
|
436
|
+
operationInput: { releasedAt: input.now },
|
|
437
|
+
update: (action) => {
|
|
438
|
+
if (action.status !== "executing" || action.dispatch !== "not_sent" || action.executionOwner !== executionOwner)
|
|
439
|
+
return action;
|
|
440
|
+
const { executionOwner: _released, ...released } = action;
|
|
441
|
+
return released;
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_PROVIDER_EXECUTION_FAILED", "Provider 在 dispatch 前失败,未确认任何外部写入。");
|
|
445
|
+
}
|
|
446
|
+
const stored = currentAction(await readControlPlane(input.root, input.workItemId), input.requestId);
|
|
447
|
+
if (!dispatched || stored.status !== "executing" || stored.dispatch !== "sent_or_unknown") {
|
|
448
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_DISPATCH_EVIDENCE_MISSING", "Provider 未记录 dispatch 边界,不能确认执行结果。");
|
|
449
|
+
}
|
|
450
|
+
let confirmed;
|
|
451
|
+
try {
|
|
452
|
+
confirmed = receipt(stored.request, stored.grant, readBack);
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return reconciliationRequired(input);
|
|
456
|
+
}
|
|
457
|
+
return persistAction({
|
|
458
|
+
...input,
|
|
459
|
+
eventType: "external-action.verified",
|
|
460
|
+
suffix: "verified",
|
|
461
|
+
operationInput: confirmed,
|
|
462
|
+
update: (action) => {
|
|
463
|
+
if (action.status !== "executing" || action.dispatch !== "sent_or_unknown" || action.executionOwner !== executionOwner)
|
|
464
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_STATE_TRANSITION_INVALID", "只有 dispatch owner 的 executing 动作可被验证。");
|
|
465
|
+
return transitionExternalAction(action, { status: "verified", request: action.request, grant: action.grant, receipt: confirmed });
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
export async function reconcileExternalAction(input) {
|
|
470
|
+
const current = currentAction(await readControlPlane(input.root, input.workItemId), input.requestId);
|
|
471
|
+
if (current.status === "verified" || current.status === "failed")
|
|
472
|
+
return current;
|
|
473
|
+
if (current.status !== "reconciliation_required")
|
|
474
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_RECONCILIATION_NOT_REQUIRED", "当前外部动作不允许协调回查。");
|
|
475
|
+
let readBack;
|
|
476
|
+
try {
|
|
477
|
+
readBack = await input.executor.reconcile({ root: input.root, request: current.request, grant: current.grant });
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_PROVIDER_RECONCILIATION_FAILED", "Provider 只读协调回查失败,未改变外部动作状态。");
|
|
481
|
+
}
|
|
482
|
+
if (readBack.outcome === "unknown") {
|
|
483
|
+
const safeReadBack = { outcome: "unknown", checkedAt: input.now };
|
|
484
|
+
return persistAction({
|
|
485
|
+
...input,
|
|
486
|
+
eventType: "external-action.reconciled",
|
|
487
|
+
suffix: `reconciled-unknown:${canonicalDigest(safeReadBack).slice("sha256:".length)}`,
|
|
488
|
+
operationInput: safeReadBack,
|
|
489
|
+
update: (action) => action.status !== "reconciliation_required" ? action : { ...action, lastCheckedAt: input.now },
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (readBack.outcome === "failed") {
|
|
493
|
+
const safeReadBack = { outcome: "failed", checkedAt: input.now };
|
|
494
|
+
return persistAction({
|
|
495
|
+
...input,
|
|
496
|
+
eventType: "external-action.reconciled",
|
|
497
|
+
suffix: `reconciled-failed:${canonicalDigest(safeReadBack).slice("sha256:".length)}`,
|
|
498
|
+
operationInput: safeReadBack,
|
|
499
|
+
update: (action) => {
|
|
500
|
+
if (action.status !== "reconciliation_required")
|
|
501
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_STATE_TRANSITION_INVALID", "协调失败结果需要 reconciliation_required 状态。");
|
|
502
|
+
return transitionExternalAction(action, {
|
|
503
|
+
status: "failed",
|
|
504
|
+
request: action.request,
|
|
505
|
+
grant: action.grant,
|
|
506
|
+
reason: "provider read-back did not verify approved content",
|
|
507
|
+
failedAt: input.now,
|
|
508
|
+
});
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
let confirmed;
|
|
513
|
+
try {
|
|
514
|
+
confirmed = receipt(current.request, current.grant, {
|
|
515
|
+
targetStableId: readBack.targetStableId,
|
|
516
|
+
publishedContentDigest: readBack.contentDigest,
|
|
517
|
+
readBackContentDigest: readBack.contentDigest,
|
|
518
|
+
verifiedAt: input.now,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return persistAction({
|
|
523
|
+
...input,
|
|
524
|
+
eventType: "external-action.reconciled",
|
|
525
|
+
suffix: `reconciled-mismatch:${canonicalDigest({ outcome: "unknown", checkedAt: input.now }).slice("sha256:".length)}`,
|
|
526
|
+
operationInput: { outcome: "unknown", checkedAt: input.now },
|
|
527
|
+
update: (action) => action.status !== "reconciliation_required" ? action : { ...action, lastCheckedAt: input.now },
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
return persistAction({
|
|
531
|
+
...input,
|
|
532
|
+
eventType: "external-action.reconciled",
|
|
533
|
+
suffix: `reconciled-verified:${canonicalDigest(confirmed).slice("sha256:".length)}`,
|
|
534
|
+
operationInput: confirmed,
|
|
535
|
+
update: (action) => {
|
|
536
|
+
if (action.status !== "reconciliation_required")
|
|
537
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_STATE_TRANSITION_INVALID", "协调确认需要 reconciliation_required 状态。");
|
|
538
|
+
return transitionExternalAction(action, { status: "verified", request: action.request, grant: action.grant, receipt: confirmed });
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
export async function markExternalActionFailed(input) {
|
|
543
|
+
if (input.terminal.isTTY !== true) {
|
|
544
|
+
throw new ExternalAuthorizationError("WSSPEC_INTERACTIVE_TTY_REQUIRED", "协调恢复决定必须来自真实交互式 TTY。");
|
|
545
|
+
}
|
|
546
|
+
if (input.evidence.trim() === "" || input.evidence.includes("\0")) {
|
|
547
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_RECONCILIATION_EVIDENCE_INVALID", "协调恢复证据不能为空。");
|
|
548
|
+
}
|
|
549
|
+
const applicationState = await loadApplicationState(input.root, input.workItemId);
|
|
550
|
+
const initial = currentAction(applicationState.projection, input.requestId);
|
|
551
|
+
if (initial.request.requestDigest !== input.expectedRequestDigest) {
|
|
552
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_REQUEST_DIGEST_MISMATCH", "协调恢复决定未绑定当前 Request 摘要。");
|
|
553
|
+
}
|
|
554
|
+
const evidenceDigest = canonicalDigest({ evidence: input.evidence }, "WSSPEC_EXTERNAL_RECONCILIATION_EVIDENCE_INVALID");
|
|
555
|
+
return persistAction({
|
|
556
|
+
root: input.root,
|
|
557
|
+
workItemId: input.workItemId,
|
|
558
|
+
requestId: input.requestId,
|
|
559
|
+
eventType: "external-action.reconciled",
|
|
560
|
+
suffix: `operator-mark-failed:${evidenceDigest.slice("sha256:".length)}`,
|
|
561
|
+
actor: input.actor,
|
|
562
|
+
operationInput: {
|
|
563
|
+
decision: "mark_failed",
|
|
564
|
+
requestDigest: input.expectedRequestDigest,
|
|
565
|
+
evidenceDigest,
|
|
566
|
+
actor: input.actor,
|
|
567
|
+
},
|
|
568
|
+
update: (action, projection) => {
|
|
569
|
+
if (action.status !== "reconciliation_required") {
|
|
570
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_RECONCILIATION_NOT_REQUIRED", "当前外部动作不允许协调恢复决定。");
|
|
571
|
+
}
|
|
572
|
+
assertActiveAttempt(projection, action.request, new Date(input.now));
|
|
573
|
+
return transitionExternalAction(action, {
|
|
574
|
+
status: "failed",
|
|
575
|
+
request: action.request,
|
|
576
|
+
grant: action.grant,
|
|
577
|
+
reason: "操作者经审计决定将未知写入标记为失败",
|
|
578
|
+
failedAt: input.now,
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
export async function adoptVerifiedExternalAction(input) {
|
|
584
|
+
if (input.terminal.isTTY !== true) {
|
|
585
|
+
throw new ExternalAuthorizationError("WSSPEC_INTERACTIVE_TTY_REQUIRED", "协调恢复决定必须来自真实交互式 TTY。");
|
|
586
|
+
}
|
|
587
|
+
if (input.evidence.trim() === "" || input.evidence.includes("\0") || Buffer.byteLength(input.evidence, "utf8") > 2_048) {
|
|
588
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_RECONCILIATION_EVIDENCE_INVALID", "协调恢复证据不能为空。");
|
|
589
|
+
}
|
|
590
|
+
const inspectedId = inspectDecodedCredentialSurface(input.externalStableId, {
|
|
591
|
+
detectCredentialKeys: true,
|
|
592
|
+
maximumBytes: 2_048,
|
|
593
|
+
maximumDecodeRounds: 4,
|
|
594
|
+
});
|
|
595
|
+
if (!inspectedId.ok || input.externalStableId.trim() === ""
|
|
596
|
+
|| Buffer.byteLength(input.externalStableId, "utf8") > 2_048 || input.externalStableId.includes("\0")) {
|
|
597
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_RECONCILIATION_EVIDENCE_INVALID", "外部稳定标识无效。");
|
|
598
|
+
}
|
|
599
|
+
const applicationState = await loadApplicationState(input.root, input.workItemId);
|
|
600
|
+
const initial = currentAction(applicationState.projection, input.requestId);
|
|
601
|
+
if (initial.request.requestDigest !== input.expectedRequestDigest) {
|
|
602
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_REQUEST_DIGEST_MISMATCH", "协调恢复决定未绑定当前 Request 摘要。");
|
|
603
|
+
}
|
|
604
|
+
if (input.contentDigest !== initial.request.expectedContentDigest) {
|
|
605
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_READBACK_MISMATCH", "待采纳内容摘要与批准内容不一致。");
|
|
606
|
+
}
|
|
607
|
+
if (input.executor.adopt === undefined) {
|
|
608
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_ADOPTION_UNSUPPORTED", "当前 Provider 不支持带稳定标识的权威回读采纳。");
|
|
609
|
+
}
|
|
610
|
+
const evidenceDigest = canonicalDigest({ evidence: input.evidence }, "WSSPEC_EXTERNAL_RECONCILIATION_EVIDENCE_INVALID");
|
|
611
|
+
return persistAction({
|
|
612
|
+
root: input.root,
|
|
613
|
+
workItemId: input.workItemId,
|
|
614
|
+
requestId: input.requestId,
|
|
615
|
+
eventType: "external-action.reconciled",
|
|
616
|
+
suffix: `operator-adopt-verified:${canonicalDigest({ externalStableId: input.externalStableId, contentDigest: input.contentDigest, evidenceDigest }).slice("sha256:".length)}`,
|
|
617
|
+
actor: input.actor,
|
|
618
|
+
operationInput: {
|
|
619
|
+
decision: "adopt_verified",
|
|
620
|
+
requestDigest: input.expectedRequestDigest,
|
|
621
|
+
externalStableId: input.externalStableId,
|
|
622
|
+
contentDigest: input.contentDigest,
|
|
623
|
+
evidenceDigest,
|
|
624
|
+
actor: input.actor,
|
|
625
|
+
},
|
|
626
|
+
update: async (action, projection) => {
|
|
627
|
+
if (action.status !== "reconciliation_required") {
|
|
628
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_RECONCILIATION_NOT_REQUIRED", "当前外部动作不允许协调恢复决定。");
|
|
629
|
+
}
|
|
630
|
+
if (action.request.requestDigest !== input.expectedRequestDigest) {
|
|
631
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_REQUEST_DIGEST_MISMATCH", "协调恢复决定未绑定当前 Request 摘要。");
|
|
632
|
+
}
|
|
633
|
+
assertActiveAttempt(projection, action.request, new Date(input.now));
|
|
634
|
+
await assertCurrentExternalActionContext(applicationState, projection, action.request);
|
|
635
|
+
let readBack;
|
|
636
|
+
try {
|
|
637
|
+
readBack = await input.executor.adopt({
|
|
638
|
+
root: input.root,
|
|
639
|
+
request: action.request,
|
|
640
|
+
grant: action.grant,
|
|
641
|
+
externalStableId: input.externalStableId,
|
|
642
|
+
contentDigest: input.contentDigest,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_PROVIDER_RECONCILIATION_FAILED", "Provider 权威只读回查失败,未改变外部动作状态。");
|
|
647
|
+
}
|
|
648
|
+
if (readBack.outcome !== "verified") {
|
|
649
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_ADOPTION_NOT_VERIFIED", "Provider 未能权威确认待采纳外部动作。");
|
|
650
|
+
}
|
|
651
|
+
if (readBack.targetStableId !== input.externalStableId || readBack.contentDigest !== input.contentDigest) {
|
|
652
|
+
throw new ExternalAuthorizationError("WSSPEC_EXTERNAL_READBACK_MISMATCH", "Provider 权威回读与待采纳稳定标识或内容摘要不一致。");
|
|
653
|
+
}
|
|
654
|
+
const confirmed = receipt(action.request, action.grant, {
|
|
655
|
+
targetStableId: action.request.target.stableId,
|
|
656
|
+
...(action.request.externalEffectKind === "issue.comment" ? { externalEffectId: input.externalStableId } : {}),
|
|
657
|
+
publishedContentDigest: input.contentDigest,
|
|
658
|
+
readBackContentDigest: readBack.contentDigest,
|
|
659
|
+
verifiedAt: input.now,
|
|
660
|
+
});
|
|
661
|
+
return transitionExternalAction(action, {
|
|
662
|
+
status: "verified",
|
|
663
|
+
request: action.request,
|
|
664
|
+
grant: action.grant,
|
|
665
|
+
receipt: confirmed,
|
|
666
|
+
});
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
//# sourceMappingURL=external-action.js.map
|