@stigmer/server 3.39.0 → 3.40.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/dist/artifactstorage/artifact-storage.d.ts +10 -2
- package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
- package/dist/artifactstorage/artifact-storage.js +6 -3
- package/dist/artifactstorage/artifact-storage.js.map +1 -1
- package/dist/boot/artifact-lane.d.ts +55 -0
- package/dist/boot/artifact-lane.d.ts.map +1 -0
- package/dist/boot/artifact-lane.js +74 -0
- package/dist/boot/artifact-lane.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +42 -19
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +11 -6
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js +10 -5
- package/dist/boot/config.js.map +1 -1
- package/dist/domain/agent/controller.d.ts +5 -2
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentexecution/controller.d.ts +18 -4
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +2 -2
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/create-steps.d.ts +29 -15
- package/dist/domain/agentexecution/create-steps.d.ts.map +1 -1
- package/dist/domain/agentexecution/create-steps.js +118 -51
- package/dist/domain/agentexecution/create-steps.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +8 -2
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentinstance/steps.d.ts +3 -1
- package/dist/domain/agentinstance/steps.d.ts.map +1 -1
- package/dist/domain/agentinstance/steps.js +12 -6
- package/dist/domain/agentinstance/steps.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts +9 -4
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +3 -3
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/artifact/file-server.d.ts +2 -1
- package/dist/domain/artifact/file-server.d.ts.map +1 -1
- package/dist/domain/artifact/file-server.js +6 -3
- package/dist/domain/artifact/file-server.js.map +1 -1
- package/dist/domain/channelapp/controller.d.ts +8 -3
- package/dist/domain/channelapp/controller.d.ts.map +1 -1
- package/dist/domain/channelapp/controller.js.map +1 -1
- package/dist/domain/environment/controller.d.ts +8 -5
- package/dist/domain/environment/controller.d.ts.map +1 -1
- package/dist/domain/environment/controller.js +3 -3
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/executioncontext/controller.d.ts +7 -4
- package/dist/domain/executioncontext/controller.d.ts.map +1 -1
- package/dist/domain/executioncontext/controller.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +6 -2
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/memory/constants.d.ts +9 -0
- package/dist/domain/memory/constants.d.ts.map +1 -1
- package/dist/domain/memory/constants.js +9 -0
- package/dist/domain/memory/constants.js.map +1 -1
- package/dist/domain/memory/controller.d.ts +18 -6
- package/dist/domain/memory/controller.d.ts.map +1 -1
- package/dist/domain/memory/controller.js +18 -21
- package/dist/domain/memory/controller.js.map +1 -1
- package/dist/domain/memory/steps.d.ts +38 -10
- package/dist/domain/memory/steps.d.ts.map +1 -1
- package/dist/domain/memory/steps.js +87 -17
- package/dist/domain/memory/steps.js.map +1 -1
- package/dist/domain/organization/controller.d.ts +7 -4
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/plugin/controller.d.ts.map +1 -1
- package/dist/domain/plugin/controller.js +10 -5
- package/dist/domain/plugin/controller.js.map +1 -1
- package/dist/domain/session/controller.d.ts +7 -3
- package/dist/domain/session/controller.d.ts.map +1 -1
- package/dist/domain/session/controller.js.map +1 -1
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +15 -5
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/workflow/controller.d.ts +6 -2
- package/dist/domain/workflow/controller.d.ts.map +1 -1
- package/dist/domain/workflow/controller.js +6 -3
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts +8 -4
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowexecution/update-status.d.ts.map +1 -1
- package/dist/domain/workflowexecution/update-status.js +3 -2
- package/dist/domain/workflowexecution/update-status.js.map +1 -1
- package/dist/domain/workflowinstance/controller.d.ts +2 -2
- package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
- package/dist/domain/workflowinstance/controller.js +10 -5
- package/dist/domain/workflowinstance/controller.js.map +1 -1
- package/dist/domain/workflowinstance/steps.d.ts +5 -3
- package/dist/domain/workflowinstance/steps.d.ts.map +1 -1
- package/dist/domain/workflowinstance/steps.js +14 -9
- package/dist/domain/workflowinstance/steps.js.map +1 -1
- package/dist/extensions/identity.d.ts +28 -14
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js +38 -0
- package/dist/extensions/identity.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +1 -13
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/package.json +6 -6
- package/src/artifactstorage/__tests__/artifact-storage.test.ts +15 -1
- package/src/artifactstorage/artifact-storage.ts +12 -4
- package/src/boot/__tests__/artifact-lane.test.ts +71 -0
- package/src/boot/__tests__/compose.test.ts +73 -3
- package/src/boot/__tests__/config.test.ts +19 -0
- package/src/boot/artifact-lane.ts +96 -0
- package/src/boot/compose.ts +57 -21
- package/src/boot/config.ts +25 -15
- package/src/domain/agent/controller.ts +5 -2
- package/src/domain/agentexecution/__tests__/create-steps.test.ts +230 -2
- package/src/domain/agentexecution/controller.ts +32 -6
- package/src/domain/agentexecution/create-steps.ts +162 -62
- package/src/domain/agentinstance/__tests__/store-faults.test.ts +69 -18
- package/src/domain/agentinstance/controller.ts +8 -2
- package/src/domain/agentinstance/steps.ts +12 -6
- package/src/domain/agentshare/controller.ts +12 -7
- package/src/domain/artifact/file-server.ts +8 -4
- package/src/domain/channelapp/controller.ts +8 -3
- package/src/domain/environment/controller.ts +11 -8
- package/src/domain/executioncontext/controller.ts +7 -4
- package/src/domain/mcpserver/controller.ts +6 -2
- package/src/domain/memory/__tests__/capture-credential.test.ts +6 -6
- package/src/domain/memory/__tests__/person-consent-composed.test.ts +187 -0
- package/src/domain/memory/__tests__/person-consent.test.ts +274 -0
- package/src/domain/memory/constants.ts +11 -0
- package/src/domain/memory/controller.ts +36 -27
- package/src/domain/memory/steps.ts +112 -17
- package/src/domain/organization/controller.ts +7 -4
- package/src/domain/plugin/__tests__/plugin.test.ts +8 -0
- package/src/domain/plugin/__tests__/store-faults.test.ts +104 -0
- package/src/domain/plugin/controller.ts +10 -5
- package/src/domain/session/controller.ts +7 -3
- package/src/domain/skill/__tests__/skill.test.ts +4 -0
- package/src/domain/skill/__tests__/store-faults.test.ts +103 -0
- package/src/domain/skill/controller.ts +15 -5
- package/src/domain/workflow/__tests__/store-faults.test.ts +107 -0
- package/src/domain/workflow/controller.ts +12 -5
- package/src/domain/workflowexecution/controller.ts +8 -4
- package/src/domain/workflowexecution/update-status.ts +3 -2
- package/src/domain/workflowinstance/__tests__/store-faults.test.ts +172 -0
- package/src/domain/workflowinstance/controller.ts +12 -7
- package/src/domain/workflowinstance/steps.ts +14 -10
- package/src/extensions/__tests__/identity.test.ts +82 -0
- package/src/extensions/identity.ts +47 -0
- package/src/pipeline/__tests__/blind-not-found.test.ts +4 -20
- package/src/pipeline/steps/guard-memory-capture.ts +4 -19
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the workflow's visibility-update load: a
|
|
3
|
+
* typed ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`workflow not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound that
|
|
6
|
+
* tells a client the workflow does not exist (stigmer/stigmer#1345).
|
|
7
|
+
*
|
|
8
|
+
* The surface is updateVisibility, reached through the registered handler on
|
|
9
|
+
* an in-process router. The composed suites reach only a real store, which
|
|
10
|
+
* cannot fail selectively, so it runs here against a store whose read throws.
|
|
11
|
+
* The validator and the default-instance creator are untouchable: a load that
|
|
12
|
+
* fails must stop the call before anything past it runs.
|
|
13
|
+
*
|
|
14
|
+
* Out of scope: the NotFound copy itself (wire contract).
|
|
15
|
+
*/
|
|
16
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
17
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
18
|
+
import { describe, expect, it } from "vitest";
|
|
19
|
+
|
|
20
|
+
import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/command_pb";
|
|
21
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
22
|
+
|
|
23
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
24
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
25
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
26
|
+
import {
|
|
27
|
+
errorOf,
|
|
28
|
+
failingStore,
|
|
29
|
+
untouchable,
|
|
30
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
31
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
32
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
33
|
+
import type { Store } from "../../../store/interface.js";
|
|
34
|
+
|
|
35
|
+
import { registerWorkflowServices } from "../controller.js";
|
|
36
|
+
|
|
37
|
+
const silentLogger = createLogger({
|
|
38
|
+
level: "error",
|
|
39
|
+
pretty: false,
|
|
40
|
+
write: () => {},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const WORKFLOW_ID = "wfl_storefault";
|
|
44
|
+
|
|
45
|
+
const NOT_FOUND_COPY = `workflow not found: ${WORKFLOW_ID}`;
|
|
46
|
+
const LOAD_FAULT_COPY = "failed to load workflow";
|
|
47
|
+
|
|
48
|
+
const MISSING = (): Error =>
|
|
49
|
+
new ResourceNotFoundError(`workflow/${WORKFLOW_ID}`);
|
|
50
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
54
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
55
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
56
|
+
* the store.
|
|
57
|
+
*/
|
|
58
|
+
function workflowCommand(
|
|
59
|
+
store: Store,
|
|
60
|
+
): Client<typeof WorkflowCommandController> {
|
|
61
|
+
const transport = createRouterTransport(
|
|
62
|
+
(router) => {
|
|
63
|
+
registerWorkflowServices(router, {
|
|
64
|
+
store,
|
|
65
|
+
logger: silentLogger,
|
|
66
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
67
|
+
authorizationLifecycle: undefined,
|
|
68
|
+
validator: untouchable("validator"),
|
|
69
|
+
workflowInstanceCreator: untouchable("workflowInstanceCreator"),
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
router: {
|
|
74
|
+
interceptors: [
|
|
75
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
76
|
+
createApiResourceInterceptor(),
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
return createClient(WorkflowCommandController, transport);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("updateVisibility — LoadWorkflowForVisibilityUpdate", () => {
|
|
85
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
86
|
+
return errorOf(() =>
|
|
87
|
+
workflowCommand(store).updateVisibility({
|
|
88
|
+
resourceId: WORKFLOW_ID,
|
|
89
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
it("a missing workflow answers NotFound with the domain's copy", async () => {
|
|
95
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
96
|
+
|
|
97
|
+
expect(error.code).toBe(Code.NotFound);
|
|
98
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
102
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
103
|
+
|
|
104
|
+
expect(error.code).toBe(Code.Internal);
|
|
105
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
* Proven by workflow.conformance.test.ts (CONFORMANCE_TARGET=local) and
|
|
14
14
|
* __tests__/.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Every chain opens with Authorize; create, delete and updateVisibility run
|
|
17
|
+
* the shared tuple-lifecycle steps against the composed lifecycle;
|
|
18
|
+
* getByReference and listVersions authorize the resolved workflow as `get`
|
|
19
|
+
* would. getVersion evaluates its annotation through authorizeDirect;
|
|
20
|
+
* validateSpec deliberately does not, since it persists nothing. Per-RPC
|
|
21
|
+
* posture: docs/authorization-coverage.md §17.
|
|
18
22
|
*/
|
|
19
23
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
20
24
|
import { create, enumToJson, fromBinary } from "@bufbuild/protobuf";
|
|
@@ -492,7 +496,7 @@ async function updateVisibility(
|
|
|
492
496
|
return reqCtx.get(UPDATE_VISIBILITY_WORKFLOW_KEY) as Workflow;
|
|
493
497
|
}
|
|
494
498
|
|
|
495
|
-
/** Loads the workflow by resource_id;
|
|
499
|
+
/** Loads the workflow by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
496
500
|
function newLoadWorkflowForVisibilityUpdateStep(
|
|
497
501
|
store: Store,
|
|
498
502
|
): PipelineStep<UpdateVisibilityDesc> {
|
|
@@ -507,8 +511,11 @@ function newLoadWorkflowForVisibilityUpdateStep(
|
|
|
507
511
|
input.resourceId,
|
|
508
512
|
WorkflowSchema,
|
|
509
513
|
);
|
|
510
|
-
} catch {
|
|
511
|
-
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if (error instanceof ResourceNotFoundError) {
|
|
516
|
+
throw notFoundError("workflow", input.resourceId);
|
|
517
|
+
}
|
|
518
|
+
throw internalError(error, "failed to load workflow");
|
|
512
519
|
}
|
|
513
520
|
ctx.set(UPDATE_VISIBILITY_WORKFLOW_KEY, workflow);
|
|
514
521
|
},
|
|
@@ -12,10 +12,14 @@
|
|
|
12
12
|
* Proven by workflowexecution.conformance.test.ts
|
|
13
13
|
* (CONFORMANCE_TARGET=local) and __tests__/.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Every chain opens with Authorize, and create also authorizes its run
|
|
16
|
+
* target (AuthorizeRunTarget); create and delete run the shared
|
|
17
|
+
* tuple-lifecycle steps against the composed lifecycle. The streams and
|
|
18
|
+
* the event log evaluate their annotation through authorizeDirect; the
|
|
19
|
+
* list, summary and pending-approval reads narrow through the composed
|
|
20
|
+
* list read scope. Subscribe streams ride the in-memory stream broker
|
|
21
|
+
* (ADR 011); there is no Redis. Per-RPC posture:
|
|
22
|
+
* docs/authorization-coverage.md §19.
|
|
19
23
|
*/
|
|
20
24
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
21
25
|
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
* phase-transition-only statusAudit bump (heartbeats must not perpetually
|
|
23
23
|
* re-sort long-running executions above new ones in the recents sidebar).
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* The chain opens with Authorize (can_edit on the execution); the
|
|
26
|
+
* broadcast rides the in-memory stream broker (ADR 011) — there is no
|
|
27
|
+
* Redis.
|
|
27
28
|
*/
|
|
28
29
|
import { create, toBinary } from "@bufbuild/protobuf";
|
|
29
30
|
import { timestampNow } from "@bufbuild/protobuf/wkt";
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the workflow instance's loads. The shared
|
|
3
|
+
* targeted-update load (updateVisibility and updateExecutionVisibility): a
|
|
4
|
+
* typed ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
5
|
+
* (`workflow instance not found: <id>`), and any other store failure is an
|
|
6
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound that
|
|
7
|
+
* tells a client the instance does not exist (stigmer/stigmer#1345). Create's
|
|
8
|
+
* parent-workflow load reads the workflow through the in-process query
|
|
9
|
+
* client, so the store's typed miss arrives as a Connect NotFound: that
|
|
10
|
+
* answers the pinned `Workflow not found: <id>`, and any other failure an
|
|
11
|
+
* Internal, never a missing parent (stigmer/stigmer#1351).
|
|
12
|
+
*
|
|
13
|
+
* Every surface is reached through the registered handler on an in-process
|
|
14
|
+
* router. The composed suites reach only a real store, which cannot fail
|
|
15
|
+
* selectively, so the updates run against a store whose read throws and
|
|
16
|
+
* create against a parent loader whose read throws. Whatever the surface does
|
|
17
|
+
* not need is untouchable: a load that fails must stop the call before
|
|
18
|
+
* anything past it runs.
|
|
19
|
+
*
|
|
20
|
+
* Out of scope: the NotFound copies themselves (wire contract).
|
|
21
|
+
*/
|
|
22
|
+
import type { Client } from "@connectrpc/connect";
|
|
23
|
+
import {
|
|
24
|
+
Code,
|
|
25
|
+
ConnectError,
|
|
26
|
+
createClient,
|
|
27
|
+
createRouterTransport,
|
|
28
|
+
} from "@connectrpc/connect";
|
|
29
|
+
import { describe, expect, it } from "vitest";
|
|
30
|
+
|
|
31
|
+
import { WorkflowInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/command_pb";
|
|
32
|
+
import { WorkflowExecutionVisibility } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/spec_pb";
|
|
33
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
34
|
+
|
|
35
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
36
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
37
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
38
|
+
import {
|
|
39
|
+
errorOf,
|
|
40
|
+
failingStore,
|
|
41
|
+
untouchable,
|
|
42
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
43
|
+
import { INTERNAL_FALLBACK_MESSAGE } from "../../../pipeline/pipeline.js";
|
|
44
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
45
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
46
|
+
import type { Store } from "../../../store/interface.js";
|
|
47
|
+
|
|
48
|
+
import { registerWorkflowInstanceServices } from "../controller.js";
|
|
49
|
+
import type { ParentWorkflowLoaderProvider } from "../steps.js";
|
|
50
|
+
|
|
51
|
+
const silentLogger = createLogger({
|
|
52
|
+
level: "error",
|
|
53
|
+
pretty: false,
|
|
54
|
+
write: () => {},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const INSTANCE_ID = "win_storefault";
|
|
58
|
+
const WORKFLOW_ID = "wfl_storefault";
|
|
59
|
+
|
|
60
|
+
const NOT_FOUND_COPY = `workflow instance not found: ${INSTANCE_ID}`;
|
|
61
|
+
const LOAD_FAULT_COPY = "failed to load workflow instance";
|
|
62
|
+
const PARENT_NOT_FOUND_COPY = `Workflow not found: ${WORKFLOW_ID}`;
|
|
63
|
+
const PARENT_LOAD_FAULT_COPY = "failed to load parent workflow";
|
|
64
|
+
|
|
65
|
+
const MISSING = (): Error =>
|
|
66
|
+
new ResourceNotFoundError(`workflow_instance/${INSTANCE_ID}`);
|
|
67
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
68
|
+
|
|
69
|
+
/** What the workflow query's `get` answers for a missing and a faulted workflow. */
|
|
70
|
+
const PARENT_MISSING = (): Error =>
|
|
71
|
+
new ConnectError(`Workflow not found: ${WORKFLOW_ID}`, Code.NotFound);
|
|
72
|
+
const PARENT_FAULTED = (): Error =>
|
|
73
|
+
new ConnectError(INTERNAL_FALLBACK_MESSAGE, Code.Internal);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
77
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
78
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
79
|
+
* the store or the parent.
|
|
80
|
+
*/
|
|
81
|
+
function instanceCommand(
|
|
82
|
+
store: Store,
|
|
83
|
+
parentWorkflowLoader: ParentWorkflowLoaderProvider = untouchable(
|
|
84
|
+
"parentWorkflowLoader",
|
|
85
|
+
),
|
|
86
|
+
): Client<typeof WorkflowInstanceCommandController> {
|
|
87
|
+
const transport = createRouterTransport(
|
|
88
|
+
(router) => {
|
|
89
|
+
registerWorkflowInstanceServices(router, {
|
|
90
|
+
store,
|
|
91
|
+
logger: silentLogger,
|
|
92
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
93
|
+
authorizationLifecycle: undefined,
|
|
94
|
+
parentWorkflowLoader,
|
|
95
|
+
listReadScope: undefined,
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
router: {
|
|
100
|
+
interceptors: [
|
|
101
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
102
|
+
createApiResourceInterceptor(),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
return createClient(WorkflowInstanceCommandController, transport);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe.each([
|
|
111
|
+
{
|
|
112
|
+
surface: "updateVisibility — LoadInstanceForVisibilityUpdate",
|
|
113
|
+
call: (store: Store) =>
|
|
114
|
+
instanceCommand(store).updateVisibility({
|
|
115
|
+
resourceId: INSTANCE_ID,
|
|
116
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
surface:
|
|
121
|
+
"updateExecutionVisibility — LoadInstanceForExecutionVisibilityUpdate",
|
|
122
|
+
call: (store: Store) =>
|
|
123
|
+
instanceCommand(store).updateExecutionVisibility({
|
|
124
|
+
resourceId: INSTANCE_ID,
|
|
125
|
+
executionVisibility: WorkflowExecutionVisibility.private,
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
])("$surface", ({ call }) => {
|
|
129
|
+
it("a missing workflow instance answers NotFound with the domain's copy", async () => {
|
|
130
|
+
const error = await errorOf(() => call(failingStore(MISSING())));
|
|
131
|
+
|
|
132
|
+
expect(error.code).toBe(Code.NotFound);
|
|
133
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
137
|
+
const error = await errorOf(() => call(failingStore(LOCKED())));
|
|
138
|
+
|
|
139
|
+
expect(error.code).toBe(Code.Internal);
|
|
140
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("create — LoadParentWorkflow", () => {
|
|
145
|
+
function surfaceError(parentError: Error): Promise<ConnectError> {
|
|
146
|
+
const failingParent: ParentWorkflowLoaderProvider = () => ({
|
|
147
|
+
get: () => Promise.reject(parentError),
|
|
148
|
+
});
|
|
149
|
+
return errorOf(() =>
|
|
150
|
+
instanceCommand(untouchable("store"), failingParent).create({
|
|
151
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
152
|
+
kind: "WorkflowInstance",
|
|
153
|
+
metadata: { name: "Store Fault Instance", org: "org_storefault" },
|
|
154
|
+
spec: { workflowId: WORKFLOW_ID },
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
it("a missing parent workflow answers NotFound with the pinned copy", async () => {
|
|
160
|
+
const error = await surfaceError(PARENT_MISSING());
|
|
161
|
+
|
|
162
|
+
expect(error.code).toBe(Code.NotFound);
|
|
163
|
+
expect(error.rawMessage).toBe(PARENT_NOT_FOUND_COPY);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("any other parent-load failure answers a sanitized Internal", async () => {
|
|
167
|
+
const error = await surfaceError(PARENT_FAULTED());
|
|
168
|
+
|
|
169
|
+
expect(error.code).toBe(Code.Internal);
|
|
170
|
+
expect(error.rawMessage).toBe(PARENT_LOAD_FAULT_COPY);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Pipeline per RPC mirrors the Go step chains character-for-character.
|
|
15
15
|
* Proven by workflowinstance.conformance.test.ts
|
|
16
|
-
* (CONFORMANCE_TARGET=local) and the family
|
|
17
|
-
* ../workflow/__tests__/workflow.test.ts (the mutual edge makes the two
|
|
16
|
+
* (CONFORMANCE_TARGET=local), __tests__/store-faults.test.ts and the family
|
|
17
|
+
* test ../workflow/__tests__/workflow.test.ts (the mutual edge makes the two
|
|
18
18
|
* domains one testable unit).
|
|
19
19
|
*/
|
|
20
20
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
newValidateVisibilityStep,
|
|
105
105
|
newValidateVisibilityUpdateStep,
|
|
106
106
|
} from "../../pipeline/steps/validate-visibility.js";
|
|
107
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
107
108
|
import type { Store } from "../../store/interface.js";
|
|
108
109
|
import { workflowInstanceSearchExtractor } from "./search-extractor.js";
|
|
109
110
|
import {
|
|
@@ -573,9 +574,10 @@ function newSetInstanceExecutionVisibilityStep(): PipelineStep<UpdateExecutionVi
|
|
|
573
574
|
// ---------------------------------------------------------------------------
|
|
574
575
|
|
|
575
576
|
/**
|
|
576
|
-
* Loads the instance by resource_id;
|
|
577
|
-
* accessor keeps the input shape compiler-checked (both
|
|
578
|
-
* inputs carry resource_id, but the compiler should prove it,
|
|
577
|
+
* Loads the instance by resource_id; a missing one answers NotFound, a store
|
|
578
|
+
* fault Internal. The accessor keeps the input shape compiler-checked (both
|
|
579
|
+
* targeted-update inputs carry resource_id, but the compiler should prove it,
|
|
580
|
+
* not a cast).
|
|
579
581
|
*/
|
|
580
582
|
function newLoadInstanceStep<Desc extends DescMessage>(
|
|
581
583
|
store: Store,
|
|
@@ -594,8 +596,11 @@ function newLoadInstanceStep<Desc extends DescMessage>(
|
|
|
594
596
|
resourceId,
|
|
595
597
|
WorkflowInstanceSchema,
|
|
596
598
|
);
|
|
597
|
-
} catch {
|
|
598
|
-
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (error instanceof ResourceNotFoundError) {
|
|
601
|
+
throw notFoundError("workflow instance", resourceId);
|
|
602
|
+
}
|
|
603
|
+
throw internalError(error, "failed to load workflow instance");
|
|
599
604
|
}
|
|
600
605
|
ctx.set(instanceKey, instance);
|
|
601
606
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the family test ../workflow/__tests__/workflow.test.ts (the mutual edge
|
|
9
9
|
* makes the two domains one testable unit).
|
|
10
10
|
*/
|
|
11
|
-
import { ConnectError } from "@connectrpc/connect";
|
|
11
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
12
12
|
import type { DescMessage } from "@bufbuild/protobuf";
|
|
13
13
|
|
|
14
14
|
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
@@ -55,9 +55,11 @@ export type ParentWorkflowLoaderProvider = () => ParentWorkflowLoader;
|
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* LoadParentWorkflow — create.go: loads the workflow the instance
|
|
58
|
-
* materializes and stashes it for the same-org rule.
|
|
59
|
-
*
|
|
60
|
-
* (capital-W copy is the
|
|
58
|
+
* materializes and stashes it for the same-org rule. The workflow is read
|
|
59
|
+
* through the in-process query client, so a missing one arrives as a Connect
|
|
60
|
+
* NotFound and answers NotFound("Workflow", id) (the capital-W copy is the
|
|
61
|
+
* wire contract); any other failure is a fault answered as Internal, never a
|
|
62
|
+
* missing parent.
|
|
61
63
|
*/
|
|
62
64
|
export function newLoadParentWorkflowStep(
|
|
63
65
|
loaderProvider: ParentWorkflowLoaderProvider,
|
|
@@ -74,12 +76,14 @@ export function newLoadParentWorkflowStep(
|
|
|
74
76
|
try {
|
|
75
77
|
parent = await loaderProvider().get(workflowId);
|
|
76
78
|
} catch (error) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
error
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
if (error instanceof ConnectError && error.code === Code.NotFound) {
|
|
80
|
+
logger.warn("Parent workflow not found", {
|
|
81
|
+
error: error.rawMessage,
|
|
82
|
+
workflowId,
|
|
83
|
+
});
|
|
84
|
+
throw notFoundError("Workflow", workflowId);
|
|
85
|
+
}
|
|
86
|
+
throw internalError(error, "failed to load parent workflow");
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
ctx.set(PARENT_WORKFLOW_KEY, parent);
|
|
@@ -13,10 +13,18 @@
|
|
|
13
13
|
* in-process origin, and never a wire `machine` account — the one row
|
|
14
14
|
* where the two predicates part, pinned so a step trusting server-composed
|
|
15
15
|
* state can never be widened to the wire by picking the wrong one.
|
|
16
|
+
*
|
|
17
|
+
* And pins `isFirstPartyHumanOperator` (stigmer#1387), the allow-list
|
|
18
|
+
* memory recall and the person's declared preferences read: a wire `user`
|
|
19
|
+
* whose bearer is not a PlatformClient user token. Every other class, an
|
|
20
|
+
* in-process `user` (the open-source schedule fire's shape) and a
|
|
21
|
+
* platform token naming a PlatformClient are excluded; another issuer's
|
|
22
|
+
* token carrying the same claim is not a platform token (stigmer#1312).
|
|
16
23
|
*/
|
|
17
24
|
import { describe, expect, it } from "vitest";
|
|
18
25
|
|
|
19
26
|
import {
|
|
27
|
+
isFirstPartyHumanOperator,
|
|
20
28
|
isPlatformPipelineCaller,
|
|
21
29
|
isServerComposedRequest,
|
|
22
30
|
} from "../identity.js";
|
|
@@ -103,3 +111,77 @@ describe("isServerComposedRequest", () => {
|
|
|
103
111
|
);
|
|
104
112
|
});
|
|
105
113
|
});
|
|
114
|
+
|
|
115
|
+
/** A JWT carrying `payload`, unsigned: the predicate reads, never verifies. */
|
|
116
|
+
function jwtWith(payload: Record<string, unknown>): string {
|
|
117
|
+
const segment = (value: object) =>
|
|
118
|
+
Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
119
|
+
return `${segment({ alg: "RS256", typ: "JWT" })}.${segment(payload)}.signature`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe("isFirstPartyHumanOperator", () => {
|
|
123
|
+
it("is true for a wire user — an OIDC login, an API key, the laptop's operator", () => {
|
|
124
|
+
expect(isFirstPartyHumanOperator(caller({}))).toBe(true);
|
|
125
|
+
expect(isFirstPartyHumanOperator(caller({ origin: "wire" }))).toBe(true);
|
|
126
|
+
expect(
|
|
127
|
+
isFirstPartyHumanOperator(
|
|
128
|
+
caller({
|
|
129
|
+
issuer: "https://issuer.example",
|
|
130
|
+
rawToken: jwtWith({ iss: "https://issuer.example", sub: "carol" }),
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("is false for every other class, a composition's own included", () => {
|
|
137
|
+
for (const callerClass of [
|
|
138
|
+
"machine",
|
|
139
|
+
"runner",
|
|
140
|
+
"internal",
|
|
141
|
+
"guest",
|
|
142
|
+
"channel",
|
|
143
|
+
"schedule",
|
|
144
|
+
"sandbox",
|
|
145
|
+
]) {
|
|
146
|
+
expect(
|
|
147
|
+
isFirstPartyHumanOperator(caller({ callerClass })),
|
|
148
|
+
callerClass,
|
|
149
|
+
).toBe(false);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("is false for a user the server composed a request for (the schedule fire)", () => {
|
|
154
|
+
expect(
|
|
155
|
+
isFirstPartyHumanOperator(
|
|
156
|
+
caller({ callerClass: "user", origin: "in-process" }),
|
|
157
|
+
),
|
|
158
|
+
).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("is false for a PlatformClient user token, and true when another issuer stamps the claim", () => {
|
|
162
|
+
expect(
|
|
163
|
+
isFirstPartyHumanOperator(
|
|
164
|
+
caller({
|
|
165
|
+
issuer: "stigmer",
|
|
166
|
+
rawToken: jwtWith({
|
|
167
|
+
iss: "stigmer",
|
|
168
|
+
sub: "ida_x",
|
|
169
|
+
platform_client_id: "pc_1",
|
|
170
|
+
}),
|
|
171
|
+
}),
|
|
172
|
+
),
|
|
173
|
+
).toBe(false);
|
|
174
|
+
expect(
|
|
175
|
+
isFirstPartyHumanOperator(
|
|
176
|
+
caller({
|
|
177
|
+
issuer: "https://issuer.example",
|
|
178
|
+
rawToken: jwtWith({
|
|
179
|
+
iss: "https://issuer.example",
|
|
180
|
+
sub: "ida_x",
|
|
181
|
+
platform_client_id: "pc_1",
|
|
182
|
+
}),
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* HandlerContext.values (never ambient state — the every-dependency-
|
|
13
13
|
* explicit doctrine).
|
|
14
14
|
*/
|
|
15
|
+
import { USER_TOKEN_CLAIMS } from "../domain/platformclient/constants.js";
|
|
16
|
+
import {
|
|
17
|
+
decodeVerifiedPlatformTokenPayload,
|
|
18
|
+
stringClaim,
|
|
19
|
+
} from "../platformtoken/envelope.js";
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* The caller-class discriminant. OSS knows user / machine / runner plus
|
|
@@ -152,3 +157,45 @@ export function isPlatformPipelineCaller(caller: CallerIdentity): boolean {
|
|
|
152
157
|
export function isServerComposedRequest(caller: CallerIdentity): boolean {
|
|
153
158
|
return caller.callerClass === "internal" || caller.origin === "in-process";
|
|
154
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Whether `caller` is a FIRST-PARTY HUMAN OPERATOR: a person at the
|
|
163
|
+
* console, the CLI or an SDK, speaking for themselves on the wire — the
|
|
164
|
+
* TS rendering of the Java RequestCallerIdentity.isFirstPartyHumanOperator
|
|
165
|
+
* (not machine, not impersonated, no token_type, not a PlatformClient user
|
|
166
|
+
* token). Stated once for every consumer that must tell the person from a
|
|
167
|
+
* lane acting near them: memory recall and the person's declared
|
|
168
|
+
* preferences on execution create (domain/agentexecution/create-steps.ts).
|
|
169
|
+
*
|
|
170
|
+
* An ALLOW-list, so a lane this module has never heard of is excluded by
|
|
171
|
+
* construction: the class must be `user` (every human verifier stamps it —
|
|
172
|
+
* OIDC, API key, the cloud's direct and federated logins; runners, the
|
|
173
|
+
* cloud's token lanes and the platform's own pipelines each carry their
|
|
174
|
+
* own class), the request must come from the wire (the open-source
|
|
175
|
+
* schedule fire acts as its creator with class `user` but rides the
|
|
176
|
+
* in-process transport, which is exactly what `isServerComposedRequest`
|
|
177
|
+
* reads), and the bearer must not be a PlatformClient user token (a
|
|
178
|
+
* person, but spoken for by a third-party client — DD-002 D4 as amended).
|
|
179
|
+
*/
|
|
180
|
+
export function isFirstPartyHumanOperator(caller: CallerIdentity): boolean {
|
|
181
|
+
return (
|
|
182
|
+
caller.callerClass === "user" &&
|
|
183
|
+
!isServerComposedRequest(caller) &&
|
|
184
|
+
!carriesPlatformClientClaim(caller.rawToken)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether the bearer is a platform token naming a PlatformClient. Anything
|
|
190
|
+
* that is not a platform token (no token, an API key, another issuer's
|
|
191
|
+
* JWT) is not one, whatever claims it carries (stigmer#1312). Read from
|
|
192
|
+
* the raw token, never from `platformClientId`, which is audit provenance
|
|
193
|
+
* set by one verifier and not a guard vocabulary (see its field doc).
|
|
194
|
+
*/
|
|
195
|
+
export function carriesPlatformClientClaim(rawToken: string): boolean {
|
|
196
|
+
const payload = decodeVerifiedPlatformTokenPayload(rawToken);
|
|
197
|
+
return (
|
|
198
|
+
payload !== undefined &&
|
|
199
|
+
stringClaim(payload, USER_TOKEN_CLAIMS.platformClientId) !== undefined
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -20,9 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* The scan parses each module with the TypeScript compiler API rather than a
|
|
22
22
|
* regular expression, because a catch block's extent (nested braces, a log
|
|
23
|
-
* line before the throw) is a syntax question.
|
|
24
|
-
*
|
|
25
|
-
* blind site fails here, and so does a fixed site still listed.
|
|
23
|
+
* line before the throw) is a syntax question. The scan must find no blind
|
|
24
|
+
* site at all, so a new one fails here with its module and line.
|
|
26
25
|
*/
|
|
27
26
|
import { readFileSync, readdirSync } from "node:fs";
|
|
28
27
|
import path from "node:path";
|
|
@@ -34,19 +33,6 @@ import { describe, expect, it } from "vitest";
|
|
|
34
33
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
35
34
|
const SRC = path.resolve(HERE, "../..");
|
|
36
35
|
|
|
37
|
-
/** Known blind sites by module (relative to `src`), with their count. */
|
|
38
|
-
const PENDING: ReadonlyMap<string, number> = new Map([
|
|
39
|
-
// Store loads, stigmer/stigmer#1345 (fixed domain by domain).
|
|
40
|
-
["domain/plugin/controller.ts", 1],
|
|
41
|
-
["domain/skill/controller.ts", 1],
|
|
42
|
-
["domain/workflow/controller.ts", 1],
|
|
43
|
-
["domain/workflowinstance/controller.ts", 1],
|
|
44
|
-
// Parent loads through the in-process chain that fold every RPC error,
|
|
45
|
-
// validation included, stigmer/stigmer#1351.
|
|
46
|
-
["domain/agentinstance/steps.ts", 1],
|
|
47
|
-
["domain/workflowinstance/steps.ts", 1],
|
|
48
|
-
]);
|
|
49
|
-
|
|
50
36
|
interface BlindSite {
|
|
51
37
|
readonly module: string;
|
|
52
38
|
readonly line: number;
|
|
@@ -258,7 +244,7 @@ describe("the blind-NotFound rule", () => {
|
|
|
258
244
|
});
|
|
259
245
|
|
|
260
246
|
describe("the server source", () => {
|
|
261
|
-
it("has no blind NotFound
|
|
247
|
+
it("has no blind NotFound", () => {
|
|
262
248
|
const sources = serverSources();
|
|
263
249
|
const constructors = notFoundConstructors(sources);
|
|
264
250
|
const found = new Map<string, number[]>();
|
|
@@ -278,8 +264,6 @@ describe("the server source", () => {
|
|
|
278
264
|
const where = [...found]
|
|
279
265
|
.flatMap(([module, lines]) => lines.map((line) => `${module}:${line}`))
|
|
280
266
|
.join("\n");
|
|
281
|
-
expect(counts, `blind NotFound sites found:\n${where}`).toEqual(
|
|
282
|
-
Object.fromEntries(PENDING),
|
|
283
|
-
);
|
|
267
|
+
expect(counts, `blind NotFound sites found:\n${where}`).toEqual({});
|
|
284
268
|
});
|
|
285
269
|
});
|