@stigmer/server 3.39.1-dev.20260928194002 → 3.40.1

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 (126) 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 +43 -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 +12 -3
  39. package/dist/domain/channelapp/controller.d.ts.map +1 -1
  40. package/dist/domain/channelapp/controller.js +19 -9
  41. package/dist/domain/channelapp/controller.js.map +1 -1
  42. package/dist/domain/environment/controller.d.ts +8 -5
  43. package/dist/domain/environment/controller.d.ts.map +1 -1
  44. package/dist/domain/environment/controller.js +3 -3
  45. package/dist/domain/environment/controller.js.map +1 -1
  46. package/dist/domain/executioncontext/controller.d.ts +7 -4
  47. package/dist/domain/executioncontext/controller.d.ts.map +1 -1
  48. package/dist/domain/executioncontext/controller.js.map +1 -1
  49. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  50. package/dist/domain/mcpserver/controller.js +6 -2
  51. package/dist/domain/mcpserver/controller.js.map +1 -1
  52. package/dist/domain/memory/constants.d.ts +9 -0
  53. package/dist/domain/memory/constants.d.ts.map +1 -1
  54. package/dist/domain/memory/constants.js +9 -0
  55. package/dist/domain/memory/constants.js.map +1 -1
  56. package/dist/domain/memory/controller.d.ts +18 -6
  57. package/dist/domain/memory/controller.d.ts.map +1 -1
  58. package/dist/domain/memory/controller.js +18 -21
  59. package/dist/domain/memory/controller.js.map +1 -1
  60. package/dist/domain/memory/steps.d.ts +38 -10
  61. package/dist/domain/memory/steps.d.ts.map +1 -1
  62. package/dist/domain/memory/steps.js +87 -17
  63. package/dist/domain/memory/steps.js.map +1 -1
  64. package/dist/domain/organization/controller.d.ts +7 -4
  65. package/dist/domain/organization/controller.d.ts.map +1 -1
  66. package/dist/domain/organization/controller.js.map +1 -1
  67. package/dist/domain/session/controller.d.ts +7 -3
  68. package/dist/domain/session/controller.d.ts.map +1 -1
  69. package/dist/domain/session/controller.js.map +1 -1
  70. package/dist/domain/skill/controller.d.ts.map +1 -1
  71. package/dist/domain/skill/controller.js +8 -2
  72. package/dist/domain/skill/controller.js.map +1 -1
  73. package/dist/domain/workflow/controller.d.ts +6 -2
  74. package/dist/domain/workflow/controller.d.ts.map +1 -1
  75. package/dist/domain/workflow/controller.js.map +1 -1
  76. package/dist/domain/workflowexecution/controller.d.ts +8 -4
  77. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  78. package/dist/domain/workflowexecution/controller.js.map +1 -1
  79. package/dist/domain/workflowexecution/update-status.d.ts.map +1 -1
  80. package/dist/domain/workflowexecution/update-status.js +3 -2
  81. package/dist/domain/workflowexecution/update-status.js.map +1 -1
  82. package/dist/extensions/identity.d.ts +28 -14
  83. package/dist/extensions/identity.d.ts.map +1 -1
  84. package/dist/extensions/identity.js +38 -0
  85. package/dist/extensions/identity.js.map +1 -1
  86. package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
  87. package/dist/pipeline/steps/guard-memory-capture.js +1 -13
  88. package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
  89. package/package.json +6 -6
  90. package/src/artifactstorage/__tests__/artifact-storage.test.ts +15 -1
  91. package/src/artifactstorage/artifact-storage.ts +12 -4
  92. package/src/boot/__tests__/artifact-lane.test.ts +71 -0
  93. package/src/boot/__tests__/compose.test.ts +73 -3
  94. package/src/boot/__tests__/config.test.ts +19 -0
  95. package/src/boot/artifact-lane.ts +96 -0
  96. package/src/boot/compose.ts +58 -21
  97. package/src/boot/config.ts +25 -15
  98. package/src/domain/agent/controller.ts +5 -2
  99. package/src/domain/agentexecution/__tests__/create-steps.test.ts +230 -2
  100. package/src/domain/agentexecution/controller.ts +32 -6
  101. package/src/domain/agentexecution/create-steps.ts +162 -62
  102. package/src/domain/agentinstance/controller.ts +8 -2
  103. package/src/domain/agentshare/controller.ts +12 -7
  104. package/src/domain/artifact/file-server.ts +8 -4
  105. package/src/domain/channelapp/controller.ts +38 -11
  106. package/src/domain/environment/controller.ts +11 -8
  107. package/src/domain/executioncontext/controller.ts +7 -4
  108. package/src/domain/mcpserver/controller.ts +6 -2
  109. package/src/domain/memory/__tests__/capture-credential.test.ts +6 -6
  110. package/src/domain/memory/__tests__/person-consent-composed.test.ts +187 -0
  111. package/src/domain/memory/__tests__/person-consent.test.ts +274 -0
  112. package/src/domain/memory/constants.ts +11 -0
  113. package/src/domain/memory/controller.ts +36 -27
  114. package/src/domain/memory/steps.ts +112 -17
  115. package/src/domain/organization/controller.ts +7 -4
  116. package/src/domain/plugin/__tests__/plugin.test.ts +8 -0
  117. package/src/domain/session/controller.ts +7 -3
  118. package/src/domain/skill/__tests__/skill.test.ts +4 -0
  119. package/src/domain/skill/controller.ts +8 -2
  120. package/src/domain/workflow/controller.ts +6 -2
  121. package/src/domain/workflowexecution/controller.ts +8 -4
  122. package/src/domain/workflowexecution/update-status.ts +3 -2
  123. package/src/extensions/__tests__/identity.test.ts +82 -0
  124. package/src/extensions/__tests__/list-read-scope-composed.test.ts +39 -1
  125. package/src/extensions/identity.ts +47 -0
  126. package/src/pipeline/steps/guard-memory-capture.ts +4 -19
