@stigmer/server 3.39.1-dev.20260928194002 → 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 (124) 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/agentshare/controller.d.ts +9 -4
  31. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  32. package/dist/domain/agentshare/controller.js +3 -3
  33. package/dist/domain/agentshare/controller.js.map +1 -1
  34. package/dist/domain/artifact/file-server.d.ts +2 -1
  35. package/dist/domain/artifact/file-server.d.ts.map +1 -1
  36. package/dist/domain/artifact/file-server.js +6 -3
  37. package/dist/domain/artifact/file-server.js.map +1 -1
  38. package/dist/domain/channelapp/controller.d.ts +8 -3
  39. package/dist/domain/channelapp/controller.d.ts.map +1 -1
  40. package/dist/domain/channelapp/controller.js.map +1 -1
  41. package/dist/domain/environment/controller.d.ts +8 -5
  42. package/dist/domain/environment/controller.d.ts.map +1 -1
  43. package/dist/domain/environment/controller.js +3 -3
  44. package/dist/domain/environment/controller.js.map +1 -1
  45. package/dist/domain/executioncontext/controller.d.ts +7 -4
  46. package/dist/domain/executioncontext/controller.d.ts.map +1 -1
  47. package/dist/domain/executioncontext/controller.js.map +1 -1
  48. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  49. package/dist/domain/mcpserver/controller.js +6 -2
  50. package/dist/domain/mcpserver/controller.js.map +1 -1
  51. package/dist/domain/memory/constants.d.ts +9 -0
  52. package/dist/domain/memory/constants.d.ts.map +1 -1
  53. package/dist/domain/memory/constants.js +9 -0
  54. package/dist/domain/memory/constants.js.map +1 -1
  55. package/dist/domain/memory/controller.d.ts +18 -6
  56. package/dist/domain/memory/controller.d.ts.map +1 -1
  57. package/dist/domain/memory/controller.js +18 -21
  58. package/dist/domain/memory/controller.js.map +1 -1
  59. package/dist/domain/memory/steps.d.ts +38 -10
  60. package/dist/domain/memory/steps.d.ts.map +1 -1
  61. package/dist/domain/memory/steps.js +87 -17
  62. package/dist/domain/memory/steps.js.map +1 -1
  63. package/dist/domain/organization/controller.d.ts +7 -4
  64. package/dist/domain/organization/controller.d.ts.map +1 -1
  65. package/dist/domain/organization/controller.js.map +1 -1
  66. package/dist/domain/session/controller.d.ts +7 -3
  67. package/dist/domain/session/controller.d.ts.map +1 -1
  68. package/dist/domain/session/controller.js.map +1 -1
  69. package/dist/domain/skill/controller.d.ts.map +1 -1
  70. package/dist/domain/skill/controller.js +8 -2
  71. package/dist/domain/skill/controller.js.map +1 -1
  72. package/dist/domain/workflow/controller.d.ts +6 -2
  73. package/dist/domain/workflow/controller.d.ts.map +1 -1
  74. package/dist/domain/workflow/controller.js.map +1 -1
  75. package/dist/domain/workflowexecution/controller.d.ts +8 -4
  76. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  77. package/dist/domain/workflowexecution/controller.js.map +1 -1
  78. package/dist/domain/workflowexecution/update-status.d.ts.map +1 -1
  79. package/dist/domain/workflowexecution/update-status.js +3 -2
  80. package/dist/domain/workflowexecution/update-status.js.map +1 -1
  81. package/dist/extensions/identity.d.ts +28 -14
  82. package/dist/extensions/identity.d.ts.map +1 -1
  83. package/dist/extensions/identity.js +38 -0
  84. package/dist/extensions/identity.js.map +1 -1
  85. package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
  86. package/dist/pipeline/steps/guard-memory-capture.js +1 -13
  87. package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
  88. package/package.json +6 -6
  89. package/src/artifactstorage/__tests__/artifact-storage.test.ts +15 -1
  90. package/src/artifactstorage/artifact-storage.ts +12 -4
  91. package/src/boot/__tests__/artifact-lane.test.ts +71 -0
  92. package/src/boot/__tests__/compose.test.ts +73 -3
  93. package/src/boot/__tests__/config.test.ts +19 -0
  94. package/src/boot/artifact-lane.ts +96 -0
  95. package/src/boot/compose.ts +57 -21
  96. package/src/boot/config.ts +25 -15
  97. package/src/domain/agent/controller.ts +5 -2
  98. package/src/domain/agentexecution/__tests__/create-steps.test.ts +230 -2
  99. package/src/domain/agentexecution/controller.ts +32 -6
  100. package/src/domain/agentexecution/create-steps.ts +162 -62
  101. package/src/domain/agentinstance/controller.ts +8 -2
  102. package/src/domain/agentshare/controller.ts +12 -7
  103. package/src/domain/artifact/file-server.ts +8 -4
  104. package/src/domain/channelapp/controller.ts +8 -3
  105. package/src/domain/environment/controller.ts +11 -8
  106. package/src/domain/executioncontext/controller.ts +7 -4
  107. package/src/domain/mcpserver/controller.ts +6 -2
  108. package/src/domain/memory/__tests__/capture-credential.test.ts +6 -6
  109. package/src/domain/memory/__tests__/person-consent-composed.test.ts +187 -0
  110. package/src/domain/memory/__tests__/person-consent.test.ts +274 -0
  111. package/src/domain/memory/constants.ts +11 -0
  112. package/src/domain/memory/controller.ts +36 -27
  113. package/src/domain/memory/steps.ts +112 -17
  114. package/src/domain/organization/controller.ts +7 -4
  115. package/src/domain/plugin/__tests__/plugin.test.ts +8 -0
  116. package/src/domain/session/controller.ts +7 -3
  117. package/src/domain/skill/__tests__/skill.test.ts +4 -0
  118. package/src/domain/skill/controller.ts +8 -2
  119. package/src/domain/workflow/controller.ts +6 -2
  120. package/src/domain/workflowexecution/controller.ts +8 -4
  121. package/src/domain/workflowexecution/update-status.ts +3 -2
  122. package/src/extensions/__tests__/identity.test.ts +82 -0
  123. package/src/extensions/identity.ts +47 -0
  124. package/src/pipeline/steps/guard-memory-capture.ts +4 -19
