@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
@@ -85,6 +85,7 @@ import {
85
85
  import { ensureOperatorAccount } from "../domain/identityaccount/operator.js";
86
86
  import { newDirectAccountProvisioner } from "../domain/identityaccount/provisioning.js";
87
87
  import { newResourceIdentityAccountStore } from "../domain/identityaccount/resource-store.js";
88
+ import type { AccountsByCaller } from "../domain/identityaccount/resolve.js";
88
89
  import { registerIamPolicyServices } from "../domain/iampolicy/controller.js";
89
90
  import { newIamPolicyGrantPath } from "../domain/iampolicy/grant-path.js";
90
91
  import { asFetch, guardedFetch, nodeLookup } from "@stigmer/outbound/egress";
@@ -178,6 +179,11 @@ import { PostgresStore } from "../store/postgres/store.js";
178
179
  import { SqliteStore } from "../store/sqlite/store.js";
179
180
  import type { Store } from "../store/interface.js";
180
181
  import { resolveExtensions } from "../extensions/registry.js";
182
+ import {
183
+ artifactLaneBindError,
184
+ resolveArtifactLanePort,
185
+ resolveArtifactServeUrl,
186
+ } from "./artifact-lane.js";
181
187
  import { resolveOAuthRedirectUri } from "./oauth-redirect-uri.js";
182
188
  import { assessSkillTransferOrigin } from "./skill-transfer-origin.js";
183
189
  import type { ServerExtension } from "../extensions/registry.js";
@@ -570,6 +576,16 @@ export async function composeServer(
570
576
  logger,
571
577
  })
572
578
  : undefined);