@@ -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))
@@ -34,6 +34,7 @@ import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_
34
34
  import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
35
35
  import type { SessionSpec } from "@stigmer/protos/ai/stigmer/agentic/session/v1/spec_pb";
36
36
  import { SessionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/spec_pb";
37
+ import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
37
38
  import type { Organization } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
38
39
  import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
39
40
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
@@ -41,6 +42,7 @@ import { clone } from "@bufbuild/protobuf";
41
42
 
42
43
  import type { Logger } from "../../boot/logger.js";
43
44
  import type { CallerIdentity } from "../../extensions/identity.js";
45
+ import { isFirstPartyHumanOperator } from "../../extensions/identity.js";
44
46
  import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
45
47
  import { notifyDefaultInstanceLinked } from "../../pipeline/steps/authorization-tuples.js";
46
48
  import {
@@ -53,9 +55,12 @@ import {
53
55
  } from "../../pipeline/errors.js";
54
56
  import { ConnectError } from "@connectrpc/connect";
55
57
  import type { PipelineStep } from "../../pipeline/pipeline.js";
58
+ import type { RequestContext } from "../../pipeline/request-context.js";
56
59
  import { ResourceNotFoundError } from "../../store/interface.js";
57
60
  import type { Store } from "../../store/interface.js";
58
61
  import { buildDefaultInstanceRequest } from "../agentinstance/defaultinstance.js";
62
+ import type { AccountsByCaller } from "../identityaccount/resolve.js";
63
+ import { accountForCaller } from "../identityaccount/resolve.js";
59
64
 
60
65
  import type { AgentExecutionStatusObserver } from "../../extensions/status-hooks.js";
61
66
 
@@ -416,19 +421,27 @@ export function newCreateSessionIfNeededStep(deps: {
416
421
  // ---------------------------------------------------------------------------
417
422
 
418
423
  /**
419
- * Snapshots the organization's declared standing context onto the
420
- * execution spec (DD-002, stigmer/stigmer#293). SERVER-OWNED: stamped
421
- * unconditionally, overwriting anything the caller supplied. OSS composes
422
- * org_context only (the local server has no per-request user identity).
423
- * BEST-EFFORT: an execution must never fail to start because its optional
424
- * preferences could not load — genuine failures log at ERROR (quiet
425
- * degradation of a should-work path must stay visible) and degrade to an
426
- * empty snapshot. Snapshot-at-create is the point: preferences are
424
+ * Snapshots the declared standing context onto the execution spec
425
+ * (DD-002, stigmer/stigmer#293). SERVER-OWNED: stamped unconditionally,
426
+ * overwriting anything the caller supplied. Two independent halves:
427
+ * org_context, the organization's, for every run; and user_context, the
428
+ * run's person's own (stigmer#1397) — composed only where callers are
429
+ * persons (`personAccounts`, the require-authentication posture) and only
430
+ * for a first-party human operator (runPersonOf), as the Java step did.
431
+ * The single-operator posture composes org_context alone: its one
432
+ * operator's context is the organization's. The org half deliberately
433
+ * keeps running for every lane (schedule and channel runs carry the
434
+ * organization's instructions today); only the person's half is about a
435
+ * person. BEST-EFFORT: an execution must never fail to start because its
436
+ * optional preferences could not load — genuine failures log at ERROR
437
+ * (quiet degradation of a should-work path must stay visible) and degrade
438
+ * that half to empty. Snapshot-at-create is the point: preferences are
427
439
  * mutable, executions are immutable audit records.
428
440
  */
429
441
  export function newComposeDeclaredPreferencesStep(
430
442
  store: Store,
431
443
  logger: Logger,
444
+ personAccounts?: AccountsByCaller,
432
445
  ): PipelineStep<CreateDesc> {
433
446
  return {
434
447
  name: "ComposeDeclaredPreferences",
@@ -438,50 +451,103 @@ export function newComposeDeclaredPreferencesStep(
438
451
  // Claim the server-owned field first, before any load can fail.
439
452
  execution.spec.declaredPreferences = create(DeclaredPreferencesSchema);
440
453
 
441
- let orgId = execution.metadata?.org ?? "";
442
- if (orgId === "") {
443
- orgId = ctx.input.metadata?.org ?? "";
444
- }
445
- if (orgId === "") {
446
- logger.debug(
447
- "No org on execution metadata, composing no declared preferences",
448
- );
449
- return;
450
- }
451
-
452
- let org: Organization;
453
- try {
454
- org = await store.getResource(
455
- ApiResourceKind.organization,
456
- orgId,
457
- OrganizationSchema,
458
- );
459
- } catch (error) {
460
- if (error instanceof ResourceNotFoundError) {
461
- logger.debug(
462
- "Org not found in store, composing no declared preferences",
463
- { orgId },
464
- );
465
- return;
466
- }
467
- logger.error(
468
- "Failed to load org for declared preferences - degrading to none (best-effort contract)",
469
- {
470
- orgId,
471
- error: error instanceof Error ? error.message : String(error),
472
- },
473
- );
474
- return;
475
- }
476
-
477
454
  // Verbatim per DD-002 D2: the server stamps content only;
478
455
  // blank-is-absent is the runner's read-side convention.
479
456
  execution.spec.declaredPreferences.orgContext =
480
- org.spec?.preferences?.standingContext ?? "";
457
+ await loadOrgStandingContext(store, logger, orgIdOf(ctx));
458
+
459
+ if (personAccounts === undefined) {
460
+ return;
461
+ }
462
+ const person = await runPersonOf(
463
+ personAccounts,
464
+ ctx.callerIdentity,
465
+ logger,
466
+ "Failed to load the run's person for declared preferences - degrading user context to none (best-effort contract)",
467
+ );
468
+ execution.spec.declaredPreferences.userContext =
469
+ person?.spec?.preferences?.standingContext ?? "";
481
470
  },
482
471
  };
483
472
  }
484
473
 
474
+ /** The org a create addresses: the execution's metadata, else the input's. */
475
+ function orgIdOf(ctx: RequestContext<CreateDesc>): string {
476
+ const orgId = ctx.newState.metadata?.org ?? "";
477
+ return orgId !== "" ? orgId : (ctx.input.metadata?.org ?? "");
478
+ }
479
+
480
+ /** The organization's standing context, best-effort ("" on every degrade). */
481
+ async function loadOrgStandingContext(
482
+ store: Store,
483
+ logger: Logger,
484
+ orgId: string,
485
+ ): Promise<string> {
486
+ if (orgId === "") {
487
+ logger.debug(
488
+ "No org on execution metadata, composing no declared preferences",
489
+ );
490
+ return "";
491
+ }
492
+
493
+ let org: Organization;
494
+ try {
495
+ org = await store.getResource(
496
+ ApiResourceKind.organization,
497
+ orgId,
498
+ OrganizationSchema,
499
+ );
500
+ } catch (error) {
501
+ if (error instanceof ResourceNotFoundError) {
502
+ logger.debug(
503
+ "Org not found in store, composing no declared preferences",
504
+ {
505
+ orgId,
506
+ },
507
+ );
508
+ return "";
509
+ }
510
+ logger.error(
511
+ "Failed to load org for declared preferences - degrading to none (best-effort contract)",
512
+ {
513
+ orgId,
514
+ error: error instanceof Error ? error.message : String(error),
515
+ },
516
+ );
517
+ return "";
518
+ }
519
+ return org.spec?.preferences?.standingContext ?? "";
520
+ }
521
+
522
+ /**
523
+ * The person a run is created by, where callers are persons
524
+ * (stigmer#1387): the account a first-party human operator stands for
525
+ * (extensions/identity.ts states who that is, once). `undefined` for every
526
+ * other lane — runner, schedule, channel, guest, machine, a PlatformClient
527
+ * token, anything server-composed — for a caller no account stands for,
528
+ * and, best-effort, on a store fault (logged at ERROR with the caller's
529
+ * copy: the compose steps degrade, they never fail a create).
530
+ */
531
+ async function runPersonOf(
532
+ personAccounts: AccountsByCaller,
533
+ caller: CallerIdentity,
534
+ logger: Logger,
535
+ faultMessage: string,
536
+ ): Promise<IdentityAccount | undefined> {
537
+ if (!isFirstPartyHumanOperator(caller)) {
538
+ return undefined;
539
+ }
540
+ try {
541
+ return await accountForCaller(personAccounts, caller);
542
+ } catch (error) {
543
+ logger.error(faultMessage, {
544
+ identityId: caller.identityId,
545
+ error: error instanceof Error ? error.message : String(error),
546
+ });
547
+ return undefined;
548
+ }
549
+ }
550
+
485
551
  // ---------------------------------------------------------------------------
486
552
  // ComposeRecalledMemories — compose_recalled_memories_step.go.
487
553
  // ---------------------------------------------------------------------------
@@ -492,18 +558,25 @@ export function newComposeDeclaredPreferencesStep(
492
558
  * memory loop, sibling of ComposeDeclaredPreferences in every invariant:
493
559
  * server-owned (enabled=false stamped on every ineligible/degraded path),
494
560
  * best-effort (degrading to DISABLED, never enabled-with-zero-facts, so a
495
- * broken recall never falsely offers the remember tool). OSS gates on the
496
- * org flag alone with the empty-string subject sentinel; confirmed-only
561
+ * broken recall never falsely offers the remember tool); confirmed-only
497
562
  * (the consent gate is meaningless otherwise); no compose-time truncation
498
563
  * (the runner's retriever selects at prompt build, recorded on
499
564
  * status.recalled_memories_report). Facts order oldest-first on
500
- * created_at — identical prompt order in both editions. The Organization
501
- * is loaded independently of the preferences step: step independence over
502
- * one saved read.
565
+ * created_at — identical prompt order in every posture.
566
+ *
567
+ * WHOSE memories follows the posture (stigmer#1387, the Java step's
568
+ * gates in its order): where callers are persons (`personAccounts`) the
569
+ * run must be created by a first-party human operator (runPersonOf), the
570
+ * organization's memory_enabled must be on, then that person's own — and
571
+ * the snapshot is that person's confirmed facts in the organization. The
572
+ * single-operator posture gates on the org flag alone with the
573
+ * empty-string subject sentinel. The Organization is loaded independently
574
+ * of the preferences step: step independence over one saved read.
503
575
  */
504
576
  export function newComposeRecalledMemoriesStep(
505
577
  store: Store,
506
578
  logger: Logger,
579
+ personAccounts?: AccountsByCaller,
507
580
  ): PipelineStep<CreateDesc> {
508
581
  return {
509
582
  name: "ComposeRecalledMemories",
@@ -513,16 +586,21 @@ export function newComposeRecalledMemoriesStep(
513
586
  // Claim the server-owned field first.
514
587
  execution.spec.recalledMemories = create(RecalledMemoriesSchema);
515
588
 
516
- let orgId = execution.metadata?.org ?? "";
517
- if (orgId === "") {
518
- orgId = ctx.input.metadata?.org ?? "";
519
- }
589
+ const orgId = orgIdOf(ctx);
520
590
  if (orgId === "") {
521
591
  logger.debug(
522
592
  "No org on execution metadata, composing no recalled memories",
523
593
  );
524
594
  return;
525
595
  }
596
+ // Only a person's own run recalls (the Java gate order: the caller
597
+ // first, before any read).
598
+ if (
599
+ personAccounts !== undefined &&
600
+ !isFirstPartyHumanOperator(ctx.callerIdentity)
601
+ ) {
602
+ return;
603
+ }
526
604
 
527
605
  let org: Organization;
528
606
  try {
@@ -555,9 +633,27 @@ export function newComposeRecalledMemoriesStep(
555
633
  return;
556
634
  }
557
635
 
636
+ // The subject whose facts are recalled: the run's person, whose own
637
+ // switch must be on too, or the single-operator sentinel.
638
+ let subject = "";
639
+ if (personAccounts !== undefined) {
640
+ const person = await runPersonOf(
641
+ personAccounts,
642
+ ctx.callerIdentity,
643
+ logger,
644
+ "Failed to load the run's person for recalled memories - degrading to disabled (best-effort contract)",
645
+ );
646
+ if (person?.spec?.preferences?.memoryEnabled !== true) {
647
+ // A person who has not opted in is normal operation, not
648
+ // degradation; no log.
649
+ return;
650
+ }
651
+ subject = person.metadata?.id ?? "";
652
+ }
653
+
558
654
  let facts;
559
655
  try {
560
- facts = await loadConfirmedFacts(store, orgId);
656
+ facts = await loadConfirmedFacts(store, orgId, subject);
561
657
  } catch (error) {
562
658
  // Enabled stays false: a broken recall must not offer the
563
659
  // remember tool (the enabled bit doubles as the tool signal).
@@ -582,13 +678,17 @@ export function newComposeRecalledMemoriesStep(
582
678
  }
583
679
 
584
680
  /**
585
- * Scans the memory kind for the OSS single-user subject's (the ""
586
- * sentinel) confirmed records in the org, oldest-first. Facts carry only
587
- * memory_id + content (the id is the transparency link back to the
588
- * addressable record). Undecodable rows are skipped — one bad record must
589
- * not take recall down.
681
+ * Scans the memory kind for `subject`'s confirmed records in the org,
682
+ * oldest-first — the run's person's account id, or the single-operator
683
+ * "" sentinel. Facts carry only memory_id + content (the id is the
684
+ * transparency link back to the addressable record). Undecodable rows are
685
+ * skipped — one bad record must not take recall down.
590
686
  */
591
- async function loadConfirmedFacts(store: Store, orgId: string) {
687
+ async function loadConfirmedFacts(
688
+ store: Store,
689
+ orgId: string,
690
+ subject: string,
691
+ ) {
592
692
  const rows = await store.listResources(ApiResourceKind.memory);
593
693
  const memories: Memory[] = [];
594
694
  for (const data of rows) {
@@ -601,7 +701,7 @@ async function loadConfirmedFacts(store: Store, orgId: string) {
601
701
  if ((memory.metadata?.org ?? "") !== orgId) {
602
702
  continue;
603
703
  }
604
- if ((memory.spec?.subjectIdentityAccountId ?? "") !== "") {
704
+ if ((memory.spec?.subjectIdentityAccountId ?? "") !== subject) {
605
705
  continue;
606
706
  }
607
707
  if (
@@ -10,8 +10,14 @@
10
10
  * Proven by agentinstance.conformance.test.ts (CONFORMANCE_TARGET=local),
11
11
  * __tests__/agentinstance.test.ts and __tests__/store-faults.test.ts.
12
12
  *
13
- * Versus Stigmer Cloud, OSS excludes the CreateIamPolicies and Publish
14
- * steps. Deliberately NO same-org rule on create, unlike WorkflowInstance:
13
+ * Every chain opens with Authorize, and create asks
14
+ * can_create_agent_instance on the organization, then can_create_instance
15
+ * on the parent agent (AuthorizeResolvedTarget); create, delete and
16
+ * updateVisibility run the shared tuple-lifecycle steps against the
17
+ * composed lifecycle; getByAgent and list narrow through the composed list
18
+ * read scope (docs/authorization-coverage.md §7).
19
+ *
20
+ * Deliberately NO same-org rule on create, unlike WorkflowInstance:
15
21
  * an agent is a shareable blueprint, and one agent legitimately has
16
22
  * instances in several orgs (the marketplace case). What governs a
17
23
  * cross-org instance in both editions is the create lane's authorization