@@ -30,6 +30,17 @@ export function memoryDisabledMessage(org: string): string {
30
30
  return `memory is not enabled for organization ${org} — an organization admin can enable it in organization preferences`;
31
31
  }
32
32
 
33
+ /**
34
+ * Refuses a create while the PERSON the memory would be about has not
35
+ * enabled memory on their own account — the member half of the double
36
+ * opt-in (DD-006 D1), and the answer for a caller no account stands for,
37
+ * since only an unprovisioned person reaches that arm. The Java
38
+ * MemoryPolicy.MEMORY_ACCOUNT_DISABLED_MESSAGE, byte-pinned: the hosted
39
+ * edition's clients have rendered it since the Java handler shipped.
40
+ */
41
+ export const MEMORY_ACCOUNT_DISABLED_MESSAGE =
42
+ "memory is not enabled for your account — enable it in account preferences";
43
+
33
44
  /**
34
45
  * Refuses confirming a rejected memory: the decision is auditable and
35
46
  * stands; a fresh proposal is the way back. Go
@@ -35,12 +35,15 @@
35
35
  *
36
36
  * Authorization posture (OSS): this edition is single-user and local, so
37
37
  * handlers perform no authorization — a documented no-op, not a silent
38
- * divergence. The subject is the empty-string sentinel (the OAuth grant
39
- * store convention); enablement is the org flag alone (the user scope
40
- * collapses — DD-002 D1). The cloud edition derives the subject from the
41
- * calling credential, enforces the strict first-party-human gate, checks
42
- * both memory_enabled flags, and gates every RPC through FGA (subject-only
43
- * can_view/can_edit/can_delete).
38
+ * divergence. The subject and the double opt-in follow the POSTURE, not
39
+ * the edition (stigmer#1387): where callers are persons (the
40
+ * require-authentication posture composes `personAccounts` — the hosted
41
+ * edition, a self-host with sign-in) the subject is the identity account
42
+ * the memory is about and both memory_enabled flags must be on; under the
43
+ * single-operator posture (trusted-local) the subject is the empty-string
44
+ * sentinel (the OAuth grant store convention) and enablement is the org
45
+ * flag alone (the user scope collapses — DD-002 D1). Every RPC's FGA
46
+ * question is subject-only (can_view/can_edit/can_delete).
44
47
  *
45
48
  * Memory is deliberately NOT search-indexed (privacy — subject-only
46
49
  * content must not surface in org-visible search): no search-extractor,
@@ -70,6 +73,7 @@ import type { Logger } from "../../boot/logger.js";
70
73
  import type { Authorizer } from "../../extensions/authorizer.js";
71
74
  import type { RunnerCredentialProvider } from "../../runnerauth/runner-credential-provider.js";
72
75
  import type { ListReadScope } from "../../extensions/list-read-scope.js";
76
+ import type { AccountsByCaller } from "../identityaccount/resolve.js";
73
77
  import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
74
78
  import { apiResourceKindKey } from "../../pipeline/interceptors/apiresource.js";
75
79
  import { internalError } from "../../pipeline/errors.js";
@@ -131,6 +135,14 @@ export interface MemoryControllerDeps {
131
135
  readonly authorizationLifecycle: ResourceAuthorizationLifecycle | undefined;
132
136
  /** The composed list read scope — list narrows through it; undefined = the OSS full scan (20260830.01). */
