@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.
Files changed (150) hide show
  1. package/dist/artifactstorage/artifact-storage.d.ts +10 -2
  2. package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
  3. package/dist/artifactstorage/artifact-storage.js +6 -3
  4. package/dist/artifactstorage/artifact-storage.js.map +1 -1
  5. package/dist/boot/artifact-lane.d.ts +55 -0
  6. package/dist/boot/artifact-lane.d.ts.map +1 -0
  7. package/dist/boot/artifact-lane.js +74 -0
  8. package/dist/boot/artifact-lane.js.map +1 -0
  9. package/dist/boot/compose.d.ts.map +1 -1
  10. package/dist/boot/compose.js +42 -19
  11. package/dist/boot/compose.js.map +1 -1
  12. package/dist/boot/config.d.ts +11 -6
  13. package/dist/boot/config.d.ts.map +1 -1
  14. package/dist/boot/config.js +10 -5
  15. package/dist/boot/config.js.map +1 -1
  16. package/dist/domain/agent/controller.d.ts +5 -2
  17. package/dist/domain/agent/controller.d.ts.map +1 -1
  18. package/dist/domain/agent/controller.js.map +1 -1
  19. package/dist/domain/agentexecution/controller.d.ts +18 -4
  20. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  21. package/dist/domain/agentexecution/controller.js +2 -2
  22. package/dist/domain/agentexecution/controller.js.map +1 -1
  23. package/dist/domain/agentexecution/create-steps.d.ts +29 -15
  24. package/dist/domain/agentexecution/create-steps.d.ts.map +1 -1
  25. package/dist/domain/agentexecution/create-steps.js +118 -51
  26. package/dist/domain/agentexecution/create-steps.js.map +1 -1
  27. package/dist/domain/agentinstance/controller.d.ts +8 -2
  28. package/dist/domain/agentinstance/controller.d.ts.map +1 -1
  29. package/dist/domain/agentinstance/controller.js.map +1 -1
  30. package/dist/domain/agentinstance/steps.d.ts +3 -1
  31. package/dist/domain/agentinstance/steps.d.ts.map +1 -1
  32. package/dist/domain/agentinstance/steps.js +12 -6
  33. package/dist/domain/agentinstance/steps.js.map +1 -1
  34. package/dist/domain/agentshare/controller.d.ts +9 -4
  35. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  36. package/dist/domain/agentshare/controller.js +3 -3
  37. package/dist/domain/agentshare/controller.js.map +1 -1
  38. package/dist/domain/artifact/file-server.d.ts +2 -1
  39. package/dist/domain/artifact/file-server.d.ts.map +1 -1
  40. package/dist/domain/artifact/file-server.js +6 -3
  41. package/dist/domain/artifact/file-server.js.map +1 -1
  42. package/dist/domain/channelapp/controller.d.ts +8 -3
  43. package/dist/domain/channelapp/controller.d.ts.map +1 -1
  44. package/dist/domain/channelapp/controller.js.map +1 -1
  45. package/dist/domain/environment/controller.d.ts +8 -5
  46. package/dist/domain/environment/controller.d.ts.map +1 -1
  47. package/dist/domain/environment/controller.js +3 -3
  48. package/dist/domain/environment/controller.js.map +1 -1
  49. package/dist/domain/executioncontext/controller.d.ts +7 -4
  50. package/dist/domain/executioncontext/controller.d.ts.map +1 -1
  51. package/dist/domain/executioncontext/controller.js.map +1 -1
  52. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  53. package/dist/domain/mcpserver/controller.js +6 -2
  54. package/dist/domain/mcpserver/controller.js.map +1 -1
  55. package/dist/domain/memory/constants.d.ts +9 -0
  56. package/dist/domain/memory/constants.d.ts.map +1 -1
  57. package/dist/domain/memory/constants.js +9 -0
  58. package/dist/domain/memory/constants.js.map +1 -1
  59. package/dist/domain/memory/controller.d.ts +18 -6
  60. package/dist/domain/memory/controller.d.ts.map +1 -1
  61. package/dist/domain/memory/controller.js +18 -21
  62. package/dist/domain/memory/controller.js.map +1 -1
  63. package/dist/domain/memory/steps.d.ts +38 -10
  64. package/dist/domain/memory/steps.d.ts.map +1 -1
  65. package/dist/domain/memory/steps.js +87 -17
  66. package/dist/domain/memory/steps.js.map +1 -1
  67. package/dist/domain/organization/controller.d.ts +7 -4
  68. package/dist/domain/organization/controller.d.ts.map +1 -1
  69. package/dist/domain/organization/controller.js.map +1 -1
  70. package/dist/domain/plugin/controller.d.ts.map +1 -1
  71. package/dist/domain/plugin/controller.js +10 -5
  72. package/dist/domain/plugin/controller.js.map +1 -1
  73. package/dist/domain/session/controller.d.ts +7 -3
  74. package/dist/domain/session/controller.d.ts.map +1 -1
  75. package/dist/domain/session/controller.js.map +1 -1
  76. package/dist/domain/skill/controller.d.ts.map +1 -1
  77. package/dist/domain/skill/controller.js +15 -5
  78. package/dist/domain/skill/controller.js.map +1 -1
  79. package/dist/domain/workflow/controller.d.ts +6 -2
  80. package/dist/domain/workflow/controller.d.ts.map +1 -1
  81. package/dist/domain/workflow/controller.js +6 -3
  82. package/dist/domain/workflow/controller.js.map +1 -1
  83. package/dist/domain/workflowexecution/controller.d.ts +8 -4
  84. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  85. package/dist/domain/workflowexecution/controller.js.map +1 -1
  86. package/dist/domain/workflowexecution/update-status.d.ts.map +1 -1
  87. package/dist/domain/workflowexecution/update-status.js +3 -2
  88. package/dist/domain/workflowexecution/update-status.js.map +1 -1
  89. package/dist/domain/workflowinstance/controller.d.ts +2 -2
  90. package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
  91. package/dist/domain/workflowinstance/controller.js +10 -5
  92. package/dist/domain/workflowinstance/controller.js.map +1 -1
  93. package/dist/domain/workflowinstance/steps.d.ts +5 -3
  94. package/dist/domain/workflowinstance/steps.d.ts.map +1 -1
  95. package/dist/domain/workflowinstance/steps.js +14 -9
  96. package/dist/domain/workflowinstance/steps.js.map +1 -1
  97. package/dist/extensions/identity.d.ts +28 -14
  98. package/dist/extensions/identity.d.ts.map +1 -1
  99. package/dist/extensions/identity.js +38 -0
  100. package/dist/extensions/identity.js.map +1 -1
  101. package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
  102. package/dist/pipeline/steps/guard-memory-capture.js +1 -13
  103. package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
  104. package/package.json +6 -6
  105. package/src/artifactstorage/__tests__/artifact-storage.test.ts +15 -1
  106. package/src/artifactstorage/artifact-storage.ts +12 -4
  107. package/src/boot/__tests__/artifact-lane.test.ts +71 -0
  108. package/src/boot/__tests__/compose.test.ts +73 -3
  109. package/src/boot/__tests__/config.test.ts +19 -0
  110. package/src/boot/artifact-lane.ts +96 -0
  111. package/src/boot/compose.ts +57 -21
  112. package/src/boot/config.ts +25 -15
  113. package/src/domain/agent/controller.ts +5 -2
  114. package/src/domain/agentexecution/__tests__/create-steps.test.ts +230 -2
  115. package/src/domain/agentexecution/controller.ts +32 -6
  116. package/src/domain/agentexecution/create-steps.ts +162 -62
  117. package/src/domain/agentinstance/__tests__/store-faults.test.ts +69 -18
  118. package/src/domain/agentinstance/controller.ts +8 -2
  119. package/src/domain/agentinstance/steps.ts +12 -6
  120. package/src/domain/agentshare/controller.ts +12 -7
  121. package/src/domain/artifact/file-server.ts +8 -4
  122. package/src/domain/channelapp/controller.ts +8 -3
  123. package/src/domain/environment/controller.ts +11 -8
  124. package/src/domain/executioncontext/controller.ts +7 -4
  125. package/src/domain/mcpserver/controller.ts +6 -2
  126. package/src/domain/memory/__tests__/capture-credential.test.ts +6 -6
  127. package/src/domain/memory/__tests__/person-consent-composed.test.ts +187 -0
  128. package/src/domain/memory/__tests__/person-consent.test.ts +274 -0
  129. package/src/domain/memory/constants.ts +11 -0
  130. package/src/domain/memory/controller.ts +36 -27
  131. package/src/domain/memory/steps.ts +112 -17
  132. package/src/domain/organization/controller.ts +7 -4
  133. package/src/domain/plugin/__tests__/plugin.test.ts +8 -0
  134. package/src/domain/plugin/__tests__/store-faults.test.ts +104 -0
  135. package/src/domain/plugin/controller.ts +10 -5
  136. package/src/domain/session/controller.ts +7 -3
  137. package/src/domain/skill/__tests__/skill.test.ts +4 -0
  138. package/src/domain/skill/__tests__/store-faults.test.ts +103 -0
  139. package/src/domain/skill/controller.ts +15 -5
  140. package/src/domain/workflow/__tests__/store-faults.test.ts +107 -0
  141. package/src/domain/workflow/controller.ts +12 -5
  142. package/src/domain/workflowexecution/controller.ts +8 -4
  143. package/src/domain/workflowexecution/update-status.ts +3 -2
  144. package/src/domain/workflowinstance/__tests__/store-faults.test.ts +172 -0
  145. package/src/domain/workflowinstance/controller.ts +12 -7
  146. package/src/domain/workflowinstance/steps.ts +14 -10
  147. package/src/extensions/__tests__/identity.test.ts +82 -0
  148. package/src/extensions/identity.ts +47 -0
  149. package/src/pipeline/__tests__/blind-not-found.test.ts +4 -20
  150. 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 with the empty-string
