@stigmer/server 3.38.3 → 3.39.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/domain/agent/controller.d.ts +2 -2
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js +7 -3
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentchannel/controller.d.ts +2 -2
- package/dist/domain/agentchannel/controller.d.ts.map +1 -1
- package/dist/domain/agentchannel/controller.js +10 -5
- package/dist/domain/agentchannel/controller.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +2 -2
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js +7 -3
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts +2 -2
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +7 -3
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js +33 -14
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/organization/slug-ledger.d.ts +86 -0
- package/dist/domain/organization/slug-ledger.d.ts.map +1 -0
- package/dist/domain/organization/slug-ledger.js +128 -0
- package/dist/domain/organization/slug-ledger.js.map +1 -0
- package/dist/domain/organization/steps.d.ts +12 -3
- package/dist/domain/organization/steps.d.ts.map +1 -1
- package/dist/domain/organization/steps.js +28 -6
- package/dist/domain/organization/steps.js.map +1 -1
- package/dist/extensions/gate-slots.d.ts +7 -5
- package/dist/extensions/gate-slots.d.ts.map +1 -1
- package/dist/extensions/gate-slots.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/errors.d.ts +8 -0
- package/dist/pipeline/errors.d.ts.map +1 -1
- package/dist/pipeline/errors.js +19 -0
- package/dist/pipeline/errors.js.map +1 -1
- package/dist/store/interface.d.ts +67 -0
- package/dist/store/interface.d.ts.map +1 -1
- package/dist/store/interface.js.map +1 -1
- package/dist/store/organization-slug-history.d.ts +71 -0
- package/dist/store/organization-slug-history.d.ts.map +1 -0
- package/dist/store/organization-slug-history.js +88 -0
- package/dist/store/organization-slug-history.js.map +1 -0
- package/dist/store/postgres/migrations.d.ts +3 -1
- package/dist/store/postgres/migrations.d.ts.map +1 -1
- package/dist/store/postgres/migrations.js +69 -1
- package/dist/store/postgres/migrations.js.map +1 -1
- package/dist/store/postgres/store.d.ts +2 -1
- package/dist/store/postgres/store.d.ts.map +1 -1
- package/dist/store/postgres/store.js +49 -0
- package/dist/store/postgres/store.js.map +1 -1
- package/dist/store/sqlite/migrations.d.ts +3 -1
- package/dist/store/sqlite/migrations.d.ts.map +1 -1
- package/dist/store/sqlite/migrations.js +54 -1
- package/dist/store/sqlite/migrations.js.map +1 -1
- package/dist/store/sqlite/store.d.ts +2 -1
- package/dist/store/sqlite/store.d.ts.map +1 -1
- package/dist/store/sqlite/store.js +55 -0
- package/dist/store/sqlite/store.js.map +1 -1
- package/package.json +6 -6
- package/src/domain/agent/__tests__/store-faults.test.ts +105 -0
- package/src/domain/agent/controller.ts +9 -5
- package/src/domain/agentchannel/__tests__/store-faults.test.ts +126 -0
- package/src/domain/agentchannel/controller.ts +12 -7
- package/src/domain/agentinstance/__tests__/store-faults.test.ts +110 -0
- package/src/domain/agentinstance/controller.ts +9 -5
- package/src/domain/agentshare/__tests__/store-faults.test.ts +101 -0
- package/src/domain/agentshare/controller.ts +9 -5
- package/src/domain/organization/__tests__/organization-delete.test.ts +73 -18
- package/src/domain/organization/__tests__/organization-slugs.test.ts +386 -0
- package/src/domain/organization/controller.ts +36 -14
- package/src/domain/organization/slug-ledger.ts +209 -0
- package/src/domain/organization/steps.ts +28 -6
- package/src/extensions/gate-slots.ts +7 -5
- package/src/index.ts +11 -0
- package/src/pipeline/__tests__/blind-not-found.test.ts +0 -4
- package/src/pipeline/errors.ts +23 -0
- package/src/store/__tests__/organization-slug-history.test.ts +109 -0
- package/src/store/__tests__/store-contract.ts +86 -1
- package/src/store/interface.ts +70 -0
- package/src/store/organization-slug-history.ts +159 -0
- package/src/store/postgres/__tests__/migrations.test.ts +138 -2
- package/src/store/postgres/__tests__/store-contract.test.ts +1 -0
- package/src/store/postgres/migrations.ts +84 -1
- package/src/store/postgres/store.ts +84 -0
- package/src/store/sqlite/__tests__/migrations.test.ts +139 -10
- package/src/store/sqlite/migrations.ts +69 -1
- package/src/store/sqlite/store.ts +84 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.39.0",
|
|
4
4
|
"description": "The Stigmer OSS control plane as a composable library: composeServer, the extension registry and the driver seams a composition builds on. The self-contained deployable of the same server is @stigmer/server-slim.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -63,11 +63,11 @@
|
|
|
63
63
|
"@connectrpc/connect-node": "^2.0.0",
|
|
64
64
|
"@kubernetes/client-node": "2.0.0",
|
|
65
65
|
"@opentelemetry/api": "^1.9.0",
|
|
66
|
-
"@stigmer/outbound": "3.
|
|
67
|
-
"@stigmer/plugin-package": "3.
|
|
68
|
-
"@stigmer/protos": "3.
|
|
69
|
-
"@stigmer/temporal-codecs": "3.
|
|
70
|
-
"@stigmer/zip-structure": "3.
|
|
66
|
+
"@stigmer/outbound": "3.39.0",
|
|
67
|
+
"@stigmer/plugin-package": "3.39.0",
|
|
68
|
+
"@stigmer/protos": "3.39.0",
|
|
69
|
+
"@stigmer/temporal-codecs": "3.39.0",
|
|
70
|
+
"@stigmer/zip-structure": "3.39.0",
|
|
71
71
|
"@temporalio/activity": "1.16.2",
|
|
72
72
|
"@temporalio/client": "1.16.2",
|
|
73
73
|
"@temporalio/common": "1.16.2",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the agent's visibility-update load: a
|
|
3
|
+
* typed ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`agent not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound
|
|
6
|
+
* that tells a client the agent 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 default-instance applier is untouchable: a load that fails must stop
|
|
12
|
+
* the call before anything past it runs.
|
|
13
|
+
*
|
|
14
|
+
* Out of scope: the NotFound copy itself (wire contract) and getByAgent's
|
|
15
|
+
* empty-list answer on an agent-read fault in the channel and share domains
|
|
16
|
+
* (stigmer/stigmer#1375).
|
|
17
|
+
*/
|
|
18
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
19
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
20
|
+
import { describe, expect, it } from "vitest";
|
|
21
|
+
|
|
22
|
+
import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
|
|
23
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
24
|
+
|
|
25
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
26
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
27
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
28
|
+
import {
|
|
29
|
+
errorOf,
|
|
30
|
+
failingStore,
|
|
31
|
+
untouchable,
|
|
32
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
33
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
34
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
35
|
+
import type { Store } from "../../../store/interface.js";
|
|
36
|
+
|
|
37
|
+
import { registerAgentServices } from "../controller.js";
|
|
38
|
+
|
|
39
|
+
const silentLogger = createLogger({
|
|
40
|
+
level: "error",
|
|
41
|
+
pretty: false,
|
|
42
|
+
write: () => {},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const AGENT_ID = "agt_storefault";
|
|
46
|
+
|
|
47
|
+
const NOT_FOUND_COPY = `agent not found: ${AGENT_ID}`;
|
|
48
|
+
const LOAD_FAULT_COPY = "failed to load agent";
|
|
49
|
+
|
|
50
|
+
const MISSING = (): Error => new ResourceNotFoundError(`agent/${AGENT_ID}`);
|
|
51
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
55
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
56
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
57
|
+
* the store.
|
|
58
|
+
*/
|
|
59
|
+
function agentCommand(store: Store): Client<typeof AgentCommandController> {
|
|
60
|
+
const transport = createRouterTransport(
|
|
61
|
+
(router) => {
|
|
62
|
+
registerAgentServices(router, {
|
|
63
|
+
store,
|
|
64
|
+
logger: silentLogger,
|
|
65
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
66
|
+
authorizationLifecycle: undefined,
|
|
67
|
+
agentInstanceApplier: untouchable("agentInstanceApplier"),
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
router: {
|
|
72
|
+
interceptors: [
|
|
73
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
74
|
+
createApiResourceInterceptor(),
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
return createClient(AgentCommandController, transport);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("updateVisibility — LoadAgentForVisibilityUpdate", () => {
|
|
83
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
84
|
+
return errorOf(() =>
|
|
85
|
+
agentCommand(store).updateVisibility({
|
|
86
|
+
resourceId: AGENT_ID,
|
|
87
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
it("a missing agent answers NotFound with the domain's copy", async () => {
|
|
93
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
94
|
+
|
|
95
|
+
expect(error.code).toBe(Code.NotFound);
|
|
96
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
100
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
101
|
+
|
|
102
|
+
expect(error.code).toBe(Code.Internal);
|
|
103
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* agent to serve: a session with no agent runs the built-in assistant.
|
|
9
9
|
*
|
|
10
10
|
* Pipeline per RPC mirrors the Go step chains character-for-character.
|
|
11
|
-
* Proven by agent.conformance.test.ts (CONFORMANCE_TARGET=local)
|
|
12
|
-
* __tests__/agent.test.ts.
|
|
11
|
+
* Proven by agent.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
12
|
+
* __tests__/agent.test.ts and __tests__/store-faults.test.ts.
|
|
13
13
|
*
|
|
14
14
|
* Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies, and
|
|
15
15
|
* Publish steps (no multi-tenant auth, IAM/FGA, or event publishing here).
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
newValidateVisibilityStep,
|
|
92
92
|
newValidateVisibilityUpdateStep,
|
|
93
93
|
} from "../../pipeline/steps/validate-visibility.js";
|
|
94
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
94
95
|
import type { Store } from "../../store/interface.js";
|
|
95
96
|
import { agentSearchExtractor } from "./search-extractor.js";
|
|
96
97
|
import {
|
|
@@ -382,7 +383,7 @@ async function updateVisibility(
|
|
|
382
383
|
return reqCtx.get(UPDATE_VISIBILITY_AGENT_KEY) as Agent;
|
|
383
384
|
}
|
|
384
385
|
|
|
385
|
-
/** Loads the agent by resource_id;
|
|
386
|
+
/** Loads the agent by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
386
387
|
function newLoadAgentForVisibilityUpdateStep(
|
|
387
388
|
store: Store,
|
|
388
389
|
): PipelineStep<UpdateVisibilityDesc> {
|
|
@@ -397,8 +398,11 @@ function newLoadAgentForVisibilityUpdateStep(
|
|
|
397
398
|
input.resourceId,
|
|
398
399
|
AgentSchema,
|
|
399
400
|
);
|
|
400
|
-
} catch {
|
|
401
|
-
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error instanceof ResourceNotFoundError) {
|
|
403
|
+
throw notFoundError("agent", input.resourceId);
|
|
404
|
+
}
|
|
405
|
+
throw internalError(error, "failed to load agent");
|
|
402
406
|
}
|
|
403
407
|
ctx.set(UPDATE_VISIBILITY_AGENT_KEY, agent);
|
|
404
408
|
},
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the install lane's channel load, which
|
|
3
|
+
* initiateInstall and completeInstall share: a typed ResourceNotFoundError
|
|
4
|
+
* answers NotFound with the domain's pinned copy
|
|
5
|
+
* (`AgentChannel not found: <id>`), and any other store failure is an
|
|
6
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound
|
|
7
|
+
* that tells a client the channel does not exist (stigmer/stigmer#1345).
|
|
8
|
+
*
|
|
9
|
+
* Both surfaces are reached through the registered handler on an in-process
|
|
10
|
+
* router. The composed suites reach only a real store, which cannot fail
|
|
11
|
+
* selectively, so each runs here against a store whose read throws. The
|
|
12
|
+
* channel runtime and the model registry are untouchable: the load comes
|
|
13
|
+
* before authorization and before the runtime, so a load that fails must
|
|
14
|
+
* stop the call before either.
|
|
15
|
+
*
|
|
16
|
+
* Out of scope: the NotFound copy itself (wire contract, pinned by
|
|
17
|
+
* agentchannel.test.ts and the agentchannel conformance suite) and
|
|
18
|
+
* getByAgent's empty-list answer on an agent-read fault
|
|
19
|
+
* (stigmer/stigmer#1375).
|
|
20
|
+
*/
|
|
21
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
22
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
23
|
+
import { describe, expect, it } from "vitest";
|
|
24
|
+
|
|
25
|
+
import { AgentChannelCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/command_pb";
|
|
26
|
+
|
|
27
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
28
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
29
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
30
|
+
import {
|
|
31
|
+
errorOf,
|
|
32
|
+
failingStore,
|
|
33
|
+
untouchable,
|
|
34
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
35
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
36
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
37
|
+
import type { Store } from "../../../store/interface.js";
|
|
38
|
+
|
|
39
|
+
import type { ChannelRuntime } from "../channel-runtime.js";
|
|
40
|
+
import { registerAgentChannelServices } from "../controller.js";
|
|
41
|
+
|
|
42
|
+
const silentLogger = createLogger({
|
|
43
|
+
level: "error",
|
|
44
|
+
pretty: false,
|
|
45
|
+
write: () => {},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const CHANNEL_ID = "ach_storefault";
|
|
49
|
+
|
|
50
|
+
const NOT_FOUND_COPY = `AgentChannel not found: ${CHANNEL_ID}`;
|
|
51
|
+
const LOAD_FAULT_COPY = "failed to load agent channel";
|
|
52
|
+
|
|
53
|
+
const MISSING = (): Error =>
|
|
54
|
+
new ResourceNotFoundError(`agent_channel/${CHANNEL_ID}`);
|
|
55
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
56
|
+
|
|
57
|
+
type ChannelCommand = Client<typeof AgentChannelCommandController>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
61
|
+
* the trusted-local caller and the apiresource interceptor the kind the
|
|
62
|
+
* install load reads; the load is the first thing either handler does.
|
|
63
|
+
*/
|
|
64
|
+
function channelCommand(store: Store): ChannelCommand {
|
|
65
|
+
const transport = createRouterTransport(
|
|
66
|
+
(router) => {
|
|
67
|
+
registerAgentChannelServices(router, {
|
|
68
|
+
store,
|
|
69
|
+
logger: silentLogger,
|
|
70
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
71
|
+
authorizationLifecycle: undefined,
|
|
72
|
+
modelRegistry: untouchable("modelRegistry"),
|
|
73
|
+
channelRuntime: untouchable<ChannelRuntime>("channelRuntime"),
|
|
74
|
+
listReadScope: undefined,
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
router: {
|
|
79
|
+
interceptors: [
|
|
80
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
81
|
+
createApiResourceInterceptor(),
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
return createClient(AgentChannelCommandController, transport);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Each surface with the input its proto requires. */
|
|
90
|
+
const SURFACES: ReadonlyArray<
|
|
91
|
+
readonly [string, (command: ChannelCommand) => Promise<unknown>]
|
|
92
|
+
> = [
|
|
93
|
+
[
|
|
94
|
+
"initiateInstall — the install load",
|
|
95
|
+
(command) => command.initiateInstall({ resourceId: CHANNEL_ID }),
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
"completeInstall — the install load",
|
|
99
|
+
(command) =>
|
|
100
|
+
command.completeInstall({
|
|
101
|
+
resourceId: CHANNEL_ID,
|
|
102
|
+
state: "install-state",
|
|
103
|
+
code: "provider-code",
|
|
104
|
+
}),
|
|
105
|
+
],
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
describe.each(SURFACES)("%s", (_name, call) => {
|
|
109
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
110
|
+
return errorOf(() => call(channelCommand(store)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
it("a missing channel answers NotFound with the domain's copy", async () => {
|
|
114
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
115
|
+
|
|
116
|
+
expect(error.code).toBe(Code.NotFound);
|
|
117
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
121
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
122
|
+
|
|
123
|
+
expect(error.code).toBe(Code.Internal);
|
|
124
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* (only same-org shares are) — a dangling channel is tolerated because
|
|
24
24
|
* OSS has no serving runtime and cloud fails closed elsewhere.
|
|
25
25
|
*
|
|
26
|
-
* Proven by agentchannel.conformance.test.ts (CONFORMANCE_TARGET=local)
|
|
27
|
-
* and __tests__/
|
|
26
|
+
* Proven by agentchannel.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
27
|
+
* __tests__/agentchannel.test.ts and __tests__/store-faults.test.ts.
|
|
28
28
|
*/
|
|
29
29
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
30
30
|
import { create, fromBinary } from "@bufbuild/protobuf";
|
|
@@ -108,6 +108,7 @@ import { newPersistStep } from "../../pipeline/steps/persist.js";
|
|
|
108
108
|
import { newResolveSlugStep } from "../../pipeline/steps/slug.js";
|
|
109
109
|
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
110
110
|
import { newValidateVisibilityStep } from "../../pipeline/steps/validate-visibility.js";
|
|
111
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
111
112
|
import type { Store } from "../../store/interface.js";
|
|
112
113
|
import type { ModelCatalogProvider } from "../workflow/registry/model-catalog-provider.js";
|
|
113
114
|
import type { ChannelRuntime } from "./channel-runtime.js";
|
|
@@ -395,9 +396,10 @@ async function completeInstall(
|
|
|
395
396
|
}
|
|
396
397
|
|
|
397
398
|
/**
|
|
398
|
-
* Loads the install target — cloud's LoadChannel NOT_FOUND, verbatim
|
|
399
|
-
*
|
|
400
|
-
* second load (the
|
|
399
|
+
* Loads the install target — cloud's LoadChannel NOT_FOUND, verbatim, for
|
|
400
|
+
* a missing channel; a store fault answers Internal. Returns the loaded
|
|
401
|
+
* channel so a composed runtime receives it without a second load (the
|
|
402
|
+
* load-then-X contract stays OSS-owned).
|
|
401
403
|
*/
|
|
402
404
|
async function loadChannelForInstall(
|
|
403
405
|
deps: AgentChannelControllerDeps,
|
|
@@ -410,8 +412,11 @@ async function loadChannelForInstall(
|
|
|
410
412
|
resourceId,
|
|
411
413
|
AgentChannelSchema,
|
|
412
414
|
);
|
|
413
|
-
} catch {
|
|
414
|
-
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if (error instanceof ResourceNotFoundError) {
|
|
417
|
+
throw notFoundError("AgentChannel", resourceId);
|
|
418
|
+
}
|
|
419
|
+
throw internalError(error, "failed to load agent channel");
|
|
415
420
|
}
|
|
416
421
|
}
|
|
417
422
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the agent instance's visibility-update
|
|
3
|
+
* load: a typed ResourceNotFoundError answers NotFound with the domain's
|
|
4
|
+
* pinned copy (`agent instance not found: <id>`), and any other store failure
|
|
5
|
+
* is an infrastructure fault answered as a sanitized Internal, never a
|
|
6
|
+
* NotFound that tells a client the instance does not exist
|
|
7
|
+
* (stigmer/stigmer#1345).
|
|
8
|
+
*
|
|
9
|
+
* The surface is updateVisibility, reached through the registered handler on
|
|
10
|
+
* an in-process router. The composed suites reach only a real store, which
|
|
11
|
+
* cannot fail selectively, so it runs here against a store whose read throws.
|
|
12
|
+
* The parent-agent loader is untouchable: a load that fails must stop the
|
|
13
|
+
* call before anything past it runs.
|
|
14
|
+
*
|
|
15
|
+
* Out of scope: the NotFound copy itself (wire contract) and create's
|
|
16
|
+
* parent-agent load, which folds every RPC error into NotFound
|
|
17
|
+
* (stigmer/stigmer#1351).
|
|
18
|
+
*/
|
|
19
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
20
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
|
|
23
|
+
import { AgentInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/command_pb";
|
|
24
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
25
|
+
|
|
26
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
27
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
28
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
29
|
+
import {
|
|
30
|
+
errorOf,
|
|
31
|
+
failingStore,
|
|
32
|
+
untouchable,
|
|
33
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
34
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
35
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
36
|
+
import type { Store } from "../../../store/interface.js";
|
|
37
|
+
|
|
38
|
+
import { registerAgentInstanceServices } from "../controller.js";
|
|
39
|
+
|
|
40
|
+
const silentLogger = createLogger({
|
|
41
|
+
level: "error",
|
|
42
|
+
pretty: false,
|
|
43
|
+
write: () => {},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const INSTANCE_ID = "ain_storefault";
|
|
47
|
+
|
|
48
|
+
const NOT_FOUND_COPY = `agent instance not found: ${INSTANCE_ID}`;
|
|
49
|
+
const LOAD_FAULT_COPY = "failed to load agent instance";
|
|
50
|
+
|
|
51
|
+
const MISSING = (): Error =>
|
|
52
|
+
new ResourceNotFoundError(`agent_instance/${INSTANCE_ID}`);
|
|
53
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
57
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
58
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
59
|
+
* the store.
|
|
60
|
+
*/
|
|
61
|
+
function instanceCommand(
|
|
62
|
+
store: Store,
|
|
63
|
+
): Client<typeof AgentInstanceCommandController> {
|
|
64
|
+
const transport = createRouterTransport(
|
|
65
|
+
(router) => {
|
|
66
|
+
registerAgentInstanceServices(router, {
|
|
67
|
+
store,
|
|
68
|
+
logger: silentLogger,
|
|
69
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
70
|
+
authorizationLifecycle: undefined,
|
|
71
|
+
parentAgentLoader: untouchable("parentAgentLoader"),
|
|
72
|
+
listReadScope: undefined,
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
router: {
|
|
77
|
+
interceptors: [
|
|
78
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
79
|
+
createApiResourceInterceptor(),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
return createClient(AgentInstanceCommandController, transport);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("updateVisibility — LoadInstanceForVisibilityUpdate", () => {
|
|
88
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
89
|
+
return errorOf(() =>
|
|
90
|
+
instanceCommand(store).updateVisibility({
|
|
91
|
+
resourceId: INSTANCE_ID,
|
|
92
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
it("a missing agent instance answers NotFound with the domain's copy", async () => {
|
|
98
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
99
|
+
|
|
100
|
+
expect(error.code).toBe(Code.NotFound);
|
|
101
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
105
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
106
|
+
|
|
107
|
+
expect(error.code).toBe(Code.Internal);
|
|
108
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* refuses default instances outright (stigmer/stigmer#556).
|
|
8
8
|
*
|
|
9
9
|
* Pipeline per RPC mirrors the Go step chains character-for-character.
|
|
10
|
-
* Proven by agentinstance.conformance.test.ts (CONFORMANCE_TARGET=local)
|
|
11
|
-
* and __tests__/
|
|
10
|
+
* Proven by agentinstance.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
11
|
+
* __tests__/agentinstance.test.ts and __tests__/store-faults.test.ts.
|
|
12
12
|
*
|
|
13
13
|
* Versus Stigmer Cloud, OSS excludes the CreateIamPolicies and Publish
|
|
14
14
|
* steps. Deliberately NO same-org rule on create, unlike WorkflowInstance:
|
|
@@ -98,6 +98,7 @@ import {
|
|
|
98
98
|
newValidateVisibilityStep,
|
|
99
99
|
newValidateVisibilityUpdateStep,
|
|
100
100
|
} from "../../pipeline/steps/validate-visibility.js";
|
|
101
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
101
102
|
import type { Store } from "../../store/interface.js";
|
|
102
103
|
import { agentInstanceSearchExtractor } from "./search-extractor.js";
|
|
103
104
|
import {
|
|
@@ -398,7 +399,7 @@ async function updateVisibility(
|
|
|
398
399
|
return reqCtx.get(UPDATE_VISIBILITY_INSTANCE_KEY) as AgentInstance;
|
|
399
400
|
}
|
|
400
401
|
|
|
401
|
-
/** Loads the agent instance by resource_id;
|
|
402
|
+
/** Loads the agent instance by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
402
403
|
function newLoadInstanceForVisibilityUpdateStep(
|
|
403
404
|
store: Store,
|
|
404
405
|
): PipelineStep<UpdateVisibilityDesc> {
|
|
@@ -413,8 +414,11 @@ function newLoadInstanceForVisibilityUpdateStep(
|
|
|
413
414
|
input.resourceId,
|
|
414
415
|
AgentInstanceSchema,
|
|
415
416
|
);
|
|
416
|
-
} catch {
|
|
417
|
-
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (error instanceof ResourceNotFoundError) {
|
|
419
|
+
throw notFoundError("agent instance", input.resourceId);
|
|
420
|
+
}
|
|
421
|
+
throw internalError(error, "failed to load agent instance");
|
|
418
422
|
}
|
|
419
423
|
ctx.set(UPDATE_VISIBILITY_INSTANCE_KEY, instance);
|
|
420
424
|
},
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the share's link-rotation load: a typed
|
|
3
|
+
* ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`AgentShare not found: <id>`), and any other store failure is an
|
|
5
|
+
* infrastructure fault answered as a sanitized Internal, never a NotFound
|
|
6
|
+
* that tells a client the share does not exist (stigmer/stigmer#1345).
|
|
7
|
+
* Rotation is how an owner kills a leaked link, so a fault that reads as a
|
|
8
|
+
* missing share would leave the link live behind a false answer.
|
|
9
|
+
*
|
|
10
|
+
* The surface is rotateShareLink, reached through the registered handler on
|
|
11
|
+
* an in-process router. The composed suites reach only a real store, which
|
|
12
|
+
* cannot fail selectively, so it runs here against a store whose read throws.
|
|
13
|
+
*
|
|
14
|
+
* Out of scope: the NotFound copy itself (wire contract, pinned by the
|
|
15
|
+
* agentshare conformance suite), the anonymous profile lanes' deliberate
|
|
16
|
+
* uniform NotFound, and getByAgent's empty-list answer on an agent-read
|
|
17
|
+
* fault (stigmer/stigmer#1375).
|
|
18
|
+
*/
|
|
19
|
+
import type { Client, ConnectError } from "@connectrpc/connect";
|
|
20
|
+
import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
|
|
23
|
+
import { AgentShareCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/command_pb";
|
|
24
|
+
|
|
25
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
26
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
27
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
28
|
+
import { errorOf, failingStore } from "../../../pipeline/__tests__/support.js";
|
|
29
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
30
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
31
|
+
import type { Store } from "../../../store/interface.js";
|
|
32
|
+
|
|
33
|
+
import { registerAgentShareServices } from "../controller.js";
|
|
34
|
+
|
|
35
|
+
const silentLogger = createLogger({
|
|
36
|
+
level: "error",
|
|
37
|
+
pretty: false,
|
|
38
|
+
write: () => {},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const SHARE_ID = "ash_storefault";
|
|
42
|
+
|
|
43
|
+
const NOT_FOUND_COPY = `AgentShare not found: ${SHARE_ID}`;
|
|
44
|
+
const LOAD_FAULT_COPY = "failed to load agent share";
|
|
45
|
+
|
|
46
|
+
const MISSING = (): Error =>
|
|
47
|
+
new ResourceNotFoundError(`agent_share/${SHARE_ID}`);
|
|
48
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
52
|
+
* the trusted-local caller and the apiresource interceptor the kind the load
|
|
53
|
+
* reads, then Authorize and ValidateProto run and the load is the first step
|
|
54
|
+
* that reads the store.
|
|
55
|
+
*/
|
|
56
|
+
function shareCommand(
|
|
57
|
+
store: Store,
|
|
58
|
+
): Client<typeof AgentShareCommandController> {
|
|
59
|
+
const transport = createRouterTransport(
|
|
60
|
+
(router) => {
|
|
61
|
+
registerAgentShareServices(router, {
|
|
62
|
+
store,
|
|
63
|
+
logger: silentLogger,
|
|
64
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
65
|
+
authorizationLifecycle: undefined,
|
|
66
|
+
listReadScope: undefined,
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
router: {
|
|
71
|
+
interceptors: [
|
|
72
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
73
|
+
createApiResourceInterceptor(),
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
return createClient(AgentShareCommandController, transport);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("rotateShareLink — LoadShareForLinkRotation", () => {
|
|
82
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
83
|
+
return errorOf(() =>
|
|
84
|
+
shareCommand(store).rotateShareLink({ resourceId: SHARE_ID }),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
it("a missing share answers NotFound with the domain's copy", async () => {
|
|
89
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
90
|
+
|
|
91
|
+
expect(error.code).toBe(Code.NotFound);
|
|
92
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
96
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
97
|
+
|
|
98
|
+
expect(error.code).toBe(Code.Internal);
|
|
99
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
* cloud edition enforces the same contracts via FGA plus app-level gates
|
|
19
19
|
* for the anonymous resolution paths.
|
|
20
20
|
*
|
|
21
|
-
* Proven by agentshare.conformance.test.ts (CONFORMANCE_TARGET=local)
|
|
22
|
-
* and __tests__/
|
|
21
|
+
* Proven by agentshare.conformance.test.ts (CONFORMANCE_TARGET=local),
|
|
22
|
+
* __tests__/agentshare.test.ts and __tests__/store-faults.test.ts.
|
|
23
23
|
*/
|
|
24
24
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
25
25
|
import { create, fromBinary } from "@bufbuild/protobuf";
|
|
@@ -105,6 +105,7 @@ import { newPersistStep } from "../../pipeline/steps/persist.js";
|
|
|
105
105
|
import { newResolveSlugStep } from "../../pipeline/steps/slug.js";
|
|
106
106
|
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
107
107
|
import { newValidateVisibilityStep } from "../../pipeline/steps/validate-visibility.js";
|
|
108
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
108
109
|
import type { Store } from "../../store/interface.js";
|
|
109
110
|
import { ORG_REQUIRED_FOR_LOOKUP_MESSAGE } from "./constants.js";
|
|
110
111
|
import {
|
|
@@ -335,7 +336,7 @@ async function rotateShareLink(
|
|
|
335
336
|
return reqCtx.get(ROTATE_SHARE_KEY) as AgentShare;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
/** Loads the share by resource_id;
|
|
339
|
+
/** Loads the share by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
339
340
|
function newLoadShareForLinkRotationStep(
|
|
340
341
|
store: Store,
|
|
341
342
|
): PipelineStep<RotateDesc> {
|
|
@@ -349,8 +350,11 @@ function newLoadShareForLinkRotationStep(
|
|
|
349
350
|
ctx.input.resourceId,
|
|
350
351
|
AgentShareSchema,
|
|
351
352
|
);
|
|
352
|
-
} catch {
|
|
353
|
-
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (error instanceof ResourceNotFoundError) {
|
|
355
|
+
throw notFoundError("AgentShare", ctx.input.resourceId);
|
|
356
|
+
}
|
|
357
|
+
throw internalError(error, "failed to load agent share");
|
|
354
358
|
}
|
|
355
359
|
ctx.set(ROTATE_SHARE_KEY, share);
|
|
356
360
|
},
|