133
137
  readonly listReadScope: ListReadScope | undefined;
138
+ /**
139
+ * The directory of the persons callers stand for (stigmer#1387) — the
140
+ * composed identity-account port, bound under the require-authentication
141
+ * posture; undefined = the single-operator posture (trusted-local),
142
+ * where memory is the organization's switch alone and the subject the
143
+ * empty-string sentinel.
144
+ */
145
+ readonly personAccounts: AccountsByCaller | undefined;
134
146
  /**
135
147
  * The composed runner-credential provider — GuardMemoryCapture consults
136
148
  * its authorizeMemoryCapture capability for the runner-credential
@@ -172,10 +184,13 @@ function kindOf(ctx: HandlerContext): ApiResourceKind {
172
184
  * No search-index step: memory is not_search_indexed by design (privacy —
173
185
  * content is subject-only and must not surface in org-visible search).
174
186
  *
175
- * Note: Unlike Stigmer Cloud, OSS excludes the strict
176
- * first-party-human-operator gate and the caller's own memory_enabled
177
- * check (no per-request user identity — the user scope collapses, DD-006
178
- * D1) and creates no FGA tuples.
187
+ * Authorization: GuardMemoryCapture decides WHO may capture (the composed
188
+ * RunnerCredentialProvider), AuthorizeResolvedTarget decides WHERE
189
+ * (can_create_session on the organization), and CreateAuthorizationTuples
190
+ * runs against the composed lifecycle. ResolveMemoryDefaults names the
191
+ * person the memory is about and CheckMemoryEnablement requires both
192
+ * halves of the double opt-in where callers are persons (both step docs;
193
+ * stigmer#1387).
179
194
  */