81
- * sentinel — the OSS single-user subject (the OAuth grant store
82
- * convention). Server-derived, never client-supplied (DD-005 D2):
83
- * the cloud edition writes the caller's identity account here.
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 OSS arms above apply
124
+ * runner-reported). Absent the handoff, the arms above apply
105
125
  * unchanged.
106
126
  */
107
- export function newResolveMemoryDefaultsStep(): PipelineStep<
108
- typeof MemorySchema
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): the OSS sentinel, or
142
- // the admitted capture credential's proved human (step doc point 5).
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
- spec.subjectIdentityAccountId =
145
- captureCredential?.subjectIdentityAccountId ?? "";
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
- * OSS checks the org flag alone — the user scope collapses in single-user
216
- * local mode (DD-006 D1). The cloud edition additionally requires the
217
- * caller's own memory_enabled and the strict first-party-human gate.
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
- * Versus Stigmer Cloud's Java service, plain OSS excludes the
24
- * CreateIamPolicies and Publish steps; with the C2 seam composed, the
25
- * CreateAuthorizationTuples/CleanupIamPolicies steps deliver the same
26
- * lifecycle through the resourceAuthorizationLifecycle driver.
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) and
20
- * plugin.conformance.test.ts (CONFORMANCE_TARGET=local, local-postgres).
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; ANY load failure → NotFound. */
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
- throw notFoundError("plugin", ctx.input.resourceId);
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
- * Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies, and
17
- * Publish steps (no multi-tenant auth, IAM/FGA, or event publishing here)
18
- * and the FGA-authorized list filtering.
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
- * Versus Stigmer Cloud, OSS excludes the TransformResponse/SendResponse
17
- * steps (no response transformation). The transfer lane's capability
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; ANY load failure → NotFound. */
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
- throw notFoundError("skill", input.resourceId);
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
  },