@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
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Proven by memory.conformance.test.ts (CONFORMANCE_TARGET=local) and
|
|
18
18
|
* __tests__/memory.test.ts.
|
|
19
19
|
*/
|
|
20
|
-
import { create, equals, fromBinary } from "@bufbuild/protobuf";
|
|
20
|
+
import { create, equals, fromBinary, isMessage } from "@bufbuild/protobuf";
|
|
21
21
|
import { timestampNow } from "@bufbuild/protobuf/wkt";
|
|
22
22
|
import type { ConnectError } from "@connectrpc/connect";
|
|
23
23
|
|
|
@@ -30,6 +30,8 @@ import { MemoryListSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/i
|
|
|
30
30
|
import { MemoryProvenanceSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/spec_pb";
|
|
31
31
|
import { MemoryStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/memory/v1/status_pb";
|
|
32
32
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
33
|
+
import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
34
|
+
import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
33
35
|
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
34
36
|
import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
35
37
|
|
|
@@ -52,10 +54,16 @@ import {
|
|
|
52
54
|
import { memoryCaptureCredentialOf } from "../../pipeline/steps/guard-memory-capture.js";
|
|
53
55
|
import { compareCreatedAtDesc } from "../../pipeline/steps/helpers.js";
|
|
54
56
|
import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
|
|
57
|
+
import type { AccountsByCaller } from "../identityaccount/resolve.js";
|
|
58
|
+
import {
|
|
59
|
+
accountForCaller,
|
|
60
|
+
accountForStamp,
|
|
61
|
+
} from "../identityaccount/resolve.js";
|
|
55
62
|
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
56
63
|
import type { Store } from "../../store/interface.js";
|
|
57
64
|
import {
|
|
58
65
|
MAX_MEMORIES_PER_SUBJECT,
|
|
66
|
+
MEMORY_ACCOUNT_DISABLED_MESSAGE,
|
|
59
67
|
MEMORY_FULL_MESSAGE,
|
|
60
68
|
MEMORY_PROVENANCE_IMMUTABLE_MESSAGE,
|
|
61
69
|
MEMORY_SUBJECT_IMMUTABLE_MESSAGE,
|
|
@@ -77,10 +85,22 @@ export const LIST_RESULT_KEY = "listResult";
|
|
|
77
85
|
* memories are id-addressed records (the remember tool sends content
|
|
78
86
|
* only), and the platform's slug machinery requires a name. A
|
|
79
87
|
* client-supplied name still wins — the default only fills absence.
|
|
80
|
-
* 3. Overwrites spec.subject_identity_account_id
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
88
|
+
* 3. Overwrites spec.subject_identity_account_id, server-derived and
|
|
89
|
+
* never client-supplied (DD-005 D2), by posture. Under the
|
|
90
|
+
* single-operator posture (no `personAccounts` composed) it is the
|
|
91
|
+
* empty-string sentinel — the laptop's one subject (the OAuth grant
|
|
92
|
+
* store convention). Where callers are persons it is the id of the
|
|
93
|
+
* identity account the memory is ABOUT (stigmer#1387, the Java
|
|
94
|
+
* MemoryCreateHandler's subject): the admitted capture credential's
|
|
95
|
+
* subject (point 5), else the calling person — each resolved through
|
|
96
|
+
* the identity-account domain (accountForStamp / accountForCaller),
|
|
97
|
+
* so a subject that arrived as a raw issuer subject is filed under
|
|
98
|
+
* the account every FGA tuple names. The resolved account rides the
|
|
99
|
+
* context under MEMORY_SUBJECT_ACCOUNT_KEY for CheckMemoryEnablement,
|
|
100
|
+
* so the subject stamped and the consent checked are one row, read
|
|
101
|
+
* once. A caller no account stands for is stamped "" and NOT refused
|
|
102
|
+
* here: the refusal belongs behind AuthorizeResolvedTarget, which
|
|
103
|
+
* runs between the two steps so a refused caller learns nothing.
|
|
84
104
|
* 4. Stores spec.provenance as supplied (Stage 3 provenance decision,
|
|
85
105
|
* owner-ratified 2026-08-22): the capture path — the remember tool
|
|
86
106
|
* via the runner-synthesized attachment — threads the agent/session/
|
|
@@ -101,15 +121,15 @@ export const LIST_RESULT_KEY = "listResult";
|
|
|
101
121
|
* human ("the sub IS the human subject the session belongs to",
|
|
102
122
|
* Java MemoryCreateHandler) and provenance.session_id is overridden
|
|
103
123
|
* with the token's own session claim (server-proved beats
|
|
104
|
-
* runner-reported). Absent the handoff, the
|
|
124
|
+
* runner-reported). Absent the handoff, the arms above apply
|
|
105
125
|
* unchanged.
|
|
106
126
|
*/
|
|
107
|
-
export function newResolveMemoryDefaultsStep(
|
|
108
|
-
|
|
109
|
-
> {
|
|
127
|
+
export function newResolveMemoryDefaultsStep(
|
|
128
|
+
personAccounts?: AccountsByCaller,
|
|
129
|
+
): PipelineStep<typeof MemorySchema> {
|
|
110
130
|
return {
|
|
111
131
|
name: "ResolveMemoryDefaults",
|
|
112
|
-
execute(ctx: RequestContext<typeof MemorySchema>): void {
|
|
132
|
+
async execute(ctx: RequestContext<typeof MemorySchema>): Promise<void> {
|
|
113
133
|
const memory = ctx.newState;
|
|
114
134
|
const metadata = memory.metadata;
|
|
115
135
|
|
|
@@ -138,11 +158,23 @@ export function newResolveMemoryDefaultsStep(): PipelineStep<
|
|
|
138
158
|
);
|
|
139
159
|
}
|
|
140
160
|
|
|
141
|
-
// The subject stays server-owned (DD-005 D2)
|
|
142
|
-
//
|
|
161
|
+
// The subject stays server-owned (DD-005 D2), by posture (step doc
|
|
162
|
+
// points 3 and 5).
|
|
143
163
|
const captureCredential = memoryCaptureCredentialOf(ctx);
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
if (personAccounts === undefined) {
|
|
165
|
+
spec.subjectIdentityAccountId =
|
|
166
|
+
captureCredential?.subjectIdentityAccountId ?? "";
|
|
167
|
+
} else {
|
|
168
|
+
const account = await resolveSubjectAccount(
|
|
169
|
+
personAccounts,
|
|
170
|
+
ctx,
|
|
171
|
+
captureCredential?.subjectIdentityAccountId,
|
|
172
|
+
);
|
|
173
|
+
spec.subjectIdentityAccountId = account?.metadata?.id ?? "";
|
|
174
|
+
if (account !== undefined) {
|
|
175
|
+
ctx.set(MEMORY_SUBJECT_ACCOUNT_KEY, account);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
146
178
|
|
|
147
179
|
// Provenance is capture-path-supplied (see the step doc, point 4);
|
|
148
180
|
// only tool_call_id is force-cleared — unreachable via MCP in v1, so
|
|
@@ -162,6 +194,54 @@ export function newResolveMemoryDefaultsStep(): PipelineStep<
|
|
|
162
194
|
};
|
|
163
195
|
}
|
|
164
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Context key carrying the identity account a memory is about, from
|
|
199
|
+
* ResolveMemoryDefaults to CheckMemoryEnablement — present exactly when
|
|
200
|
+
* callers are persons and the subject resolved (step doc point 3).
|
|
201
|
+
*/
|
|
202
|
+
export const MEMORY_SUBJECT_ACCOUNT_KEY = "memorySubjectAccount";
|
|
203
|
+
|
|
204
|
+
/** The subject's account stashed by ResolveMemoryDefaults, if any. */
|
|
205
|
+
export function memorySubjectAccountOf(
|
|
206
|
+
ctx: RequestContext<typeof MemorySchema>,
|
|
207
|
+
): IdentityAccount | undefined {
|
|
208
|
+
const payload = ctx.get(MEMORY_SUBJECT_ACCOUNT_KEY);
|
|
209
|
+
return isMessage(payload, IdentityAccountSchema) ? payload : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The account the memory is about: the capture credential's subject read
|
|
214
|
+
* as a creator-style stamp (an account id, or a raw issuer subject), else
|
|
215
|
+
* the account the calling person stands for. `undefined` when nobody
|
|
216
|
+
* resolves. A store fault is an infrastructure fault; an account row with
|
|
217
|
+
* no id is a loud one, never an empty principal (the resolve.ts rule).
|
|
218
|
+
*/
|
|
219
|
+
async function resolveSubjectAccount(
|
|
220
|
+
personAccounts: AccountsByCaller,
|
|
221
|
+
ctx: RequestContext<typeof MemorySchema>,
|
|
222
|
+
credentialSubject: string | undefined,
|
|
223
|
+
): Promise<IdentityAccount | undefined> {
|
|
224
|
+
let account: IdentityAccount | undefined;
|
|
225
|
+
try {
|
|
226
|
+
account =
|
|
227
|
+
credentialSubject !== undefined
|
|
228
|
+
? await accountForStamp(personAccounts, credentialSubject)
|
|
229
|
+
: await accountForCaller(personAccounts, ctx.callerIdentity);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
throw internalError(
|
|
232
|
+
error,
|
|
233
|
+
"failed to resolve the identity account a memory is about",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (account !== undefined && (account.metadata?.id ?? "") === "") {
|
|
237
|
+
throw internalError(
|
|
238
|
+
new Error("identity account carries no id"),
|
|
239
|
+
"failed to resolve the identity account a memory is about",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return account;
|
|
243
|
+
}
|
|
244
|
+
|
|
165
245
|
/** The deny copy of the create lane's organization bar. */
|
|
166
246
|
export const MEMORY_CREATE_DENIED_MESSAGE =
|
|
167
247
|
"unauthorized to capture memory in this organization";
|
|
@@ -212,15 +292,22 @@ export function resolveMemoryCreateTargets(
|
|
|
212
292
|
* authorization: "the label is not authorization; the server refuses"
|
|
213
293
|
* (the conversation-attachment doctrine, applied verbatim).
|
|
214
294
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
295
|
+
* The double opt-in (DD-006 D1), in the Java handler's order: the
|
|
296
|
+
* organization's switch first (the gate an admin controls), then — where
|
|
297
|
+
* callers are persons (`personAccounts` composed) — the switch of the
|
|
298
|
+
* person the memory is about, read from the account ResolveMemoryDefaults
|
|
299
|
+
* resolved and stashed (stigmer#1387). No stashed account under that
|
|
300
|
+
* posture refuses with the person's copy: nobody the server can name has
|
|
301
|
+
* consented. The single-operator posture checks the org flag alone — its
|
|
302
|
+
* one operator is the organization's decider. Who may capture at all is
|
|
303
|
+
* GuardMemoryCapture's question, not this step's.
|
|
218
304
|
*
|
|
219
305
|
* Reads the Organization row directly from the store, matching Go — no
|
|
220
306
|
* cross-domain client.
|
|
221
307
|
*/
|
|
222
308
|
export function newCheckMemoryEnablementStep(
|
|
223
309
|
store: Store,
|
|
310
|
+
personAccounts?: AccountsByCaller,
|
|
224
311
|
): PipelineStep<typeof MemorySchema> {
|
|
225
312
|
return {
|
|
226
313
|
name: "CheckMemoryEnablement",
|
|
@@ -247,6 +334,14 @@ export function newCheckMemoryEnablementStep(
|
|
|
247
334
|
if (org.spec?.preferences?.memoryEnabled !== true) {
|
|
248
335
|
throw failedPreconditionError(memoryDisabledMessage(orgID));
|
|
249
336
|
}
|
|
337
|
+
|
|
338
|
+
if (personAccounts === undefined) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const subject = memorySubjectAccountOf(ctx);
|
|
342
|
+
if (subject?.spec?.preferences?.memoryEnabled !== true) {
|
|
343
|
+
throw failedPreconditionError(MEMORY_ACCOUNT_DISABLED_MESSAGE);
|
|
344
|
+
}
|
|
250
345
|
},
|
|
251
346
|
};
|
|
252
347
|
}
|
|
@@ -20,10 +20,13 @@
|
|
|
20
20
|
* carries the other two edition forks: find's enumeration posture and
|
|
21
21
|
* findMyOrganizations' filtering (see organization-directory.ts).
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* Every chain opens with Authorize; create and delete run the shared
|
|
24
|
+
* tuple-lifecycle steps (CreateAuthorizationTuples, CleanupIamPolicies)
|
|
25
|
+
* against the lifecycle compose.ts hands it: a composed
|
|
26
|
+
* resourceAuthorizationLifecycle driver when a unit registers one, else,
|
|
27
|
+
* under the built-in posture, open source's role lifecycle (the creator's
|
|
28
|
+
* `owner` row), and under trusted-local none, where the steps no-op.
|
|
29
|
+
* Per-RPC posture: docs/authorization-coverage.md §2.
|
|
27
30
|
*/
|
|
28
31
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
29
32
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
@@ -104,6 +104,10 @@ beforeAll(async () => {
|
|
|
104
104
|
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
105
105
|
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
106
106
|
GRPC_PORT: String(grpcPort),
|
|
107
|
+
// The reserved port is the unified one only; its +1 neighbour is not
|
|
108
|
+
// reserved, and a lane that cannot bind fails the boot (#1089), so
|
|
109
|
+
// the artifact lane binds ephemeral.
|
|
110
|
+
ARTIFACT_HTTP_PORT: "0",
|
|
107
111
|
DB_PATH: path.join(dir, "stigmer.db"),
|
|
108
112
|
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
109
113
|
STORAGE_PATH: path.join(dir, "storage"),
|
|
@@ -801,6 +805,10 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
|
|
|
801
805
|
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
802
806
|
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
803
807
|
GRPC_PORT: String(grpcPort),
|
|
808
|
+
// The reserved port is the unified one only; its +1 neighbour is not
|
|
809
|
+
// reserved, and a lane that cannot bind fails the boot (#1089), so
|
|
810
|
+
// the artifact lane binds ephemeral.
|
|
811
|
+
ARTIFACT_HTTP_PORT: "0",
|
|
804
812
|
DB_PATH: path.join(enforcingDir, "stigmer.db"),
|
|
805
813
|
ARTIFACT_LOCAL_BASE_PATH: path.join(enforcingDir, "artifacts"),
|
|
806
814
|
STORAGE_PATH: path.join(enforcingDir, "storage"),
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the plugin's visibility-update load: a
|
|
3
|
+
* typed ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`plugin 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 plugin 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 archive store and the member materializer are untouchable: a load that
|
|
12
|
+
* fails must stop the call before the plugin's level fans out to its members.
|
|
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 { PluginCommandController } from "@stigmer/protos/ai/stigmer/agentic/plugin/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 { registerPluginServices } from "../controller.js";
|
|
36
|
+
|
|
37
|
+
const silentLogger = createLogger({
|
|
38
|
+
level: "error",
|
|
39
|
+
pretty: false,
|
|
40
|
+
write: () => {},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const PLUGIN_ID = "plg_storefault";
|
|
44
|
+
|
|
45
|
+
const NOT_FOUND_COPY = `plugin not found: ${PLUGIN_ID}`;
|
|
46
|
+
const LOAD_FAULT_COPY = "failed to load plugin";
|
|
47
|
+
|
|
48
|
+
const MISSING = (): Error => new ResourceNotFoundError(`plugin/${PLUGIN_ID}`);
|
|
49
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
53
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
54
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
55
|
+
* the store.
|
|
56
|
+
*/
|
|
57
|
+
function pluginCommand(store: Store): Client<typeof PluginCommandController> {
|
|
58
|
+
const transport = createRouterTransport(
|
|
59
|
+
(router) => {
|
|
60
|
+
registerPluginServices(router, {
|
|
61
|
+
store,
|
|
62
|
+
logger: silentLogger,
|
|
63
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
64
|
+
authorizationLifecycle: undefined,
|
|
65
|
+
artifactStorage: untouchable("artifactStorage"),
|
|
66
|
+
materializerProvider: untouchable("materializerProvider"),
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
router: {
|
|
71
|
+
interceptors: [
|
|
72
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
73
|
+
createApiResourceInterceptor(),
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
return createClient(PluginCommandController, transport);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("updateVisibility — LoadPluginForVisibilityUpdate", () => {
|
|
82
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
83
|
+
return errorOf(() =>
|
|
84
|
+
pluginCommand(store).updateVisibility({
|
|
85
|
+
resourceId: PLUGIN_ID,
|
|
86
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
it("a missing plugin answers NotFound with the domain's copy", async () => {
|
|
92
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
93
|
+
|
|
94
|
+
expect(error.code).toBe(Code.NotFound);
|
|
95
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
99
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
100
|
+
|
|
101
|
+
expect(error.code).toBe(Code.Internal);
|
|
102
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* in-process clients it rides exist only after the routes this controller
|
|
17
17
|
* is registered in.
|
|
18
18
|
*
|
|
19
|
-
* Proven by __tests__/plugin.test.ts (composed-server round-trips)
|
|
20
|
-
* plugin.conformance.test.ts
|
|
19
|
+
* Proven by __tests__/plugin.test.ts (composed-server round-trips),
|
|
20
|
+
* __tests__/store-faults.test.ts and plugin.conformance.test.ts
|
|
21
|
+
* (CONFORMANCE_TARGET=local, local-postgres).
|
|
21
22
|
*/
|
|
22
23
|
import { create, fromBinary } from "@bufbuild/protobuf";
|
|
23
24
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
@@ -104,6 +105,7 @@ import {
|
|
|
104
105
|
} from "../../pipeline/steps/authorize-resolved-target.js";
|
|
105
106
|
import type { VersionHistoryBinding } from "../../pipeline/steps/version-history.js";
|
|
106
107
|
import { metadataOf } from "../../pipeline/steps/shapes.js";
|
|
108
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
107
109
|
import type { Store } from "../../store/interface.js";
|
|
108
110
|
import type {
|
|
109
111
|
ArchiveStaging,
|
|
@@ -386,7 +388,7 @@ async function updateVisibility(
|
|
|
386
388
|
return reqCtx.get(UPDATE_VISIBILITY_PLUGIN_KEY) as Plugin;
|
|
387
389
|
}
|
|
388
390
|
|
|
389
|
-
/** Loads the plugin by resource_id;
|
|
391
|
+
/** Loads the plugin by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
390
392
|
function newLoadPluginByIdStep(
|
|
391
393
|
store: Store,
|
|
392
394
|
key: string,
|
|
@@ -401,8 +403,11 @@ function newLoadPluginByIdStep(
|
|
|
401
403
|
ctx.input.resourceId,
|
|
402
404
|
PluginSchema,
|
|
403
405
|
);
|
|
404
|
-
} catch {
|
|
405
|
-
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (error instanceof ResourceNotFoundError) {
|
|
408
|
+
throw notFoundError("plugin", ctx.input.resourceId);
|
|
409
|
+
}
|
|
410
|
+
throw internalError(error, "failed to load plugin");
|
|
406
411
|
}
|
|
407
412
|
ctx.set(key, plugin);
|
|
408
413
|
},
|
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
* Proven by session.conformance.test.ts (CONFORMANCE_TARGET=local) and
|
|
14
14
|
* __tests__/session.test.ts.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Every chain opens with Authorize, and create also asks can_execute on
|
|
17
|
+
* the agent instance it binds to (AuthorizeRunTarget); create and delete
|
|
18
|
+
* run the shared tuple-lifecycle steps against the composed lifecycle;
|
|
19
|
+
* updateSubject evaluates its annotation through authorizeDirect after the
|
|
20
|
+
* load (#224); list, listByAgentInstance and listByChannel narrow through
|
|
21
|
+
* the composed list read scope. Per-RPC posture:
|
|
22
|
+
* docs/authorization-coverage.md §8.
|
|
19
23
|
*/
|
|
20
24
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
21
25
|
|
|
@@ -74,6 +74,10 @@ beforeAll(async () => {
|
|
|
74
74
|
// a live local Temporal (the conformance CRUD harness does the same).
|
|
75
75
|
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
76
76
|
GRPC_PORT: String(grpcPort),
|
|
77
|
+
// The reserved port is the unified one only; its +1 neighbour is not
|
|
78
|
+
// reserved, and a lane that cannot bind fails the boot (#1089), so
|
|
79
|
+
// the artifact lane binds ephemeral.
|
|
80
|
+
ARTIFACT_HTTP_PORT: "0",
|
|
77
81
|
DB_PATH: path.join(dir, "stigmer.db"),
|
|
78
82
|
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
79
83
|
STORAGE_PATH: path.join(dir, "storage"),
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the store-fault contract of the skill's visibility-update load: a
|
|
3
|
+
* typed ResourceNotFoundError answers NotFound with the domain's pinned copy
|
|
4
|
+
* (`skill 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 skill 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 artifact storage is untouchable: a load that fails must stop the call
|
|
12
|
+
* 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 { SkillCommandController } from "@stigmer/protos/ai/stigmer/agentic/skill/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 { registerSkillServices } from "../controller.js";
|
|
36
|
+
|
|
37
|
+
const silentLogger = createLogger({
|
|
38
|
+
level: "error",
|
|
39
|
+
pretty: false,
|
|
40
|
+
write: () => {},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const SKILL_ID = "skl_storefault";
|
|
44
|
+
|
|
45
|
+
const NOT_FOUND_COPY = `skill not found: ${SKILL_ID}`;
|
|
46
|
+
const LOAD_FAULT_COPY = "failed to load skill";
|
|
47
|
+
|
|
48
|
+
const MISSING = (): Error => new ResourceNotFoundError(`skill/${SKILL_ID}`);
|
|
49
|
+
const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The registered handlers on an in-process router: the verifier chain stamps
|
|
53
|
+
* the trusted-local caller and the apiresource interceptor the kind, then
|
|
54
|
+
* Authorize and ValidateProto run and the load is the first step that reads
|
|
55
|
+
* the store.
|
|
56
|
+
*/
|
|
57
|
+
function skillCommand(store: Store): Client<typeof SkillCommandController> {
|
|
58
|
+
const transport = createRouterTransport(
|
|
59
|
+
(router) => {
|
|
60
|
+
registerSkillServices(router, {
|
|
61
|
+
store,
|
|
62
|
+
logger: silentLogger,
|
|
63
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
64
|
+
authorizationLifecycle: undefined,
|
|
65
|
+
artifactStorage: untouchable("artifactStorage"),
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
router: {
|
|
70
|
+
interceptors: [
|
|
71
|
+
createVerifierChainInterceptor([], [], silentLogger),
|
|
72
|
+
createApiResourceInterceptor(),
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
return createClient(SkillCommandController, transport);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("updateVisibility — LoadSkillForVisibilityUpdate", () => {
|
|
81
|
+
function surfaceError(store: Store): Promise<ConnectError> {
|
|
82
|
+
return errorOf(() =>
|
|
83
|
+
skillCommand(store).updateVisibility({
|
|
84
|
+
resourceId: SKILL_ID,
|
|
85
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
it("a missing skill answers NotFound with the domain's copy", async () => {
|
|
91
|
+
const error = await surfaceError(failingStore(MISSING()));
|
|
92
|
+
|
|
93
|
+
expect(error.code).toBe(Code.NotFound);
|
|
94
|
+
expect(error.rawMessage).toBe(NOT_FOUND_COPY);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("any other store failure answers a sanitized Internal", async () => {
|
|
98
|
+
const error = await surfaceError(failingStore(LOCKED()));
|
|
99
|
+
|
|
100
|
+
expect(error.code).toBe(Code.Internal);
|
|
101
|
+
expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
* optional deps; every absent-surface answer is a deliberate arm, not an
|
|
14
14
|
* accident.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Every chain opens with Authorize; push runs the tuple-lifecycle splice
|
|
17
|
+
* (SkillPushAuthorizationTuples), and updateVisibility and delete the
|
|
18
|
+
* shared tuple steps; getByReference and listVersions authorize the
|
|
19
|
+
* resolved skill as `get` would; getArtifact and getArtifactDownloadUrl
|
|
20
|
+
* skip authorization by design, the content-hash key being the capability
|
|
21
|
+
* (docs/authorization-coverage.md §21).
|
|
22
|
+
*
|
|
23
|
+
* The transfer lane's capability
|
|
18
24
|
* URLs come from the blob driver through the staging port
|
|
19
25
|
* (transfer/staging.ts): the local driver's point at this server's own
|
|
20
26
|
* HTTP lane, a bucket driver's straight at the bucket — same
|
|
@@ -99,6 +105,7 @@ import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-
|
|
|
99
105
|
import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
|
|
100
106
|
import { newValidateVisibilityUpdateStep } from "../../pipeline/steps/validate-visibility.js";
|
|
101
107
|
import { newDeleteVersionArchivesStep } from "../../pipeline/steps/version-archive.js";
|
|
108
|
+
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
102
109
|
import type { Store } from "../../store/interface.js";
|
|
103
110
|
import { MAX_ZIP_SIZE, TRANSFER_LANE_NOT_CONFIGURED } from "./constants.js";
|
|
104
111
|
import {
|
|
@@ -463,7 +470,7 @@ async function updateVisibility(
|
|
|
463
470
|
return reqCtx.get(UPDATE_VISIBILITY_SKILL_KEY) as Skill;
|
|
464
471
|
}
|
|
465
472
|
|
|
466
|
-
/** Loads the skill by resource_id;
|
|
473
|
+
/** Loads the skill by resource_id; a missing one answers NotFound, a store fault Internal. */
|
|
467
474
|
function newLoadSkillForVisibilityUpdateStep(
|
|
468
475
|
store: Store,
|
|
469
476
|
): PipelineStep<UpdateVisibilityDesc> {
|
|
@@ -478,8 +485,11 @@ function newLoadSkillForVisibilityUpdateStep(
|
|
|
478
485
|
input.resourceId,
|
|
479
486
|
SkillSchema,
|
|
480
487
|
);
|
|
481
|
-
} catch {
|
|
482
|
-
|
|
488
|
+
} catch (error) {
|
|
489
|
+
if (error instanceof ResourceNotFoundError) {
|
|
490
|
+
throw notFoundError("skill", input.resourceId);
|
|
491
|
+
}
|
|
492
|
+
throw internalError(error, "failed to load skill");
|
|
483
493
|
}
|
|
484
494
|
ctx.set(UPDATE_VISIBILITY_SKILL_KEY, skill);
|
|
485
495
|
},
|