create-windy 0.6.14 → 0.6.16
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/dist/cli.js +1008 -61
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +58 -8
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/agent/client-session/attempt-authority.test.ts +162 -0
- package/template/apps/server/src/agent/client-session/attempt-authority.ts +191 -0
- package/template/apps/server/src/agent/client-session/bootstrap.ts +27 -11
- package/template/apps/server/src/agent/client-session/completion.ts +75 -24
- package/template/apps/server/src/agent/client-session/contracts.ts +23 -0
- package/template/apps/server/src/agent/client-session/creation.ts +103 -0
- package/template/apps/server/src/agent/client-session/digest.ts +11 -1
- package/template/apps/server/src/agent/client-session/drizzle-records.ts +150 -0
- package/template/apps/server/src/agent/client-session/drizzle-repository.integration.test.ts +30 -3
- package/template/apps/server/src/agent/client-session/drizzle-repository.ts +23 -132
- package/template/apps/server/src/agent/client-session/finalization.ts +99 -0
- package/template/apps/server/src/agent/client-session/host-auth.ts +22 -10
- package/template/apps/server/src/agent/client-session/host-revocation-composition.test.ts +77 -0
- package/template/apps/server/src/agent/client-session/host-revocation-composition.ts +28 -0
- package/template/apps/server/src/agent/client-session/launch-authority.contract.test.ts +21 -0
- package/template/apps/server/src/agent/client-session/manifest-admission.ts +11 -0
- package/template/apps/server/src/agent/client-session/service.test-support.ts +22 -0
- package/template/apps/server/src/agent/client-session/service.ts +104 -134
- package/template/apps/server/src/agent/client-session/terminal-digest.test.ts +23 -0
- package/template/apps/server/src/agent/client-workflow-artifact/active-blob-references.ts +8 -0
- package/template/apps/server/src/agent/client-workflow-artifact/artifact-builder.ts +60 -0
- package/template/apps/server/src/agent/client-workflow-artifact/authority-current.ts +59 -0
- package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.test.ts +189 -0
- package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.ts +153 -0
- package/template/apps/server/src/agent/client-workflow-artifact/bootstrap.ts +77 -0
- package/template/apps/server/src/agent/client-workflow-artifact/cancellation.ts +47 -0
- package/template/apps/server/src/agent/client-workflow-artifact/completed-recovery.ts +31 -0
- package/template/apps/server/src/agent/client-workflow-artifact/contracts.ts +104 -0
- package/template/apps/server/src/agent/client-workflow-artifact/drizzle-locks.ts +30 -0
- package/template/apps/server/src/agent/client-workflow-artifact/drizzle-mapper.ts +138 -0
- package/template/apps/server/src/agent/client-workflow-artifact/drizzle-repository.ts +346 -0
- package/template/apps/server/src/agent/client-workflow-artifact/error.ts +21 -0
- package/template/apps/server/src/agent/client-workflow-artifact/handler.ts +167 -0
- package/template/apps/server/src/agent/client-workflow-artifact/host-revocation.ts +29 -0
- package/template/apps/server/src/agent/client-workflow-artifact/in-memory-repository.ts +232 -0
- package/template/apps/server/src/agent/client-workflow-artifact/job-port.ts +101 -0
- package/template/apps/server/src/agent/client-workflow-artifact/presentation.ts +41 -0
- package/template/apps/server/src/agent/client-workflow-artifact/recovery.contract.test.ts +253 -0
- package/template/apps/server/src/agent/client-workflow-artifact/release-binding.ts +85 -0
- package/template/apps/server/src/agent/client-workflow-artifact/repository.test.ts +204 -0
- package/template/apps/server/src/agent/client-workflow-artifact/repository.ts +97 -0
- package/template/apps/server/src/agent/client-workflow-artifact/routes-handler.contract.test.ts +201 -0
- package/template/apps/server/src/agent/client-workflow-artifact/routes.ts +161 -0
- package/template/apps/server/src/agent/client-workflow-artifact/runtime-composition.ts +101 -0
- package/template/apps/server/src/agent/client-workflow-artifact/scope-authority.ts +39 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service-authorization.ts +117 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service-support.ts +28 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service.contract.test.ts +345 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service.ts +350 -0
- package/template/apps/server/src/agent/client-workflow-artifact/session-boundary.ts +39 -0
- package/template/apps/server/src/agent/client-workflow-artifact/source-access.contract.test.ts +98 -0
- package/template/apps/server/src/agent/client-workflow-artifact/source-access.ts +80 -0
- package/template/apps/server/src/agent/client-workflow-artifact/takeover.ts +122 -0
- package/template/apps/server/src/agent/client-workflow-artifact/task-authority.contract.test.ts +97 -0
- package/template/apps/server/src/agent/client-workflow-artifact/task-authority.ts +147 -0
- package/template/apps/server/src/agent/client-workflow-artifact/terminal-authority.contract.test.ts +100 -0
- package/template/apps/server/src/agent/client-workflow-artifact/terminal-replay.ts +31 -0
- package/template/apps/server/src/agent/client-workflow-artifact/terminal-seal.ts +211 -0
- package/template/apps/server/src/agent/client-workflow-artifact/workflow-artifact.test-support.ts +173 -0
- package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.contract.test.ts +254 -0
- package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.ts +285 -0
- package/template/apps/server/src/agent/run-facade.test-support.ts +17 -4
- package/template/apps/server/src/agent/run-facade.test.ts +16 -0
- package/template/apps/server/src/agent/run-session-authorization.ts +12 -10
- package/template/apps/server/src/agent/runtime-bootstrap-input.ts +5 -1
- package/template/apps/server/src/agent/runtime-bootstrap.ts +33 -33
- package/template/apps/server/src/agent/runtime-public-ports.ts +40 -0
- package/template/apps/server/src/agent/session-invalidation/contracts.ts +25 -26
- package/template/apps/server/src/agent/session-projection-access.ts +31 -19
- package/template/apps/server/src/agent/unavailable-routes.ts +21 -0
- package/template/apps/server/src/agent/workflow-runtime-bootstrap.ts +2 -0
- package/template/apps/server/src/agent-workflow/artifact-source-authorization.ts +12 -6
- package/template/apps/server/src/agent-workflow/command-facade.ts +70 -11
- package/template/apps/server/src/agent-workflow/drizzle-artifact-port.ts +9 -9
- package/template/apps/server/src/agent-workflow/drizzle-delegation-completion.ts +1 -1
- package/template/apps/server/src/agent-workflow/platform-node-adapters.ts +14 -6
- package/template/apps/server/src/agent-workflow/project-launch-authority.contract.test.ts +53 -0
- package/template/apps/server/src/agent-workflow/project-launch-authority.ts +92 -0
- package/template/apps/server/src/agent-workflow/runtime-composition.ts +30 -14
- package/template/apps/server/src/app/agent-runtime-composition.ts +6 -1
- package/template/apps/server/src/app/client-agent-workflow-artifact-binding.ts +14 -0
- package/template/apps/server/src/application-services.ts +6 -0
- package/template/apps/server/src/data-access/scope-containment.test.ts +31 -0
- package/template/apps/server/src/data-access/scope-containment.ts +48 -0
- package/template/apps/server/src/jobs/artifact-publication.test.ts +192 -0
- package/template/apps/server/src/jobs/artifact-publication.ts +56 -0
- package/template/apps/server/src/jobs/artifact-publisher.ts +101 -0
- package/template/apps/server/src/jobs/drizzle-artifact.ts +73 -1
- package/template/apps/server/src/jobs/drizzle-execution-repository.ts +14 -1
- package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +65 -0
- package/template/apps/server/src/jobs/drizzle-repository.integration.test.ts +1 -0
- package/template/apps/server/src/jobs/execution-repository.ts +28 -0
- package/template/apps/server/src/jobs/execution.test.ts +3 -1
- package/template/apps/server/src/jobs/in-memory-execution.ts +16 -0
- package/template/apps/server/src/jobs/in-memory-lease.ts +45 -1
- package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +2 -0
- package/template/apps/server/src/jobs/postgres-cleanup-contract.test.ts +58 -0
- package/template/apps/server/src/jobs/worker.ts +8 -14
- package/template/apps/server/src/module-host/background-ai-unavailable.ts +18 -0
- package/template/apps/server/src/module-host/durable-context.ts +19 -22
- package/template/apps/server/src/module-host/implementation-completeness.test.ts +80 -1
- package/template/apps/server/src/module-host/implementation-completeness.ts +28 -1
- package/template/apps/server/src/module-host/request-context.ts +32 -8
- package/template/apps/server/src/module-host/storage-port.ts +3 -0
- package/template/apps/server/src/module-host/workflow-input-artifact-port.test.ts +104 -0
- package/template/apps/server/src/module-host/workflow-input-artifact-port.ts +229 -0
- package/template/apps/server/src/module-host/workflow-input-materialization.test.ts +163 -0
- package/template/apps/server/src/module-host/workflow-input-materialization.ts +161 -0
- package/template/apps/server/src/workflow/drizzle-repository.integration.test.ts +1 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +2 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +16 -0
- package/template/docs/architecture/agent-workflow.md +13 -2
- package/template/docs/architecture/durable-jobs.md +7 -0
- package/template/docs/operations/agent-session-memory.md +2 -1
- package/template/docs/operations/agent-session-troubleshooting.md +87 -2
- package/template/docs/operations/agent-workflow-recovery.md +25 -14
- package/template/docs/platform/client-agent-session.md +148 -2
- package/template/packages/ai-client/package.json +2 -1
- package/template/packages/ai-client/src/client-agent/index.ts +1 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/contracts.ts +85 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/http.test.ts +49 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/http.ts +82 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/index.ts +3 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.test.ts +154 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.ts +332 -0
- package/template/packages/database/drizzle/0062_client_agent_workflow_artifact.sql +63 -0
- package/template/packages/database/drizzle/meta/0062_snapshot.json +14839 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +177 -0
- package/template/packages/database/src/schema/client-agent-workflow-artifact.ts +158 -0
- package/template/packages/database/src/schema/index.ts +1 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs/src/handlers.test.ts +10 -0
- package/template/packages/jobs/src/handlers.ts +28 -11
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/ai-client-execution-policy.ts +6 -0
- package/template/packages/modules/src/manifest.ts +2 -0
- package/template/packages/modules/src/system-agent-audit-actions.ts +3 -0
- package/template/packages/modules/src/system-ai-api-bindings.ts +15 -0
- package/template/packages/modules/src/system-audit-actions.ts +15 -0
- package/template/packages/server-sdk/index.ts +52 -0
- package/template/packages/server-sdk/package.json +5 -1
- package/template/packages/server-sdk/src/client-agent-session.ts +13 -1
- package/template/packages/server-sdk/src/client-agent-workflow-artifact.contract.test.ts +113 -0
- package/template/packages/server-sdk/src/client-agent-workflow-artifact.ts +183 -0
- package/template/packages/server-sdk/src/durable-handler.ts +7 -0
- package/template/packages/server-sdk/src/request-context.ts +7 -0
- package/template/packages/server-sdk/src/workflow-input-artifact-port.ts +104 -0
- package/template/packages/server-sdk/src/workflow-input-artifact-validation.contract.test.ts +124 -0
- package/template/packages/server-sdk/src/workflow-input-artifact-validation.ts +347 -0
- package/template/packages/server-sdk/src/workflow-input-materialization-handler.ts +43 -0
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage/src/blob-store-put-once.test.ts +70 -0
- package/template/packages/storage/src/blob-store.ts +9 -0
- package/template/packages/storage/src/local-file-blob-put-once.ts +120 -0
- package/template/packages/storage/src/local-file-blob-store.ts +53 -1
- package/template/packages/storage/src/memory-blob-store.ts +73 -33
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ClientAgentSessionError } from "../client-session/errors.js";
|
|
2
|
+
import { ClientAgentWorkflowArtifactError } from "./error.js";
|
|
3
|
+
|
|
4
|
+
export async function clientWorkflowSessionCall<T>(
|
|
5
|
+
operation: Promise<T>,
|
|
6
|
+
phase: "create" | "finalize" | "revoke",
|
|
7
|
+
): Promise<T> {
|
|
8
|
+
try {
|
|
9
|
+
return await operation;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (!(error instanceof ClientAgentSessionError)) throw error;
|
|
12
|
+
if (error.code === "PROVIDER_FAILED") {
|
|
13
|
+
throw new ClientAgentWorkflowArtifactError(
|
|
14
|
+
"PROVIDER_FAILED",
|
|
15
|
+
"Browser Assistant 暂不可用",
|
|
16
|
+
503,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (phase === "finalize") {
|
|
20
|
+
throw new ClientAgentWorkflowArtifactError(
|
|
21
|
+
"RECEIPT_REJECTED",
|
|
22
|
+
"Client Agent 终态回执未通过校验",
|
|
23
|
+
error.status === 400 ? 400 : 409,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (error.code === "NOT_FOUND" || error.code === "FORBIDDEN") {
|
|
27
|
+
throw new ClientAgentWorkflowArtifactError(
|
|
28
|
+
"NOT_FOUND",
|
|
29
|
+
"Workflow Client Agent 任务不存在或不可访问",
|
|
30
|
+
404,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
throw new ClientAgentWorkflowArtifactError(
|
|
34
|
+
"CONFLICT",
|
|
35
|
+
phase === "create" ? "Client Agent Session 无法签发" : "会话无法撤销",
|
|
36
|
+
409,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
package/template/apps/server/src/agent/client-workflow-artifact/source-access.contract.test.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { DurableJobService } from "../../jobs/service.js";
|
|
3
|
+
import { buildScopeContext } from "../../data-access/context.js";
|
|
4
|
+
import { captureAuthorizationScope } from "../../data-access/scope-snapshot.js";
|
|
5
|
+
import { authorizationReadCoordinateForActor } from "../session-projection-access.js";
|
|
6
|
+
import type { CausalSourceReference } from "../session-invalidation/contracts.js";
|
|
7
|
+
import type { ClientAgentWorkflowArtifactRepository } from "./repository.js";
|
|
8
|
+
import { ClientAgentWorkflowArtifactSourceAccess } from "./source-access.js";
|
|
9
|
+
import { workflowRecord } from "./workflow-artifact.test-support.js";
|
|
10
|
+
|
|
11
|
+
test("来源读取允许当前多案 scope 包含冻结单案 scope,撤权或角色漂移拒绝", async () => {
|
|
12
|
+
const frozenContext = scope(["case-1"]);
|
|
13
|
+
const frozen = captureAuthorizationScope(frozenContext);
|
|
14
|
+
const record = workflowRecord({
|
|
15
|
+
scopeDigest: `sha256:${frozen.hash}`,
|
|
16
|
+
status: "completed",
|
|
17
|
+
terminalReceiptId: "receipt-1",
|
|
18
|
+
terminalReceiptDigest: `sha256:${"d".repeat(64)}`,
|
|
19
|
+
historySequence: 3,
|
|
20
|
+
});
|
|
21
|
+
const access = new ClientAgentWorkflowArtifactSourceAccess(
|
|
22
|
+
{
|
|
23
|
+
find: async () => record,
|
|
24
|
+
} as unknown as ClientAgentWorkflowArtifactRepository,
|
|
25
|
+
{
|
|
26
|
+
get: async () => ({
|
|
27
|
+
job: {
|
|
28
|
+
id: record.durableJobId,
|
|
29
|
+
handlerKey: record.taskKey,
|
|
30
|
+
requestedBy: record.actorId,
|
|
31
|
+
authorizationScope: frozen.snapshot,
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
} as unknown as DurableJobService,
|
|
35
|
+
);
|
|
36
|
+
const version = record.terminalReceiptId;
|
|
37
|
+
const digest = record.terminalReceiptDigest;
|
|
38
|
+
if (!version || !digest) throw new Error("terminal source fixture missing");
|
|
39
|
+
const source: CausalSourceReference = {
|
|
40
|
+
moduleKey: "system.agent-workflow",
|
|
41
|
+
sourceKind: "client-agent-artifact",
|
|
42
|
+
sourceId: record.workId,
|
|
43
|
+
version,
|
|
44
|
+
sequence: record.historySequence,
|
|
45
|
+
digest,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
await expect(
|
|
49
|
+
access.authorizeCurrent({
|
|
50
|
+
source,
|
|
51
|
+
authorization: authorization(["case-1", "case-2"]),
|
|
52
|
+
requestId: "request-containing",
|
|
53
|
+
}),
|
|
54
|
+
).resolves.toMatchObject({ outcome: "allowed" });
|
|
55
|
+
await expect(
|
|
56
|
+
access.authorizeCurrent({
|
|
57
|
+
source,
|
|
58
|
+
authorization: authorization(["case-2"]),
|
|
59
|
+
requestId: "request-revoked",
|
|
60
|
+
}),
|
|
61
|
+
).resolves.toEqual({ outcome: "denied", reason: "object-forbidden" });
|
|
62
|
+
await expect(
|
|
63
|
+
access.authorizeCurrent({
|
|
64
|
+
source,
|
|
65
|
+
authorization: authorization(["case-1"], "role-reviewer"),
|
|
66
|
+
requestId: "request-role-drift",
|
|
67
|
+
}),
|
|
68
|
+
).resolves.toEqual({ outcome: "denied", reason: "object-forbidden" });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function authorization(values: string[], roleId = "role-analyst") {
|
|
72
|
+
const scopeContext = scope(values, roleId);
|
|
73
|
+
return authorizationReadCoordinateForActor({
|
|
74
|
+
id: "actor-1",
|
|
75
|
+
type: "user",
|
|
76
|
+
name: "Analyst",
|
|
77
|
+
roleCodes: [roleId],
|
|
78
|
+
permissionKeys: ["system.agent-workflow.manage"],
|
|
79
|
+
dataAccess: scopeContext,
|
|
80
|
+
scopeContext,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function scope(values: string[], roleId = "role-analyst") {
|
|
85
|
+
return buildScopeContext(
|
|
86
|
+
"actor-1",
|
|
87
|
+
undefined,
|
|
88
|
+
[
|
|
89
|
+
{
|
|
90
|
+
id: roleId,
|
|
91
|
+
code: roleId,
|
|
92
|
+
dataScope: "custom",
|
|
93
|
+
departmentIds: values,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
[{ scope: "custom", dimension: "caseId", values }],
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthoritativeSourceAccessDecision,
|
|
3
|
+
AuthoritativeSourceAccessPort,
|
|
4
|
+
AuthoritativeSourceAccessQuery,
|
|
5
|
+
} from "../session-invalidation/contracts.js";
|
|
6
|
+
import type { DurableJobService } from "../../jobs/service.js";
|
|
7
|
+
import { hashAuthorizationScope } from "../../data-access/scope-snapshot.js";
|
|
8
|
+
import {
|
|
9
|
+
clientAgentWorkflowArtifactSourceKind,
|
|
10
|
+
clientAgentWorkflowArtifactSourceModule,
|
|
11
|
+
} from "./contracts.js";
|
|
12
|
+
import type { ClientAgentWorkflowArtifactRepository } from "./repository.js";
|
|
13
|
+
import {
|
|
14
|
+
currentClientWorkflowScopeContains,
|
|
15
|
+
requireClientWorkflowFrozenScope,
|
|
16
|
+
} from "./scope-authority.js";
|
|
17
|
+
|
|
18
|
+
export class ClientAgentWorkflowArtifactSourceAccess implements AuthoritativeSourceAccessPort {
|
|
19
|
+
constructor(
|
|
20
|
+
private readonly repository: ClientAgentWorkflowArtifactRepository,
|
|
21
|
+
private readonly jobs: DurableJobService,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
async authorizeCurrent(
|
|
25
|
+
query: AuthoritativeSourceAccessQuery,
|
|
26
|
+
): Promise<AuthoritativeSourceAccessDecision> {
|
|
27
|
+
if (
|
|
28
|
+
query.source.moduleKey !== clientAgentWorkflowArtifactSourceModule ||
|
|
29
|
+
query.source.sourceKind !== clientAgentWorkflowArtifactSourceKind ||
|
|
30
|
+
query.authorization.actorId.trim() === ""
|
|
31
|
+
) {
|
|
32
|
+
return denied();
|
|
33
|
+
}
|
|
34
|
+
const record = await this.repository.find(query.source.sourceId);
|
|
35
|
+
const currentScope = query.authorization.scopeSnapshot;
|
|
36
|
+
if (
|
|
37
|
+
!record ||
|
|
38
|
+
!currentScope ||
|
|
39
|
+
record.status !== "completed" ||
|
|
40
|
+
record.actorId !== query.authorization.actorId ||
|
|
41
|
+
record.terminalReceiptId !== query.source.version ||
|
|
42
|
+
record.historySequence !== query.source.sequence ||
|
|
43
|
+
!record.terminalReceiptDigest ||
|
|
44
|
+
(query.source.digest !== undefined &&
|
|
45
|
+
record.terminalReceiptDigest !== query.source.digest)
|
|
46
|
+
) {
|
|
47
|
+
return denied();
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const frozenScope = requireClientWorkflowFrozenScope(
|
|
51
|
+
record,
|
|
52
|
+
await this.jobs.get(record.durableJobId),
|
|
53
|
+
);
|
|
54
|
+
if (
|
|
55
|
+
`sha256:${hashAuthorizationScope(currentScope)}` !==
|
|
56
|
+
query.authorization.scopeDigest ||
|
|
57
|
+
!currentClientWorkflowScopeContains({
|
|
58
|
+
current: currentScope,
|
|
59
|
+
currentDigest: query.authorization.scopeDigest,
|
|
60
|
+
frozen: frozenScope,
|
|
61
|
+
frozenDigest: record.scopeDigest,
|
|
62
|
+
})
|
|
63
|
+
) {
|
|
64
|
+
return denied();
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
return denied();
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
outcome: "allowed",
|
|
71
|
+
sourceVersion: record.terminalReceiptId,
|
|
72
|
+
sourceSequence: record.historySequence,
|
|
73
|
+
authorizationGeneration: query.authorization.generation,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function denied(): AuthoritativeSourceAccessDecision {
|
|
79
|
+
return { outcome: "denied", reason: "object-forbidden" };
|
|
80
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { GovernedClientAgentSessionService } from "../client-session/service.js";
|
|
2
|
+
import type { ClientSessionRepository } from "../client-session/repository.js";
|
|
3
|
+
import type { ClientAgentWorkflowArtifactRepository } from "./repository.js";
|
|
4
|
+
import type { ClientAgentWorkflowArtifactRecord } from "./contracts.js";
|
|
5
|
+
import { workflowArtifactConflict } from "./service-support.js";
|
|
6
|
+
import { clientWorkflowSessionCall } from "./session-boundary.js";
|
|
7
|
+
|
|
8
|
+
export async function reconcileClientWorkflowTakeover(input: {
|
|
9
|
+
previousClientSessionId?: string;
|
|
10
|
+
workId: string;
|
|
11
|
+
sessions: GovernedClientAgentSessionService;
|
|
12
|
+
sessionRepository: ClientSessionRepository;
|
|
13
|
+
repository: ClientAgentWorkflowArtifactRepository;
|
|
14
|
+
now: number;
|
|
15
|
+
}) {
|
|
16
|
+
if (!input.previousClientSessionId) return;
|
|
17
|
+
const previous = await input.sessionRepository.find(
|
|
18
|
+
input.previousClientSessionId,
|
|
19
|
+
);
|
|
20
|
+
if (previous && (previous.attemptCount > 0 || previous.receipts.length)) {
|
|
21
|
+
const record = await input.repository.find(input.workId);
|
|
22
|
+
if (record) {
|
|
23
|
+
await input.repository.requestCancel({
|
|
24
|
+
workId: record.workId,
|
|
25
|
+
now: input.now,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
await clientWorkflowSessionCall(
|
|
29
|
+
input.sessions.revokeSession(
|
|
30
|
+
input.previousClientSessionId,
|
|
31
|
+
"workflow-unsafe-takeover",
|
|
32
|
+
),
|
|
33
|
+
"revoke",
|
|
34
|
+
);
|
|
35
|
+
if (record) {
|
|
36
|
+
await input.repository.finalizeCancel({
|
|
37
|
+
workId: record.workId,
|
|
38
|
+
now: input.now,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
throw workflowArtifactConflict("旧领取已产生页面副作用,不能在新宿主重放");
|
|
42
|
+
}
|
|
43
|
+
await clientWorkflowSessionCall(
|
|
44
|
+
input.sessions.revokeSession(
|
|
45
|
+
input.previousClientSessionId,
|
|
46
|
+
"workflow-takeover",
|
|
47
|
+
),
|
|
48
|
+
"revoke",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function canResumeClientWorkflowClaim(input: {
|
|
53
|
+
record: ClientAgentWorkflowArtifactRecord;
|
|
54
|
+
binding: {
|
|
55
|
+
clientBindingDigest: string;
|
|
56
|
+
installationId: string;
|
|
57
|
+
hostSessionDigest: string;
|
|
58
|
+
};
|
|
59
|
+
sessionRepository: ClientSessionRepository;
|
|
60
|
+
}) {
|
|
61
|
+
const { record, binding } = input;
|
|
62
|
+
if (
|
|
63
|
+
record.status !== "claimed" ||
|
|
64
|
+
record.clientBindingDigest !== binding.clientBindingDigest ||
|
|
65
|
+
record.installationId !== binding.installationId ||
|
|
66
|
+
record.hostSessionDigest !== binding.hostSessionDigest
|
|
67
|
+
) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (!record.clientSessionId) return true;
|
|
71
|
+
const session = await input.sessionRepository.find(record.clientSessionId);
|
|
72
|
+
return Boolean(
|
|
73
|
+
!session ||
|
|
74
|
+
(session.attemptCount === 0 &&
|
|
75
|
+
session.activeRequests === 0 &&
|
|
76
|
+
session.receipts.length === 0),
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function fenceResumableClientWorkflowClaim(
|
|
81
|
+
input: Parameters<typeof canResumeClientWorkflowClaim>[0] & {
|
|
82
|
+
sessions: GovernedClientAgentSessionService;
|
|
83
|
+
repository: ClientAgentWorkflowArtifactRepository;
|
|
84
|
+
now: number;
|
|
85
|
+
},
|
|
86
|
+
) {
|
|
87
|
+
const resumable = await canResumeClientWorkflowClaim(input);
|
|
88
|
+
const expired =
|
|
89
|
+
input.record.status === "claimed" &&
|
|
90
|
+
Boolean(
|
|
91
|
+
input.record.claimExpiresAt && input.record.claimExpiresAt <= input.now,
|
|
92
|
+
);
|
|
93
|
+
if (!resumable && !expired) return false;
|
|
94
|
+
if (input.record.clientSessionId) {
|
|
95
|
+
await clientWorkflowSessionCall(
|
|
96
|
+
input.sessions.revokeSession(
|
|
97
|
+
input.record.clientSessionId,
|
|
98
|
+
"workflow-takeover-fence",
|
|
99
|
+
),
|
|
100
|
+
"revoke",
|
|
101
|
+
);
|
|
102
|
+
const fenced = await input.sessionRepository.find(
|
|
103
|
+
input.record.clientSessionId,
|
|
104
|
+
);
|
|
105
|
+
if (fenced && (fenced.attemptCount > 0 || fenced.receipts.length)) {
|
|
106
|
+
const requested = await input.repository.requestCancel({
|
|
107
|
+
workId: input.record.workId,
|
|
108
|
+
now: input.now,
|
|
109
|
+
});
|
|
110
|
+
if (requested) {
|
|
111
|
+
await input.repository.finalizeCancel({
|
|
112
|
+
workId: requested.workId,
|
|
113
|
+
now: input.now,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
throw workflowArtifactConflict(
|
|
117
|
+
"旧领取已产生页面副作用,不能在新宿主重放",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
package/template/apps/server/src/agent/client-workflow-artifact/task-authority.contract.test.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { AgentWorkflowArtifactPort } from "../../agent-workflow/node-execution-ports.js";
|
|
3
|
+
import { ClientAgentWorkflowTaskAuthority } from "./task-authority.js";
|
|
4
|
+
import { captureAuthorizationScope } from "../../data-access/scope-snapshot.js";
|
|
5
|
+
import {
|
|
6
|
+
guardContext,
|
|
7
|
+
workflowBinding,
|
|
8
|
+
workflowInput,
|
|
9
|
+
workflowRecord,
|
|
10
|
+
workflowSignal,
|
|
11
|
+
} from "./workflow-artifact.test-support.js";
|
|
12
|
+
|
|
13
|
+
test("claim task 只投影 project canonical input,不公开已重授权 raw Artifact", async () => {
|
|
14
|
+
const raw = new TextEncoder().encode(
|
|
15
|
+
JSON.stringify({ sourceId: "source-1", secretEvidence: "raw" }),
|
|
16
|
+
);
|
|
17
|
+
const artifact = {
|
|
18
|
+
artifactId: "artifact-1",
|
|
19
|
+
kind: "case-input",
|
|
20
|
+
mediaType: "application/json",
|
|
21
|
+
byteSize: raw.byteLength,
|
|
22
|
+
sha256: "a".repeat(64),
|
|
23
|
+
retentionPolicyKey: "case-analysis",
|
|
24
|
+
source: {
|
|
25
|
+
moduleKey: "sharp-full-case",
|
|
26
|
+
kind: "case-source",
|
|
27
|
+
id: "source-1",
|
|
28
|
+
version: "1",
|
|
29
|
+
sequence: 1,
|
|
30
|
+
},
|
|
31
|
+
} as const;
|
|
32
|
+
const tasks = new ClientAgentWorkflowTaskAuthority();
|
|
33
|
+
let authorized = true;
|
|
34
|
+
const artifactPort: AgentWorkflowArtifactPort = {
|
|
35
|
+
async seal() {
|
|
36
|
+
return [];
|
|
37
|
+
},
|
|
38
|
+
async reauthorize() {
|
|
39
|
+
return authorized;
|
|
40
|
+
},
|
|
41
|
+
async resolve() {
|
|
42
|
+
return {
|
|
43
|
+
artifact,
|
|
44
|
+
token: "token-1",
|
|
45
|
+
expiresAt: "2026-08-28T00:01:00.000Z",
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
async open() {
|
|
49
|
+
return {
|
|
50
|
+
artifact,
|
|
51
|
+
body: new Blob([raw]).stream(),
|
|
52
|
+
filename: "input.json",
|
|
53
|
+
contentType: "application/json",
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
tasks.bind(artifactPort);
|
|
58
|
+
const task = await tasks.build({
|
|
59
|
+
facts: {
|
|
60
|
+
record: workflowRecord(),
|
|
61
|
+
input: workflowInput,
|
|
62
|
+
inputArtifacts: [artifact],
|
|
63
|
+
frozenScope: captureAuthorizationScope(guardContext.actor.scopeContext)
|
|
64
|
+
.snapshot,
|
|
65
|
+
binding: {
|
|
66
|
+
...workflowBinding,
|
|
67
|
+
clientTaskAuthority: {
|
|
68
|
+
...workflowBinding.clientTaskAuthority,
|
|
69
|
+
async build({ inputArtifacts }) {
|
|
70
|
+
const value = inputArtifacts[0]?.value;
|
|
71
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
72
|
+
throw new Error("test artifact invalid");
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
taskKey: "sharp.full-case",
|
|
76
|
+
input: { sourceId: value.sourceId as string },
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
actorId: "actor-1",
|
|
83
|
+
scopeDigest: workflowRecord().scopeDigest,
|
|
84
|
+
signal: workflowSignal,
|
|
85
|
+
});
|
|
86
|
+
expect(task.input).toEqual({ sourceId: "source-1" });
|
|
87
|
+
expect(JSON.stringify(task)).not.toContain("secretEvidence");
|
|
88
|
+
authorized = false;
|
|
89
|
+
await expect(
|
|
90
|
+
tasks.openInputArtifacts({
|
|
91
|
+
artifacts: [artifact],
|
|
92
|
+
actorId: "actor-1",
|
|
93
|
+
scopeDigest: workflowRecord().scopeDigest,
|
|
94
|
+
signal: workflowSignal,
|
|
95
|
+
}),
|
|
96
|
+
).rejects.toMatchObject({ code: "PROVIDER_FAILED" });
|
|
97
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { CLIENT_AGENT_WORKFLOW_ARTIFACT_BINDING_LIMITS as limits } from "@southwind-ai/server-sdk";
|
|
2
|
+
import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
|
|
3
|
+
import type { ModuleClientAgentWorkflowTaskInputArtifact } from "@southwind-ai/server-sdk";
|
|
4
|
+
import type { AgentWorkflowArtifactPort } from "../../agent-workflow/node-execution-ports.js";
|
|
5
|
+
import { canonicalJson as canonicalArtifact } from "../../agent-workflow/canonical-json.js";
|
|
6
|
+
import { digestJson } from "../client-session/digest.js";
|
|
7
|
+
import type {
|
|
8
|
+
ClientAgentWorkflowBeginFacts,
|
|
9
|
+
ClientAgentWorkflowExecutionTask,
|
|
10
|
+
} from "./contracts.js";
|
|
11
|
+
import { ClientAgentWorkflowArtifactError } from "./error.js";
|
|
12
|
+
|
|
13
|
+
/** Startup-bound Host Seam; a claim cannot proceed before Workflow Artifact runtime is ready. */
|
|
14
|
+
export class ClientAgentWorkflowTaskAuthority {
|
|
15
|
+
#artifacts?: AgentWorkflowArtifactPort;
|
|
16
|
+
|
|
17
|
+
bind(artifacts: AgentWorkflowArtifactPort) {
|
|
18
|
+
if (this.#artifacts && this.#artifacts !== artifacts) {
|
|
19
|
+
throw new Error("CLIENT_WORKFLOW_TASK_ARTIFACT_PORT_ALREADY_BOUND");
|
|
20
|
+
}
|
|
21
|
+
this.#artifacts = artifacts;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async build(input: {
|
|
25
|
+
readonly facts: ClientAgentWorkflowBeginFacts;
|
|
26
|
+
readonly actorId: string;
|
|
27
|
+
readonly scopeDigest: `sha256:${string}`;
|
|
28
|
+
readonly signal: AbortSignal;
|
|
29
|
+
}): Promise<ClientAgentWorkflowExecutionTask> {
|
|
30
|
+
const opened = await this.openInputArtifacts({
|
|
31
|
+
artifacts: input.facts.inputArtifacts,
|
|
32
|
+
actorId: input.actorId,
|
|
33
|
+
scopeDigest: input.scopeDigest,
|
|
34
|
+
signal: input.signal,
|
|
35
|
+
});
|
|
36
|
+
let projected;
|
|
37
|
+
try {
|
|
38
|
+
projected = await input.facts.binding.clientTaskAuthority.build({
|
|
39
|
+
workflowInput: input.facts.input,
|
|
40
|
+
inputArtifacts: opened,
|
|
41
|
+
actorId: input.actorId,
|
|
42
|
+
scopeDigest: input.scopeDigest,
|
|
43
|
+
signal: input.signal,
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
throw unavailable();
|
|
47
|
+
}
|
|
48
|
+
if (!validTaskKey(projected.taskKey)) throw unavailable();
|
|
49
|
+
const taskInput = canonicalJson(projected.input);
|
|
50
|
+
return {
|
|
51
|
+
schemaVersion: 1,
|
|
52
|
+
taskKey: projected.taskKey,
|
|
53
|
+
input: taskInput,
|
|
54
|
+
inputDigest: digestJson(taskInput),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async openInputArtifacts(input: {
|
|
59
|
+
readonly artifacts: ClientAgentWorkflowBeginFacts["inputArtifacts"];
|
|
60
|
+
readonly actorId: string;
|
|
61
|
+
readonly scopeDigest: `sha256:${string}`;
|
|
62
|
+
readonly signal: AbortSignal;
|
|
63
|
+
}): Promise<readonly ModuleClientAgentWorkflowTaskInputArtifact[]> {
|
|
64
|
+
const artifacts = this.#artifacts;
|
|
65
|
+
if (!artifacts && input.artifacts.length) throw unavailable();
|
|
66
|
+
if (
|
|
67
|
+
artifacts &&
|
|
68
|
+
!(await artifacts.reauthorize({
|
|
69
|
+
artifacts: input.artifacts,
|
|
70
|
+
actorId: input.actorId,
|
|
71
|
+
scopeDigest: input.scopeDigest,
|
|
72
|
+
signal: input.signal,
|
|
73
|
+
}))
|
|
74
|
+
) {
|
|
75
|
+
throw unavailable();
|
|
76
|
+
}
|
|
77
|
+
const opened = [];
|
|
78
|
+
let totalBytes = 0;
|
|
79
|
+
for (const artifact of input.artifacts) {
|
|
80
|
+
totalBytes += artifact.byteSize;
|
|
81
|
+
if (totalBytes > limits.clientTaskInputArtifactMaxBytes) {
|
|
82
|
+
throw unavailable();
|
|
83
|
+
}
|
|
84
|
+
const resolved = await artifacts!.resolve({
|
|
85
|
+
artifactId: artifact.artifactId,
|
|
86
|
+
actorId: input.actorId,
|
|
87
|
+
scopeDigest: input.scopeDigest,
|
|
88
|
+
signal: input.signal,
|
|
89
|
+
});
|
|
90
|
+
const value = await artifacts!.open({
|
|
91
|
+
token: resolved.token,
|
|
92
|
+
actorId: input.actorId,
|
|
93
|
+
scopeDigest: input.scopeDigest,
|
|
94
|
+
signal: input.signal,
|
|
95
|
+
});
|
|
96
|
+
if (
|
|
97
|
+
value.contentType !== "application/json" ||
|
|
98
|
+
canonicalArtifact(resolved.artifact) !== canonicalArtifact(artifact) ||
|
|
99
|
+
canonicalArtifact(value.artifact) !== canonicalArtifact(artifact)
|
|
100
|
+
) {
|
|
101
|
+
throw unavailable();
|
|
102
|
+
}
|
|
103
|
+
const bytes = new Uint8Array(
|
|
104
|
+
await new Response(value.body).arrayBuffer(),
|
|
105
|
+
);
|
|
106
|
+
if (bytes.byteLength !== artifact.byteSize) throw unavailable();
|
|
107
|
+
opened.push({ artifact, value: parseJson(bytes) });
|
|
108
|
+
}
|
|
109
|
+
return opened;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseJson(bytes: Uint8Array): ModuleAiJsonValue {
|
|
114
|
+
try {
|
|
115
|
+
return canonicalJson(JSON.parse(new TextDecoder().decode(bytes)));
|
|
116
|
+
} catch {
|
|
117
|
+
throw unavailable();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function canonicalJson(value: ModuleAiJsonValue): ModuleAiJsonValue {
|
|
122
|
+
const encoded = JSON.stringify(value);
|
|
123
|
+
if (
|
|
124
|
+
encoded === undefined ||
|
|
125
|
+
new TextEncoder().encode(encoded).byteLength > limits.clientTaskMaxBytes
|
|
126
|
+
) {
|
|
127
|
+
throw unavailable();
|
|
128
|
+
}
|
|
129
|
+
return JSON.parse(encoded) as ModuleAiJsonValue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validTaskKey(value: string) {
|
|
133
|
+
return (
|
|
134
|
+
typeof value === "string" &&
|
|
135
|
+
value.length > 0 &&
|
|
136
|
+
value.length <= limits.clientTaskKey &&
|
|
137
|
+
/^[a-z0-9][a-z0-9._-]*$/u.test(value)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function unavailable() {
|
|
142
|
+
return new ClientAgentWorkflowArtifactError(
|
|
143
|
+
"PROVIDER_FAILED",
|
|
144
|
+
"Client Agent canonical task authority unavailable",
|
|
145
|
+
503,
|
|
146
|
+
);
|
|
147
|
+
}
|
package/template/apps/server/src/agent/client-workflow-artifact/terminal-authority.contract.test.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { MemoryBlobStore } from "@southwind-ai/storage";
|
|
3
|
+
import {
|
|
4
|
+
createFixture,
|
|
5
|
+
validCompletion,
|
|
6
|
+
} from "../client-session/service.test-support.js";
|
|
7
|
+
import { hostSessionDigest } from "../client-session/host-auth.js";
|
|
8
|
+
import { InMemoryClientAgentWorkflowArtifactRepository } from "./in-memory-repository.js";
|
|
9
|
+
import { GovernedClientAgentWorkflowArtifactService } from "./service.js";
|
|
10
|
+
import { ClientAgentWorkflowTaskAuthority } from "./task-authority.js";
|
|
11
|
+
import {
|
|
12
|
+
clientBindingDigest,
|
|
13
|
+
guardContext,
|
|
14
|
+
principal,
|
|
15
|
+
workflowLoader,
|
|
16
|
+
workflowRecord,
|
|
17
|
+
workflowSignal,
|
|
18
|
+
} from "./workflow-artifact.test-support.js";
|
|
19
|
+
|
|
20
|
+
test("project terminal authority 拒绝时 Session 与 Artifact 均不完成", async () => {
|
|
21
|
+
const repository = new InMemoryClientAgentWorkflowArtifactRepository();
|
|
22
|
+
const sessions = await createFixture({
|
|
23
|
+
attemptAuthority: {
|
|
24
|
+
async check(facts) {
|
|
25
|
+
const current = await repository.findForActorRun({
|
|
26
|
+
actorId: facts.actorId,
|
|
27
|
+
runId: "run-1",
|
|
28
|
+
});
|
|
29
|
+
return current?.status === "claimed" &&
|
|
30
|
+
current.clientSessionId === facts.sessionId
|
|
31
|
+
? "current"
|
|
32
|
+
: "revoked";
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const blobs = new MemoryBlobStore();
|
|
37
|
+
const service = new GovernedClientAgentWorkflowArtifactService(
|
|
38
|
+
repository,
|
|
39
|
+
workflowLoader({
|
|
40
|
+
canonicalize: async () => {
|
|
41
|
+
throw new Error("PROJECT_RESULT_PROVENANCE_REJECTED");
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
sessions.service,
|
|
45
|
+
sessions.repository,
|
|
46
|
+
blobs,
|
|
47
|
+
new ClientAgentWorkflowTaskAuthority(),
|
|
48
|
+
() => sessions.clock.value,
|
|
49
|
+
);
|
|
50
|
+
await service.begin({
|
|
51
|
+
jobId: "job-1",
|
|
52
|
+
payload: {
|
|
53
|
+
commandId: "command-1",
|
|
54
|
+
runId: "run-1",
|
|
55
|
+
nodeRunId: "node-run-1",
|
|
56
|
+
effectGeneration: 1,
|
|
57
|
+
requestDigest: workflowRecord().requestDigest,
|
|
58
|
+
},
|
|
59
|
+
signal: workflowSignal,
|
|
60
|
+
});
|
|
61
|
+
const claimed = await service.claim({
|
|
62
|
+
workflowRunId: "run-1",
|
|
63
|
+
clientBindingDigest,
|
|
64
|
+
principal,
|
|
65
|
+
guardContext,
|
|
66
|
+
signal: workflowSignal,
|
|
67
|
+
});
|
|
68
|
+
if (!("sessionGrant" in claimed) || !("claimToken" in claimed)) {
|
|
69
|
+
throw new Error("claim grant missing");
|
|
70
|
+
}
|
|
71
|
+
const completion = await sessions.service.complete(
|
|
72
|
+
claimed.sessionGrant.sessionId,
|
|
73
|
+
validCompletion(),
|
|
74
|
+
{
|
|
75
|
+
token: claimed.sessionGrant.accessToken,
|
|
76
|
+
actorId: principal.actorId,
|
|
77
|
+
clientBindingDigest,
|
|
78
|
+
hostSessionDigest: hostSessionDigest(principal),
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
await expect(
|
|
82
|
+
service.advance({
|
|
83
|
+
workId: claimed.workId,
|
|
84
|
+
claimVersion: claimed.claimVersion,
|
|
85
|
+
claimToken: claimed.claimToken,
|
|
86
|
+
receiptIds: [completion.receipt.receiptId],
|
|
87
|
+
result: { answer: "未经来源校验的结果" },
|
|
88
|
+
principal,
|
|
89
|
+
guardContext,
|
|
90
|
+
signal: workflowSignal,
|
|
91
|
+
}),
|
|
92
|
+
).rejects.toMatchObject({ code: "RECEIPT_REJECTED", status: 400 });
|
|
93
|
+
expect(
|
|
94
|
+
(await sessions.repository.find(claimed.sessionGrant.sessionId))?.status,
|
|
95
|
+
).toBe("active");
|
|
96
|
+
const rejected = await repository.find(claimed.workId);
|
|
97
|
+
expect(rejected?.status).toBe("claimed");
|
|
98
|
+
expect(rejected?.artifact).toBeUndefined();
|
|
99
|
+
expect(blobs.size).toBe(0);
|
|
100
|
+
});
|