579
+ // The persons callers stand for (stigmer#1387): under the
580
+ // require-authentication posture every caller is someone the
581
+ // identity-account domain can name, so memory and the person's declared
582
+ // preferences are that person's own; trusted-local composes none — the
583
+ // laptop's one operator, whose memory is the organization's switch alone
584
+ // (the contract's single-operator arm). The same binding both verifiers
585
+ // read, so a composed driver is followed here too.
586
+ const personAccounts: AccountsByCaller | undefined = requireAuthentication
587
+ ? identityAccounts
588
+ : undefined;
573
589
  logger.info("authorization posture resolved", {
574
590
  posture: authorizationPosture,
575
591
  authorizer:
@@ -951,11 +967,24 @@ export async function composeServer(
951
967
  // arm landed with #13; the health probe runs in start(), matching Go's
952
968
  // boot check. Since O5 the factory consults the composition's registered
953
969
  // drivers for non-built-in types (§6b).
970
+ //
971
+ // The download lane's placement (boot/artifact-lane.ts): the configured
972
+ // port, or the port beside the one the unified listener binds, ephemeral
973
+ // when that one is. Unset, the serve URL reads the port the lane actually
974
+ // bound in start(), before any request can mint a URL.
975
+ const artifactLanePort = resolveArtifactLanePort({
976
+ configured: config.artifactHttpPort,
977
+ unifiedPort: options.portOverride ?? config.grpcPort,
978
+ });
979
+ let artifactLaneBoundPort: number | undefined;
954
980
  const artifactStorage = newArtifactStorage(
955
981
  {
956
982
  type: config.artifactStorageType,
957
983
  localBasePath: config.artifactLocalBasePath,
958
- localServeUrl: config.artifactLocalServeUrl,
984
+ localServeUrl: resolveArtifactServeUrl(
985
+ config.artifactLocalServeUrl,
986
+ () => artifactLaneBoundPort,
987
+ ),
959
988
  r2Bucket: config.r2Bucket,
960
989
  r2Endpoint: config.r2Endpoint,
961
990
  r2AccessKeyId: config.r2AccessKeyId,
@@ -964,10 +993,9 @@ export async function composeServer(
964
993
  },
965
994
  extensions.drivers.artifactStorageDrivers,
966
995
  );
967
- // The artifact download lane: a SECOND loopback listener on
968
- // ARTIFACT_HTTP_PORT, local storage only (Go server.go 849–870) —
969
- // deliberately not a unified-port lane. Lifecycle rides start()/
970
- // shutdown().
996
+ // The artifact download lane: a SECOND loopback listener, local storage
997
+ // only (Go server.go 849–870) — deliberately not a unified-port lane.
998
+ // Lifecycle rides start()/shutdown().
971
999
  const artifactFileServer =
972
1000
  config.artifactStorageType === "local"
973
1001
  ? createArtifactFileServer({
@@ -1461,6 +1489,7 @@ export async function composeServer(
1461
1489
  authorizer,
1462
1490
  authorizationLifecycle,
1463
1491
  listReadScope,
1492
+ personAccounts,
1464
1493
  runnerCredentialProvider: runnerCredentials,
1465
1494
  });
1466
1495
  registerAgentExecutionServices(router, {
@@ -1469,6 +1498,7 @@ export async function composeServer(
1469
1498
  authorizer,
1470
1499
  authorizationLifecycle,
1471
1500
  listReadScope,
1501
+ personAccounts,
1472
1502
  runnerCredentialProvider: runnerCredentials,
1473
1503
  broker: agentExecutionStreamBroker,
1474
1504
  engineState: executionEngineState,
@@ -1846,6 +1876,28 @@ export async function composeServer(
1846
1876
  // Artifact storage must be reachable and writable before the server
1847
1877
  // answers (Go server.go boots-fatal on the same probe).
1848
1878
  await artifactStorage.health();
1879
+ // The download lane binds with the store it serves, and on the same
1880
+ // terms: a lane that cannot bind fails the boot, where the retired Go
1881
+ // server logged the failure and served on with every download dead
1882
+ // (stigmer#1089). Binding here, before SERVING and the unified port,
1883
+ // also means the derived serve URL exists before a request can mint
1884
+ // one.
1885
+ if (artifactFileServer !== undefined) {
1886
+ await warnOnLegacyArtifactLayout(config.artifactLocalBasePath, logger);
1887
+ try {
1888
+ artifactLaneBoundPort = await artifactFileServer.listen(
1889
+ artifactLanePort,
1890
+ config.artifactHttpHost,
1891
+ );
1892
+ } catch (error) {
1893
+ throw artifactLaneBindError(
1894
+ error,
1895
+ config.artifactHttpHost,
1896
+ artifactLanePort,
1897
+ config.artifactHttpPort !== undefined,
1898
+ );
1899
+ }
1900
+ }
1849
1901
  // Rebuild the search index before the port binds (Go
1850
1902
  // server.go:617): the index is separate from the resources table,
1851
1903
  // and rebuilding here makes every resource — including rows an
@@ -1884,22 +1936,6 @@ export async function composeServer(
1884
1936
  options.host,
1885
1937
  );
1886
1938
  logger.info("stigmer-server listening", { port });
1887
- // The artifact file server binds AFTER the main server, exactly Go's
1888
- // boot order; a bind failure is logged but NOT fatal (Go's
1889
- // ListenAndServe goroutine logs and dies while the server runs on).
1890
- if (artifactFileServer !== undefined) {
1891
- await warnOnLegacyArtifactLayout(config.artifactLocalBasePath, logger);
1892
- try {
1893
- await artifactFileServer.listen(
1894
- config.artifactHttpPort,
1895
- config.artifactHttpHost,
1896
- );
1897
- } catch (error) {
1898
- logger.error("Artifact HTTP file server failed", {
1899
- error: error instanceof Error ? error.message : String(error),
1900
- });
1901
- }
1902
- }
1903
1939
  return port;
1904
1940
  },
1905
1941
 
@@ -95,16 +95,21 @@ export interface ServerConfig {
95
95
  /** The artifact root — shared with the runner's LOCAL_ARTIFACT_PATH (#285). */
96
96
  readonly artifactLocalBasePath: string;
97
97
  /**
98
- * Base URL for local artifact download URLs — the port+1 artifact file
99
- * server (#13); no trailing path segment (the storage key carries the
100
- * full path).
98
+ * Base URL for local artifact download URLs (ARTIFACT_LOCAL_SERVE_URL;
99
+ * #13); no trailing path segment (the storage key carries the full path).
100
+ * "" = unset: the URL follows the port the artifact file server actually
101
+ * bound (boot/artifact-lane.ts), so an ephemeral lane mints URLs that
102
+ * reach it.
101
103
  */
102
104
  readonly artifactLocalServeUrl: string;
103
105
  /**
104
- * The artifact file server's own port (ARTIFACT_HTTP_PORT, default
105
- * grpcPort+1). Only bound when artifact storage is local.
106
+ * The artifact file server's own port (ARTIFACT_HTTP_PORT). Undefined =
107
+ * unset: the lane follows the unified port, +1, or ephemeral when that
108
+ * port is 0 — the rule lives in boot/artifact-lane.ts, because only the
109
+ * composition knows the port the unified listener binds. Only bound when
110
+ * artifact storage is local, and a lane that cannot bind fails the boot.
106
111
  */
107
- readonly artifactHttpPort: number;
112
+ readonly artifactHttpPort: number | undefined;
108
113
  /**
109
114
  * The artifact file server's bind host (ARTIFACT_HTTP_HOST, DD-013;
110
115
  * shipped with the Docker image, Phase-2 P4). Defaults to 127.0.0.1 —
@@ -294,8 +299,9 @@ export const DEFAULT_SANDBOX_RUNNER_IMAGE = "ghcr.io/stigmer/runner:latest";
294
299
  export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {
295
300
  const { operatorEmail, operatorName } = loadOperatorIdentity(env);
296
301
  const grpcPort = envInt(env, "GRPC_PORT", DEFAULT_GRPC_PORT);
297
- // Go: ARTIFACT_HTTP_PORT defaults to the gRPC port + 1.
298
- const artifactHttpPort = envInt(env, "ARTIFACT_HTTP_PORT", grpcPort + 1);
302
+ // Unset (or malformed, the loader's leniency) leaves the lane to follow
303
+ // the unified port; boot/artifact-lane.ts derives it at composition.
304
+ const artifactHttpPort = envOptionalInt(env, "ARTIFACT_HTTP_PORT");
299
305
  const artifactHttpHost = envString(env, "ARTIFACT_HTTP_HOST", "127.0.0.1");
300
306
  const artifactStorageType = envString(env, "ARTIFACT_STORAGE_TYPE", "local");
301
307
  const r2 = {
@@ -337,11 +343,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): ServerConfig {
337
343
  "ARTIFACT_LOCAL_BASE_PATH",
338
344
  defaultArtifactPath(),
339
345
  ),
340
- artifactLocalServeUrl: envString(
341
- env,
342
- "ARTIFACT_LOCAL_SERVE_URL",
343
- `http://localhost:${artifactHttpPort}`,
344
- ),
346
+ artifactLocalServeUrl: envString(env, "ARTIFACT_LOCAL_SERVE_URL", ""),
345
347
  logLevel: envString(env, "LOG_LEVEL", "info"),
346
348
  env: envString(env, "ENV", "local"),
347
349
  modelRegistryUpstream: envString(
@@ -531,14 +533,22 @@ function envString(
531
533
  }
532
534
 
533
535
  function envInt(env: NodeJS.ProcessEnv, key: string, fallback: number): number {
536
+ return envOptionalInt(env, key) ?? fallback;
537
+ }
538
+
539
+ /** An integer setting, or undefined when it is unset or malformed. */
540
+ function envOptionalInt(
541
+ env: NodeJS.ProcessEnv,
542
+ key: string,
543
+ ): number | undefined {
534
544
  const value = env[key];
535
545
  if (value === undefined || value === "") {
536
- return fallback;
546
+ return undefined;
537
547
  }
538
548
  const parsed = Number.parseInt(value, 10);
539
549
  // Go's strconv.Atoi rejects trailing garbage ("7234x"); Number.parseInt
540
550
  // would accept it, so the round-trip check keeps the two loaders aligned.
541
551
  return Number.isSafeInteger(parsed) && String(parsed) === value.trim()
542
552
  ? parsed
543
- : fallback;
553
+ : undefined;
544
554
  }
@@ -11,8 +11,11 @@
11
11
  * Proven by agent.conformance.test.ts (CONFORMANCE_TARGET=local),
12
12
  * __tests__/agent.test.ts and __tests__/store-faults.test.ts.
13
13
  *
14
- * Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies, and
15
- * Publish steps (no multi-tenant auth, IAM/FGA, or event publishing here).
14
+ * Every chain opens with Authorize; create, delete and updateVisibility run
15
+ * the shared tuple-lifecycle steps against the composed lifecycle;
16
+ * getByReference loads, then authorizes the loaded agent exactly as `get`
17
+ * would (AuthorizeResolvedTarget). The kind has no list RPC. Per-RPC
18
+ * posture: docs/authorization-coverage.md §6.
16
19
  */
17
20
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
18
21
 
@@ -7,8 +7,18 @@
7
7
  * direct-step shape as Go). Go's nil-client skip proofs map to throwing
8
8
  * providers — reaching the client would fail the test just as a nil
9
9
  * dereference would panic Go.
10
+ *
11
+ * The two compose steps are also pinned where callers are persons
12
+ * (stigmer#1387, stigmer#1397): recall is the run's person's own
13
+ * confirmed facts, behind the first-party gate, the org switch and the
14
+ * person's own, and user_context is that person's standing context — the
15
+ * retired Java steps' gates, in their order.
10
16
  */
11
17
  import { testCallerIdentity } from "../../../pipeline/__tests__/support.js";
18
+ import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
19
+ import type { CallerIdentity } from "../../../extensions/identity.js";
20
+ import type { AccountsByCaller } from "../../identityaccount/resolve.js";
21
+ import { fakeIdentityAccountStore } from "../../identityaccount/__tests__/support.js";
12
22
  import { mkdtempSync, rmSync } from "node:fs";
13
23
  import { tmpdir } from "node:os";
14
24
  import path from "node:path";
@@ -94,11 +104,12 @@ function newExecution(sessionId: string, agentId: string): AgentExecution {
94
104
 
95
105
  function newContext(
96
106
  execution: AgentExecution,
107
+ caller: CallerIdentity = testCallerIdentity(),
97
108
  ): RequestContext<typeof AgentExecutionSchema> {
98
109
  return new RequestContext(
99
110
  AgentExecutionSchema,
100
111
  execution,
101
- testCallerIdentity(),
112
+ caller,
102
113
  ApiResourceKind.agent_execution,
103
114
  );
104
115
  }
@@ -476,7 +487,7 @@ describe("newComposeDeclaredPreferencesStep", () => {
476
487
  "server-owned field must be stamped on every path",
477
488
  ).toBeDefined();
478
489
  expect(got?.orgContext).toBe(tt.wantOrgContext);
479
- // user_context must stay empty in OSS (no per-request user identity).
490
+ // user_context stays empty under the single-operator posture.
480
491
  expect(got?.userContext).toBe("");
481
492
  });
482
493
  }
@@ -775,6 +786,223 @@ describe("newComposeRecalledMemoriesStep", () => {
775
786
  }
776
787
  });
777
788
 
789
+ // ---------------------------------------------------------------------------
790
+ // Where callers are persons (stigmer#1387, stigmer#1397).
791
+ // ---------------------------------------------------------------------------
792
+
793
+ describe("the compose steps where callers are persons", () => {
794
+ const CAROL = "ida_carol";
795
+ const DAVE = "ida_dave";
796
+
797
+ /** Carol at the console: a wire user whose identity is her account id. */
798
+ const carol = testCallerIdentity({ identityId: CAROL });
799
+
800
+ function directory(preferences: {
801
+ memoryEnabled?: boolean;
802
+ standingContext?: string;
803
+ }): AccountsByCaller {
804
+ const accounts = fakeIdentityAccountStore();
805
+ accounts.rows.set(
806
+ CAROL,
807
+ create(IdentityAccountSchema, {
808
+ metadata: { id: CAROL, name: "Carol" },
809
+ spec: { idpId: "auth0|carol", preferences },
810
+ }),
811
+ );
812
+ return accounts;
813
+ }
814
+
815
+ const faultingDirectory: AccountsByCaller = {
816
+ findById: async () => {
817
+ throw new Error("simulated store fault");
818
+ },
819
+ findDirectByIdpId: async () => {
820
+ throw new Error("simulated store fault");
821
+ },
822
+ };
823
+
824
+ /** A JWT carrying `payload`, unsigned: the gate reads, never verifies. */
825
+ function jwtWith(payload: Record<string, unknown>): string {
826
+ const segment = (value: object) =>
827
+ Buffer.from(JSON.stringify(value)).toString("base64url");
828
+ return `${segment({ alg: "RS256", typ: "JWT" })}.${segment(payload)}.signature`;
829
+ }
830
+
831
+ /** Every lane that is not a person speaking for themselves. */
832
+ const notFirstParty: Array<[string, CallerIdentity]> = [
833
+ [
834
+ "a runner acting as Carol",
835
+ testCallerIdentity({ identityId: CAROL, callerClass: "runner" }),
836
+ ],
837
+ [
838
+ "a machine account",
839
+ testCallerIdentity({ identityId: CAROL, callerClass: "machine" }),
840
+ ],
841
+ [
842
+ "a cloud channel sender",
843
+ testCallerIdentity({ identityId: CAROL, callerClass: "channel" }),
844
+ ],
845
+ [
846
+ "the schedule fire acting as Carol (in-process)",
847
+ testCallerIdentity({ identityId: CAROL, origin: "in-process" }),
848
+ ],
849
+ [
850
+ "Carol through a PlatformClient user token",
851
+ testCallerIdentity({
852
+ identityId: CAROL,
853
+ issuer: "stigmer",
854
+ rawToken: jwtWith({
855
+ iss: "stigmer",
856
+ sub: CAROL,
857
+ platform_client_id: "pc_1",
858
+ }),
859
+ }),
860
+ ],
861
+ ];
862
+
863
+ async function seedRecallRows(): Promise<void> {
864
+ await seedMemoryOrg("test-org", true);
865
+ const confirmed = MemoryLifecycleState.lifecycle_state_confirmed;
866
+ await seedMemory({
867
+ id: "mem_carol",
868
+ orgId: "test-org",
869
+ subject: CAROL,
870
+ content: "Carol's",
871
+ state: confirmed,
872
+ createdAt: new Date("2026-09-01T00:00:00Z"),
873
+ });
874
+ await seedMemory({
875
+ id: "mem_dave",
876
+ orgId: "test-org",
877
+ subject: DAVE,
878
+ content: "Dave's",
879
+ state: confirmed,
880
+ createdAt: new Date("2026-09-02T00:00:00Z"),
881
+ });
882
+ await seedMemory({
883
+ id: "mem_sentinel",
884
+ orgId: "test-org",
885
+ subject: "",
886
+ content: "nobody's",
887
+ state: confirmed,
888
+ createdAt: new Date("2026-09-03T00:00:00Z"),
889
+ });
890
+ await seedMemory({
891
+ id: "mem_carol_proposed",
892
+ orgId: "test-org",
893
+ subject: CAROL,
894
+ content: "unconfirmed",
895
+ state: MemoryLifecycleState.lifecycle_state_proposed,
896
+ createdAt: new Date("2026-09-04T00:00:00Z"),
897
+ });
898
+ }
899
+
900
+ async function recall(
901
+ accounts: AccountsByCaller | undefined,
902
+ caller: CallerIdentity,
903
+ ) {
904
+ const step = newComposeRecalledMemoriesStep(store, silentLogger, accounts);
905
+ const ctx = newContext(newExecution("ses_1", "agt_1"), caller);
906
+ await step.execute(ctx);
907
+ return ctx.newState.spec?.recalledMemories;
908
+ }
909
+
910
+ it("recalls exactly the run's person's confirmed facts", async () => {
911
+ await seedRecallRows();
912
+ const got = await recall(directory({ memoryEnabled: true }), carol);
913
+ expect(got?.enabled).toBe(true);
914
+ expect(got?.facts.map((f) => f.memoryId)).toEqual(["mem_carol"]);
915
+ });
916
+
917
+ it("recalls nothing and offers no remember tool when the person's own switch is off", async () => {
918
+ await seedRecallRows();
919
+ const got = await recall(directory({ memoryEnabled: false }), carol);
920
+ expect(got?.enabled).toBe(false);
921
+ expect(got?.facts).toEqual([]);
922
+ });
923
+
924
+ it("recalls nothing when the organization's switch is off, whatever the person's", async () => {
925
+ await seedMemoryOrg("test-org", false);
926
+ const got = await recall(directory({ memoryEnabled: true }), carol);
927
+ expect(got?.enabled).toBe(false);
928
+ });
929
+
930
+ it("recalls nothing for a caller no account stands for", async () => {
931
+ await seedRecallRows();
932
+ const got = await recall(
933
+ directory({ memoryEnabled: true }),
934
+ testCallerIdentity({ identityId: "auth0|stranger" }),
935
+ );
936
+ expect(got?.enabled).toBe(false);
937
+ });
938
+
939
+ it("degrades to disabled on a directory fault (best-effort)", async () => {
940
+ await seedRecallRows();
941
+ const got = await recall(faultingDirectory, carol);
942
+ expect(got?.enabled).toBe(false);
943
+ });
944
+
945
+ for (const [name, caller] of notFirstParty) {
946
+ it(`recalls nothing for ${name}`, async () => {
947
+ await seedRecallRows();
948
+ const got = await recall(directory({ memoryEnabled: true }), caller);
949
+ expect(got?.enabled).toBe(false);
950
+ expect(got?.facts).toEqual([]);
951
+ });
952
+ }
953
+
954
+ async function declared(
955
+ accounts: AccountsByCaller | undefined,
956
+ caller: CallerIdentity,
957
+ ) {
958
+ await seedOrg("test-org", "We deploy to us-east-1.");
959
+ const step = newComposeDeclaredPreferencesStep(
960
+ store,
961
+ silentLogger,
962
+ accounts,
963
+ );
964
+ const execution = newExecution("ses_1", "agt_1");
965
+ execution.spec!.declaredPreferences = create(DeclaredPreferencesSchema, {
966
+ userContext: "injected user context",
967
+ });
968
+ const ctx = newContext(execution, caller);
969
+ await step.execute(ctx);
970
+ return ctx.newState.spec?.declaredPreferences;
971
+ }
972
+
973
+ it("composes the run's person's standing context beside the organization's", async () => {
974
+ const got = await declared(
975
+ directory({ standingContext: "Call me Carol." }),
976
+ carol,
977
+ );
978
+ expect(got?.orgContext).toBe("We deploy to us-east-1.");
979
+ expect(got?.userContext).toBe("Call me Carol.");
980
+ });
981
+
982
+ it("keeps the organization's context and composes no person's on a directory fault", async () => {
983
+ const got = await declared(faultingDirectory, carol);
984
+ expect(got?.orgContext).toBe("We deploy to us-east-1.");
985
+ expect(got?.userContext).toBe("");
986
+ });
987
+
988
+ it("composes no person's context under the single-operator posture", async () => {
989
+ const got = await declared(undefined, carol);
990
+ expect(got?.orgContext).toBe("We deploy to us-east-1.");
991
+ expect(got?.userContext).toBe("");
992
+ });
993
+
994
+ for (const [name, caller] of notFirstParty) {
995
+ it(`composes no person's context for ${name}, and keeps the organization's`, async () => {
996
+ const got = await declared(
997
+ directory({ standingContext: "Call me Carol." }),
998
+ caller,
999
+ );
1000
+ expect(got?.orgContext).toBe("We deploy to us-east-1.");
1001
+ expect(got?.userContext).toBe("");
1002
+ });
1003
+ }
1004
+ });
1005
+
778
1006
  // ---------------------------------------------------------------------------
779
1007
  // agentCallTaskEnvironmentRefs (create_execution_context_workflow_refs_test.go).
780
1008
  // ---------------------------------------------------------------------------
@@ -8,10 +8,15 @@
8
8
  * Proven by agentexecution.conformance.test.ts
9
9
  * (CONFORMANCE_TARGET=local) and __tests__/.
10
10
  *
11
- * Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies,
12
- * Publish, PublishToRedis, and TransformResponse steps (no multi-tenant
13
- * auth, IAM/FGA, event publishing, or Redis here — subscribe streams ride
14
- * in-memory channels per ADR 011).
11
+ * Every chain opens with Authorize, and create also authorizes its run
12
+ * target (AuthorizeRunTarget); create and delete run the shared
13
+ * tuple-lifecycle steps against the composed lifecycle. The direct
14
+ * handlers — subscribe and the two artifact reads — evaluate their
15
+ * annotation through authorizeDirect; uploadAttachment is authorized at
16
+ * the create its storage key feeds. list, listBySession and
17
+ * getExecutionSummary narrow through the composed list read scope.
18
+ * Subscribe streams ride the in-memory stream broker (ADR 011); there is
19
+ * no Redis. Per-RPC posture: docs/authorization-coverage.md §16.
15
20
  */
16
21
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
17
22
 
@@ -31,6 +36,7 @@ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/
31
36
  import type { Logger } from "../../boot/logger.js";
32
37
  import type { Authorizer } from "../../extensions/authorizer.js";
33
38
  import type { ListReadScope } from "../../extensions/list-read-scope.js";
39
+ import type { AccountsByCaller } from "../identityaccount/resolve.js";
34
40
  import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
35
41
  import type { ResolvedGateSteps } from "../../extensions/gate-slots.js";
36
42
  import { stepsForSlot } from "../../extensions/gate-slots.js";
@@ -151,6 +157,14 @@ export interface AgentExecutionControllerDeps {
151
157
  readonly authorizationLifecycle: ResourceAuthorizationLifecycle | undefined;
152
158
  /** The composed summary read scope — undefined = the OSS full scan (C2 Stage 4). */
153
159
  readonly listReadScope: ListReadScope | undefined;
160
+ /**
161
+ * The directory of the persons callers stand for (stigmer#1387) — the
162
+ * composed identity-account port, bound under the require-authentication
163
+ * posture; undefined = the single-operator posture (trusted-local),
164
+ * where memory is the organization's switch alone and the subject the
165
+ * empty-string sentinel.
166
+ */
167
+ readonly personAccounts: AccountsByCaller | undefined;
154
168
  /**
155
169
  * The shared broadcast fabric for subscribe streams. ONE instance spans
156
170
  * both routers (serving + in-process) — see stream-broker.ts; the
@@ -360,8 +374,20 @@ async function createExecution(
360
374
  sessionCreator: deps.sessionCreator,
361
375
  }),
362
376
  )
363
- .addStep(newComposeDeclaredPreferencesStep(deps.store, deps.logger))
364
- .addStep(newComposeRecalledMemoriesStep(deps.store, deps.logger))
377
+ .addStep(
378
+ newComposeDeclaredPreferencesStep(
379
+ deps.store,
380
+ deps.logger,
381
+ deps.personAccounts,
382
+ ),
383
+ )
384
+ .addStep(
385
+ newComposeRecalledMemoriesStep(
386
+ deps.store,
387
+ deps.logger,
388
+ deps.personAccounts,
389
+ ),
390
+ )
365
391
  .addStep(newSetInitialPhaseStep())
366
392
  .addStep(newCreateExecutionContextStep(deps.executionContextBuilder))
367
393
  .addStep(newProcessAttachmentsStep(deps.logger))