@stigmer/server 3.38.3 → 3.39.1-dev.20260928194002
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/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 +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/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/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +7 -3
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/workflow/controller.js +6 -3
- package/dist/domain/workflow/controller.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/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 +161 -0
- package/src/domain/agentinstance/controller.ts +9 -5
- package/src/domain/agentinstance/steps.ts +12 -6
- 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/domain/plugin/__tests__/store-faults.test.ts +104 -0
- package/src/domain/plugin/controller.ts +10 -5
- package/src/domain/skill/__tests__/store-faults.test.ts +103 -0
- package/src/domain/skill/controller.ts +7 -3
- package/src/domain/workflow/__tests__/store-faults.test.ts +107 -0
- package/src/domain/workflow/controller.ts +6 -3
- 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/gate-slots.ts +7 -5
- package/src/index.ts +11 -0
- package/src/pipeline/__tests__/blind-not-found.test.ts +4 -24
- 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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the agent instance's two loads. The
|
|
3
|
+
* visibility-update load: a typed ResourceNotFoundError answers NotFound with
|
|
4
|
+
* the domain's pinned copy (`agent instance not found: <id>`), and any other
|
|
5
|
+
* store failure is an infrastructure fault answered as a sanitized Internal,
|
|
6
|
+
* never a NotFound that tells a client the instance does not exist
|
|
7
|
+
* (stigmer/stigmer#1345). Create's parent-agent load reads the agent through
|
|
8
|
+
* the in-process query client, so the store's typed miss arrives as a Connect
|
|
9
|
+
* NotFound: that answers the pinned `Agent not found: <id>`, and any other
|
|
10
|
+
* failure an Internal, never a missing parent (stigmer/stigmer#1351).
|
|
11
|
+
*
|
|
12
|
+
* Both surfaces are reached through the registered handler on an in-process
|
|
13
|
+
* router. The composed suites reach only a real store, which cannot fail
|
|
14
|
+
* selectively, so the visibility update runs against a store whose read
|
|
15
|
+
* throws and create against a parent loader whose read throws. Whatever the
|
|
16
|
+
* surface does not need is untouchable: a load that fails must stop the call
|
|
17
|
+
* before anything past it runs.
|
|
18
|
+
*
|
|
19
|
+
* Out of scope: the NotFound copies themselves (wire contract).
|
|
20
|
+
*/
|
|
21
|
+
import type { Client } from "@connectrpc/connect";
|
|
22
|
+
import {
|
|
23
|
+
Code,
|
|
24
|
+
ConnectError,
|
|
25
|
+
createClient,
|
|
26
|
+
createRouterTransport,
|
|
27
|
+
} from "@connectrpc/connect";
|
|
28
|
+
import { describe, expect, it } from "vitest";
|
|
29
|
+
|
|
30
|
+
import { AgentInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/command_pb";
|
|
31
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
32
|
+
|
|
33
|
+
import { createLogger } from "../../../boot/logger.js";
|
|
34
|
+
import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
|
|
35
|
+
import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
36
|
+
import {
|
|
37
|
+
errorOf,
|
|
38
|
+
failingStore,
|
|
39
|
+
untouchable,
|
|
40
|
+
} from "../../../pipeline/__tests__/support.js";
|
|
41
|
+
import { INTERNAL_FALLBACK_MESSAGE } from "../../../pipeline/pipeline.js";
|
|
42
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
43
|
+
import { ResourceNotFoundError } from "../../../store/interface.js";
|
|
44
|
+
import type { Store } from "../../../store/interface.js";
|
|
45
|
+
|
|
46
|
+
import { registerAgentInstanceServices } from "../controller.js";
|
|
47
|
+
import type { ParentAgentLoaderProvider } from "../steps.js";
|
|
48
|
+
|
|
49
|
+
const silentLogger = createLogger({
|
|
50
|
+
level: "error",
|
|
51
|
+
pretty: false,
|
|
52
|
+
write: () => {},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const INSTANCE_ID = "ain_storefault";
|
|
56
|
+
const AGENT_ID = "agt_storefault";
|
|
57
|
+
|
|
58
|
+
const NOT_FOUND_COPY = `agent instance not found: ${INSTANCE_ID}`;
|
|
59
|
+
const LOAD_FAULT_COPY = "failed to load agent instance";
|
|
60
|
+
const PARENT_NOT_FOUND_COPY = `Agent not found: ${AGENT_ID}`;
|
|
61
|
+
const PARENT_LOAD_FAULT_COPY = "failed to load parent agent";
|
|
62
|
+
|
|
63
|
+
const MISSING = (): Error =>
|
|
64
|
+
new ResourceNotFoundError(`agent_instance/${INSTANCE_ID}`);
|
|
65
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
66
|
+
|
|
67
|
+
/** What the agent query's `get` answers for a missing and a faulted agent. */
|
|
68
|
+
const PARENT_MISSING = (): Error =>
|
|
69
|
+
new ConnectError(`Agent not found: ${AGENT_ID}`, Code.NotFound);
|
|
70
|
+
const PARENT_FAULTED = (): Error =>
|
|
71
|
+
new ConnectError(INTERNAL_FALLBACK_MESSAGE, Code.Internal);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
75
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
76
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
77
|
+
* the store or the parent.
|
|
78
|
+
*/
|
|
79
|
+
function instanceCommand(
|
|
80
|
+
store: Store,
|
|
81
|
+
parentAgentLoader: ParentAgentLoaderProvider = untouchable(
|
|
82
|
+
"parentAgentLoader",
|
|
83
|
+
),
|
|
84
|
+
): Client<typeof AgentInstanceCommandController> {
|
|
85
|
+
const transport = createRouterTransport(
|
|
86
|
+
(router) => {
|
|
87
|
+
registerAgentInstanceServices(router, {
|
|
88
|
+
store,
|
|
89
|
+
logger: silentLogger,
|
|
90
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
91
|
+
authorizationLifecycle: undefined,
|
|
92
|
+
parentAgentLoader,
|
|
93
|
+
listReadScope: undefined,
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
router: {
|
|
98
|
+
interceptors: [
|
|
99
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
100
|
+
createApiResourceInterceptor(),
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
return createClient(AgentInstanceCommandController, transport);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe("updateVisibility — LoadInstanceForVisibilityUpdate", () => {
|
|
109
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
110
|
+
return errorOf(() =>
|
|
111
|
+
instanceCommand(store).updateVisibility({
|
|
112
|
+
resourceId: INSTANCE_ID,
|
|
113
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
it("a missing agent instance answers NotFound with the domain's copy", async () => {
|
|
119
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
120
|
+
|
|
121
|
+
expect(error.code).toBe(Code.NotFound);
|
|
122
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
126
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
127
|
+
|
|
128
|
+
expect(error.code).toBe(Code.Internal);
|
|
129
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("create — LoadParentAgent", () => {
|
|
134
|
+
function surfaceError(parentError: Error): Promise<ConnectError> {
|
|
135
|
+
const failingParent: ParentAgentLoaderProvider = () => ({
|
|
136
|
+
get: () => Promise.reject(parentError),
|
|
137
|
+
});
|
|
138
|
+
return errorOf(() =>
|
|
139
|
+
instanceCommand(untouchable("store"), failingParent).create({
|
|
140
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
141
|
+
kind: "AgentInstance",
|
|
142
|
+
metadata: { name: "Store Fault Instance", org: "org_storefault" },
|
|
143
|
+
spec: { agentId: AGENT_ID },
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
it("a missing parent agent answers NotFound with the pinned copy", async () => {
|
|
149
|
+
const error = await surfaceError(PARENT_MISSING());
|
|
150
|
+
|
|
151
|
+
expect(error.code).toBe(Code.NotFound);
|
|
152
|
+
expect(error.rawMessage).toBe(PARENT_NOT_FOUND_COPY);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("any other parent-load failure answers a sanitized Internal", async () => {
|
|
156
|
+
const error = await surfaceError(PARENT_FAULTED());
|
|
157
|
+
|
|
158
|
+
expect(error.code).toBe(Code.Internal);
|
|
159
|
+
expect(error.rawMessage).toBe(PARENT_LOAD_FAULT_COPY);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -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
|
},
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* contracts: the parent-agent edge, the immutable agent_id, the
|
|
7
7
|
* default-instance visibility guard, and the org/label list filters.
|
|
8
8
|
*/
|
|
9
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
9
10
|
import { create, fromBinary } from "@bufbuild/protobuf";
|
|
10
11
|
import type { DescMessage } from "@bufbuild/protobuf";
|
|
11
12
|
|
|
@@ -69,7 +70,9 @@ export const PARENT_AGENT_KEY = "parent_agent";
|
|
|
69
70
|
* an agent is a shareable blueprint and one agent legitimately has
|
|
70
71
|
* instances in several orgs (the marketplace case) — the instance's
|
|
71
72
|
* organization is the caller's, and the check below asks the caller's
|
|
72
|
-
* standing there.
|
|
73
|
+
* standing there. The agent is read through the in-process query client, so
|
|
74
|
+
* a missing one arrives as a Connect NotFound; any other failure is a fault
|
|
75
|
+
* answered as Internal, never a missing parent.
|
|
73
76
|
*/
|
|
74
77
|
export function newLoadParentAgentStep(
|
|
75
78
|
agentLoader: ParentAgentLoaderProvider,
|
|
@@ -88,11 +91,14 @@ export function newLoadParentAgentStep(
|
|
|
88
91
|
try {
|
|
89
92
|
parentAgent = await agentLoader().get(agentId);
|
|
90
93
|
} catch (error) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
if (error instanceof ConnectError && error.code === Code.NotFound) {
|
|
95
|
+
logger.warn("Parent agent not found", {
|
|
96
|
+
agentId,
|
|
97
|
+
error: error.rawMessage,
|
|
98
|
+
});
|
|
99
|
+
throw notFoundError("Agent", agentId);
|
|
100
|
+
}
|
|
101
|
+
throw internalError(error, "failed to load parent agent");
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
logger.debug("Loaded parent agent", {
|
|
@@ -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
|
},
|
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pins the organization delete's order through a composed server in the
|
|
3
|
-
* trusted-local posture:
|
|
4
|
-
* before its row, and a fault stops the delete
|
|
5
|
-
* place. An organization's id is its slug and
|
|
6
|
-
*
|
|
3
|
+
* trusted-local posture: the slug is retired first, everything that grants
|
|
4
|
+
* on the organization goes before its row, and a fault stops the delete
|
|
5
|
+
* with the organization in place. An organization's id is its slug, and a
|
|
6
|
+
* slug is never taken again, so nothing a deleted organization left behind
|
|
7
|
+
* can pass to a new holder.
|
|
7
8
|
*
|
|
8
9
|
* - the `org-delete:pre-delete` slot runs with the organization loaded
|
|
9
10
|
* and still stored, and its rows still in place;
|
|
10
11
|
* - a slot step's refusal answers its own code and copy, and leaves the
|
|
11
|
-
* organization and its rows;
|
|
12
|
+
* organization and its rows, its slug already retired;
|
|
13
|
+
* - a fault retiring the slug fails the delete before anything else, with
|
|
14
|
+
* the organization and its rows in place;
|
|
12
15
|
* - the organization's policy rows are revoked before its row, as
|
|
13
16
|
* principal and as resource: a revocation fault answers Internal with
|
|
14
17
|
* fixed copy, leaves the organization and its owner row, and a retry
|
|
15
18
|
* completes the delete;
|
|
16
|
-
* - a
|
|
17
|
-
* organization
|
|
19
|
+
* - a create of a deleted organization's slug is refused with the
|
|
20
|
+
* ORGANIZATION_SLUG_RESERVED reason, and no row of the old organization
|
|
21
|
+
* gains an owner.
|
|
18
22
|
*
|
|
19
23
|
* The policy store is the library's in-memory double, composed as the
|
|
20
24
|
* IamPolicy store driver so a test can make one row's delete fault; the
|
|
@@ -28,13 +32,22 @@ import type { DescMessage } from "@bufbuild/protobuf";
|
|
|
28
32
|
import { Code, ConnectError, createClient } from "@connectrpc/connect";
|
|
29
33
|
import type { Client, Transport } from "@connectrpc/connect";
|
|
30
34
|
import { createGrpcTransport } from "@connectrpc/connect-node";
|
|
31
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
afterAll,
|
|
37
|
+
beforeAll,
|
|
38
|
+
beforeEach,
|
|
39
|
+
describe,
|
|
40
|
+
expect,
|
|
41
|
+
it,
|
|
42
|
+
vi,
|
|
43
|
+
} from "vitest";
|
|
32
44
|
|
|
33
45
|
import { IamPolicyCommandController } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/command_pb";
|
|
34
46
|
import type { IamPolicy } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
35
47
|
import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
36
48
|
import { OrganizationCommandController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/command_pb";
|
|
37
49
|
import { OrganizationQueryController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/query_pb";
|
|
50
|
+
import { ErrorInfoSchema } from "@stigmer/protos/google/rpc/error_details_pb";
|
|
38
51
|
|
|
39
52
|
import { loadConfig } from "../../../boot/config.js";
|
|
40
53
|
import { composeServer } from "../../../boot/compose.js";
|
|
@@ -215,6 +228,36 @@ describe("organization delete (composed server, trusted-local posture)", () => {
|
|
|
215
228
|
(await organizationQuery.get({ value: REFUSED_SLUG })).metadata?.id,
|
|
216
229
|
).toBe(REFUSED_SLUG);
|
|
217
230
|
expect(rowsNaming(REFUSED_SLUG)).toHaveLength(3);
|
|
231
|
+
// The slug was retired before the slot refused; the organization lives
|
|
232
|
+
// on, and its slug is refused to a create either way.
|
|
233
|
+
expect(
|
|
234
|
+
(await server.store.organizationSlugs.find(REFUSED_SLUG))?.retiredAt,
|
|
235
|
+
).not.toBe("");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("a fault retiring the slug fails the delete before anything else, with the organization and its rows in place", async () => {
|
|
239
|
+
await organizationWithRows("delete-retire-faults");
|
|
240
|
+
const retire = vi
|
|
241
|
+
.spyOn(server.store.organizationSlugs, "retire")
|
|
242
|
+
.mockRejectedValueOnce(new Error("ledger unavailable"));
|
|
243
|
+
try {
|
|
244
|
+
const failed = await grpcError(() =>
|
|
245
|
+
organizations.delete({ value: "delete-retire-faults" }),
|
|
246
|
+
);
|
|
247
|
+
expect(failed.code).toBe(Code.Internal);
|
|
248
|
+
expect(failed.rawMessage).toBe("failed to retire the organization slug");
|
|
249
|
+
} finally {
|
|
250
|
+
retire.mockRestore();
|
|
251
|
+
}
|
|
252
|
+
expect(seenBySlot.has("delete-retire-faults")).toBe(false);
|
|
253
|
+
expect(
|
|
254
|
+
(await organizationQuery.get({ value: "delete-retire-faults" })).metadata
|
|
255
|
+
?.id,
|
|
256
|
+
).toBe("delete-retire-faults");
|
|
257
|
+
expect(rowsNaming("delete-retire-faults")).toHaveLength(3);
|
|
258
|
+
|
|
259
|
+
await organizations.delete({ value: "delete-retire-faults" });
|
|
260
|
+
expect(rowsNaming("delete-retire-faults")).toEqual([]);
|
|
218
261
|
});
|
|
219
262
|
|
|
220
263
|
it("a revocation fault fails the delete with the organization and its owner in place, and the retry completes it", async () => {
|
|
@@ -248,19 +291,31 @@ describe("organization delete (composed server, trusted-local posture)", () => {
|
|
|
248
291
|
expect(rowsNaming("delete-faults")).toEqual([]);
|
|
249
292
|
});
|
|
250
293
|
|
|
251
|
-
it("a slug
|
|
294
|
+
it("a deleted organization's slug is never taken again: the create is refused with the reserved reason", async () => {
|
|
252
295
|
await organizationWithRows("delete-reborn");
|
|
253
296
|
await organizations.delete({ value: "delete-reborn" });
|
|
254
297
|
|
|
255
|
-
await
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
298
|
+
const refused = await grpcError(() =>
|
|
299
|
+
organizations.create({
|
|
300
|
+
apiVersion: "tenancy.stigmer.ai/v1",
|
|
301
|
+
kind: "Organization",
|
|
302
|
+
metadata: { name: "delete-reborn", slug: "delete-reborn", org: "" },
|
|
303
|
+
spec: { description: "the slug, taken again" },
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
261
306
|
|
|
262
|
-
expect(
|
|
263
|
-
|
|
264
|
-
|
|
307
|
+
expect(refused.code).toBe(Code.AlreadyExists);
|
|
308
|
+
expect(refused.rawMessage).toBe(
|
|
309
|
+
"Organization slug 'delete-reborn' belonged to an organization that was deleted, and a slug is never reused",
|
|
310
|
+
);
|
|
311
|
+
const [reason] = refused.findDetails(ErrorInfoSchema);
|
|
312
|
+
expect(reason?.reason).toBe("ORGANIZATION_SLUG_RESERVED");
|
|
313
|
+
expect(reason?.domain).toBe("stigmer.ai");
|
|
314
|
+
expect(reason?.metadata).toEqual({ slug: "delete-reborn" });
|
|
315
|
+
expect(rowsNaming("delete-reborn")).toEqual([]);
|
|
316
|
+
const gone = await grpcError(() =>
|
|
317
|
+
organizationQuery.get({ value: "delete-reborn" }),
|
|
318
|
+
);
|
|
319
|
+
expect(gone.code).toBe(Code.NotFound);
|
|
265
320
|
});
|
|
266
321
|
});
|