180
195
  async function createMemory(
181
196
  deps: MemoryControllerDeps,
@@ -195,7 +210,7 @@ async function createMemory(
195
210
  .addStep(newValidateProtoStep())
196
211
  .addStep(newGuardMemoryCaptureStep(deps.runnerCredentialProvider))
197
212
  .addStep(newValidateVisibilityStep())
198
- .addStep(newResolveMemoryDefaultsStep())
213
+ .addStep(newResolveMemoryDefaultsStep(deps.personAccounts))
199
214
  // Before the enablement and cap checks, so a refused caller learns
200
215
  // nothing about the organization's settings (steps.ts,
201
216
  // resolveMemoryCreateTargets).
@@ -205,7 +220,7 @@ async function createMemory(
205
220
  resolveMemoryCreateTargets,
206
221
  ),
207
222
  )
208
- .addStep(newCheckMemoryEnablementStep(deps.store))
223
+ .addStep(newCheckMemoryEnablementStep(deps.store, deps.personAccounts))
209
224
  .addStep(newCheckMemoryCapStep(deps.store))
210
225
  .addStep(newResolveSlugStep())
211
226
  .addStep(newCheckDuplicateStep(deps.store))
@@ -236,8 +251,7 @@ async function createMemory(
236
251
  * owners (create, confirm, reject) last wrote it, even against a
237
252
  * concurrent confirm landing between this pipeline's load and persist.
238
253
  *
239
- * Note: Unlike Stigmer Cloud, OSS excludes the authorization step (cloud
240
- * requires can_edit on the memory — FGA subject-only).
254
+ * Authorize evaluates can_edit on the memory (subject-only in the model).
241
255
  */
242
256
  async function update(
243
257
  deps: MemoryControllerDeps,
@@ -332,9 +346,8 @@ async function reject(
332
346
  * runTransition: one contract with opposite verdicts (DD-005 D3),
333
347
  * answering with the post-image row.
334
348
  *
335
- * Note: Unlike Stigmer Cloud, OSS excludes the authorization step (cloud
336
- * requires can_edit on the memory — FGA subject-only, loading before
337
- * authorizing so a missing memory answers NOT_FOUND, #224).
349
+ * Authorize evaluates can_edit on the memory (subject-only in the model)
350
+ * against the request's id, before the load.
338
351
  */
339
352
  async function runTransition(
340
353
  deps: MemoryControllerDeps,
@@ -381,8 +394,7 @@ async function runTransition(
381
394
  *
382
395
  * No search-index cleanup: memory is not_search_indexed.
383
396
  *
384
- * Note: Unlike Stigmer Cloud, OSS excludes the authorization step (cloud
385
- * requires can_delete on the memory — FGA subject-only).
397
+ * Authorize evaluates can_delete on the memory (subject-only in the model).
386
398
  *
387
399
  * The deleted memory is returned for audit trail purposes (gRPC
388
400
  * convention).
@@ -425,9 +437,8 @@ async function deleteMemory(
425
437
  /**
426
438
  * Get — chain per Go buildGetPipeline (ExtractResourceId → LoadTarget).
427
439
  *
428
- * Note: Unlike Stigmer Cloud, OSS excludes the authorization step (cloud
429
- * requires can_view — FGA subject-only: only the person a memory is about
430
- * can read it).
440
+ * Authorize evaluates can_view on the memory: in the model only the person
441
+ * a memory is about can read it.
431
442
  */
432
443
  async function get(
433
444
  deps: MemoryControllerDeps,
@@ -466,11 +477,9 @@ async function get(
466
477
  * console's presentation concern (DD-005 D4), deliberately not an RPC
467
478
  * parameter at the kind's dozens-of-records scale.
468
479
  *
469
- * Note: Unlike Stigmer Cloud, OSS excludes:
470
- * - Authorization filtering (single user — every record is the
471
- * caller's; cloud filters to can_view via FGA, which resolves to the
472
- * subject)
473
- * - Pagination (returns all matching results)
480
+ * Narrowed to the caller's authorized rows (can_view, which resolves to
481
+ * the subject) by the composed list read scope. Unpaginated (returns all
482
+ * matching results).
474
483
  */
475
484
  async function list(
476
485
  deps: MemoryControllerDeps,
@@ -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"),
@@ -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"),
@@ -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
@@ -13,8 +13,12 @@
13
13
  * Proven by workflow.conformance.test.ts (CONFORMANCE_TARGET=local) and
14
14
  * __tests__/.
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).
16
+ * Every chain opens with Authorize; create, delete and updateVisibility run
17
+ * the shared tuple-lifecycle steps against the composed lifecycle;
18
+ * getByReference and listVersions authorize the resolved workflow as `get`
19
+ * would. getVersion evaluates its annotation through authorizeDirect;
20
+ * validateSpec deliberately does not, since it persists nothing. Per-RPC
21
+ * posture: docs/authorization-coverage.md §17.
18
22
  */
19
23
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
20
24
  import { create, enumToJson, fromBinary } from "@bufbuild/protobuf";
@@ -12,10 +12,14 @@
12
12
  * Proven by workflowexecution.conformance.test.ts
13
13
  * (CONFORMANCE_TARGET=local) and __tests__/.
14
14
  *
15
- * Versus Stigmer Cloud, OSS excludes the Authorize, Publish,
16
- * PublishToRedis, and TransformResponse steps (no multi-tenant auth,
17
- * event publishing, or Redis here — subscribe streams ride in-memory
18
- * channels per ADR 011).
15
+ * Every chain opens with Authorize, and create also authorizes its run
16
+ * target (AuthorizeRunTarget); create and delete run the shared
17
+ * tuple-lifecycle steps against the composed lifecycle. The streams and
18
+ * the event log evaluate their annotation through authorizeDirect; the
19
+ * list, summary and pending-approval reads narrow through the composed
20
+ * list read scope. Subscribe streams ride the in-memory stream broker
21
+ * (ADR 011); there is no Redis. Per-RPC posture:
22
+ * docs/authorization-coverage.md §19.
19
23
  */
20
24
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
21
25
 
@@ -22,8 +22,9 @@
22
22
  * phase-transition-only statusAudit bump (heartbeats must not perpetually
23
23
  * re-sort long-running executions above new ones in the recents sidebar).
24
24
  *
25
- * Versus Stigmer Cloud, OSS excludes the Authorize, PublishToRedis, and
26
- * Publish steps (broadcast rides in-memory channels per ADR 011).
25
+ * The chain opens with Authorize (can_edit on the execution); the
26
+ * broadcast rides the in-memory stream broker (ADR 011) — there is no
27
+ * Redis.
27
28
  */
28
29
  import { create, toBinary } from "@bufbuild/protobuf";
29
30
  import { timestampNow } from "@bufbuild/protobuf/wkt";
@@ -13,10 +13,18 @@
13
13
  * in-process origin, and never a wire `machine` account — the one row
14
14
  * where the two predicates part, pinned so a step trusting server-composed
15
15
  * state can never be widened to the wire by picking the wrong one.
16
+ *
17
+ * And pins `isFirstPartyHumanOperator` (stigmer#1387), the allow-list
18
+ * memory recall and the person's declared preferences read: a wire `user`
19
+ * whose bearer is not a PlatformClient user token. Every other class, an
20
+ * in-process `user` (the open-source schedule fire's shape) and a
21
+ * platform token naming a PlatformClient are excluded; another issuer's
22
+ * token carrying the same claim is not a platform token (stigmer#1312).
16
23
  */
17
24
  import { describe, expect, it } from "vitest";
18
25
 
19
26
  import {
27
+ isFirstPartyHumanOperator,
20
28
  isPlatformPipelineCaller,
21
29
  isServerComposedRequest,
22
30
  } from "../identity.js";
@@ -103,3 +111,77 @@ describe("isServerComposedRequest", () => {
103
111
  );
104
112
  });
105
113
  });
114
+
115
+ /** A JWT carrying `payload`, unsigned: the predicate reads, never verifies. */
116
+ function jwtWith(payload: Record<string, unknown>): string {
117
+ const segment = (value: object) =>
118
+ Buffer.from(JSON.stringify(value)).toString("base64url");
119
+ return `${segment({ alg: "RS256", typ: "JWT" })}.${segment(payload)}.signature`;
120
+ }
121
+
122
+ describe("isFirstPartyHumanOperator", () => {
123
+ it("is true for a wire user — an OIDC login, an API key, the laptop's operator", () => {
124
+ expect(isFirstPartyHumanOperator(caller({}))).toBe(true);
125
+ expect(isFirstPartyHumanOperator(caller({ origin: "wire" }))).toBe(true);
126
+ expect(
127
+ isFirstPartyHumanOperator(
128
+ caller({
129
+ issuer: "https://issuer.example",
130
+ rawToken: jwtWith({ iss: "https://issuer.example", sub: "carol" }),
131
+ }),
132
+ ),
133
+ ).toBe(true);
134
+ });
135
+
136
+ it("is false for every other class, a composition's own included", () => {
137
+ for (const callerClass of [
138
+ "machine",
139
+ "runner",
140
+ "internal",
141
+ "guest",
142
+ "channel",
143
+ "schedule",
144
+ "sandbox",
145
+ ]) {
146
+ expect(
147
+ isFirstPartyHumanOperator(caller({ callerClass })),
148
+ callerClass,
149
+ ).toBe(false);
150
+ }
151
+ });
152
+
153
+ it("is false for a user the server composed a request for (the schedule fire)", () => {
154
+ expect(
155
+ isFirstPartyHumanOperator(
156
+ caller({ callerClass: "user", origin: "in-process" }),
157
+ ),
158
+ ).toBe(false);
159
+ });
160
+
161
+ it("is false for a PlatformClient user token, and true when another issuer stamps the claim", () => {
162
+ expect(
163
+ isFirstPartyHumanOperator(
164
+ caller({
165
+ issuer: "stigmer",
166
+ rawToken: jwtWith({
167
+ iss: "stigmer",
168
+ sub: "ida_x",
169
+ platform_client_id: "pc_1",
170
+ }),
171
+ }),
172
+ ),
173
+ ).toBe(false);
174
+ expect(
175
+ isFirstPartyHumanOperator(
176
+ caller({
177
+ issuer: "https://issuer.example",
178
+ rawToken: jwtWith({
179
+ iss: "https://issuer.example",
180
+ sub: "ida_x",
181
+ platform_client_id: "pc_1",
182
+ }),
183
+ }),
184
+ ),
185
+ ).toBe(true);
186
+ });
187
+ });