@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
@@ -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 (
@@ -1,23 +1,30 @@
1
1
  /**
2
- * Pins the store-fault contract of the agent instance's visibility-update
3
- * load: a typed ResourceNotFoundError answers NotFound with the domain's
4
- * pinned copy (`agent instance not found: <id>`), and any other store failure
5
- * is an infrastructure fault answered as a sanitized Internal, never a
6
- * NotFound that tells a client the instance does not exist
7
- * (stigmer/stigmer#1345).
2
+ * Pins the store-fault contract of the agent instance's two loads. The
3
+ * visibility-update load: a typed ResourceNotFoundError answers NotFound with
4
+ * the domain's pinned copy (`agent instance not found: <id>`), and any other
5
+ * store failure is an infrastructure fault answered as a sanitized Internal,
6
+ * never a NotFound that tells a client the instance does not exist
7
+ * (stigmer/stigmer#1345). Create's parent-agent load reads the agent through
8
+ * the in-process query client, so the store's typed miss arrives as a Connect
9
+ * NotFound: that answers the pinned `Agent not found: <id>`, and any other
10
+ * failure an Internal, never a missing parent (stigmer/stigmer#1351).
8
11
  *
9
- * The surface is updateVisibility, reached through the registered handler on
10
- * an in-process router. The composed suites reach only a real store, which
11
- * cannot fail selectively, so it runs here against a store whose read throws.
12
- * The parent-agent loader is untouchable: a load that fails must stop the
13
- * call before anything past it runs.
12
+ * Both surfaces are reached through the registered handler on an in-process
13
+ * router. The composed suites reach only a real store, which cannot fail
14
+ * selectively, so the visibility update runs against a store whose read
15
+ * throws and create against a parent loader whose read throws. Whatever the
16
+ * surface does not need is untouchable: a load that fails must stop the call
17
+ * before anything past it runs.
14
18
  *
15
- * Out of scope: the NotFound copy itself (wire contract) and create's
16
- * parent-agent load, which folds every RPC error into NotFound
17
- * (stigmer/stigmer#1351).
19
+ * Out of scope: the NotFound copies themselves (wire contract).
18
20
  */
19
- import type { Client, ConnectError } from "@connectrpc/connect";
20
- import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
21
+ import type { Client } from "@connectrpc/connect";
22
+ import {
23
+ Code,
24
+ ConnectError,
25
+ createClient,
26
+ createRouterTransport,
27
+ } from "@connectrpc/connect";
21
28
  import { describe, expect, it } from "vitest";
22
29
 
23
30
  import { AgentInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/command_pb";
@@ -31,11 +38,13 @@ import {
31
38
  failingStore,
32
39
  untouchable,
33
40
  } from "../../../pipeline/__tests__/support.js";
41
+ import { INTERNAL_FALLBACK_MESSAGE } from "../../../pipeline/pipeline.js";
34
42
  import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
35
43
  import { ResourceNotFoundError } from "../../../store/interface.js";
36
44
  import type { Store } from "../../../store/interface.js";
37
45
 
38
46
  import { registerAgentInstanceServices } from "../controller.js";
47
+ import type { ParentAgentLoaderProvider } from "../steps.js";
39
48
 
40
49
  const silentLogger = createLogger({
41
50
  level: "error",
@@ -44,22 +53,34 @@ const silentLogger = createLogger({
44
53
  });
45
54
 
46
55
  const INSTANCE_ID = "ain_storefault";
56
+ const AGENT_ID = "agt_storefault";
47
57
 
48
58
  const NOT_FOUND_COPY = `agent instance not found: ${INSTANCE_ID}`;
49
59
  const LOAD_FAULT_COPY = "failed to load agent instance";
60
+ const PARENT_NOT_FOUND_COPY = `Agent not found: ${AGENT_ID}`;
61
+ const PARENT_LOAD_FAULT_COPY = "failed to load parent agent";
50
62
 
51
63
  const MISSING = (): Error =>
52
64
  new ResourceNotFoundError(`agent_instance/${INSTANCE_ID}`);
53
65
  const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
54
66
 
67
+ /** What the agent query's `get` answers for a missing and a faulted agent. */
68
+ const PARENT_MISSING = (): Error =>
69
+ new ConnectError(`Agent not found: ${AGENT_ID}`, Code.NotFound);
70
+ const PARENT_FAULTED = (): Error =>
71
+ new ConnectError(INTERNAL_FALLBACK_MESSAGE, Code.Internal);
72
+
55
73
  /**
56
74
  * The registered handlers on an in-process router: the verifier chain stamps
57
75
  * the trusted-local caller and the apiresource interceptor the kind, then
58
76
  * Authorize and ValidateProto run and the load is the first step that reads
59
- * the store.
77
+ * the store or the parent.
60
78
  */
61
79
  function instanceCommand(
62
80
  store: Store,
81
+ parentAgentLoader: ParentAgentLoaderProvider = untouchable(
82
+ "parentAgentLoader",
83
+ ),
63
84
  ): Client<typeof AgentInstanceCommandController> {
64
85
  const transport = createRouterTransport(
65
86
  (router) => {
@@ -68,7 +89,7 @@ function instanceCommand(
68
89
  logger: silentLogger,
69
90
  authorizer: newPermissiveSingleTeamAuthorizer(),
70
91
  authorizationLifecycle: undefined,
71
- parentAgentLoader: untouchable("parentAgentLoader"),
92
+ parentAgentLoader,
72
93
  listReadScope: undefined,
73
94
  });
74
95
  },
@@ -108,3 +129,33 @@ describe("updateVisibility — LoadInstanceForVisibilityUpdate", () => {
108
129
  expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
109
130
  });
110
131
  });
132
+
133
+ describe("create — LoadParentAgent", () => {
134
+ function surfaceError(parentError: Error): Promise<ConnectError> {
135
+ const failingParent: ParentAgentLoaderProvider = () => ({
136
+ get: () => Promise.reject(parentError),
137
+ });
138
+ return errorOf(() =>
139
+ instanceCommand(untouchable("store"), failingParent).create({
140
+ apiVersion: "agentic.stigmer.ai/v1",
141
+ kind: "AgentInstance",
142
+ metadata: { name: "Store Fault Instance", org: "org_storefault" },
143
+ spec: { agentId: AGENT_ID },
144
+ }),
145
+ );
146
+ }
147
+
148
+ it("a missing parent agent answers NotFound with the pinned copy", async () => {
149
+ const error = await surfaceError(PARENT_MISSING());
150
+
151
+ expect(error.code).toBe(Code.NotFound);
152
+ expect(error.rawMessage).toBe(PARENT_NOT_FOUND_COPY);
153
+ });
154
+
155
+ it("any other parent-load failure answers a sanitized Internal", async () => {
156
+ const error = await surfaceError(PARENT_FAULTED());
157
+
158
+ expect(error.code).toBe(Code.Internal);
159
+ expect(error.rawMessage).toBe(PARENT_LOAD_FAULT_COPY);
160
+ });
161
+ });
@@ -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
@@ -6,6 +6,7 @@
6
6
  * contracts: the parent-agent edge, the immutable agent_id, the
7
7
  * default-instance visibility guard, and the org/label list filters.
8
8
  */
9
+ import { Code, ConnectError } from "@connectrpc/connect";
9
10
  import { create, fromBinary } from "@bufbuild/protobuf";
10
11
  import type { DescMessage } from "@bufbuild/protobuf";
11
12
 
@@ -69,7 +70,9 @@ export const PARENT_AGENT_KEY = "parent_agent";
69
70
  * an agent is a shareable blueprint and one agent legitimately has
70
71
  * instances in several orgs (the marketplace case) — the instance's
71
72
  * organization is the caller's, and the check below asks the caller's
72
- * standing there.
73
+ * standing there. The agent is read through the in-process query client, so
74
+ * a missing one arrives as a Connect NotFound; any other failure is a fault
75
+ * answered as Internal, never a missing parent.
73
76
  */
74
77
  export function newLoadParentAgentStep(
75
78
  agentLoader: ParentAgentLoaderProvider,
@@ -88,11 +91,14 @@ export function newLoadParentAgentStep(
88
91
  try {
89
92
  parentAgent = await agentLoader().get(agentId);
90
93
  } catch (error) {
91
- logger.warn("Parent agent not found", {
92
- agentId,
93
- error: error instanceof Error ? error.message : String(error),
94
- });
95
- throw notFoundError("Agent", agentId);
94
+ if (error instanceof ConnectError && error.code === Code.NotFound) {
95
+ logger.warn("Parent agent not found", {
96
+ agentId,
97
+ error: error.rawMessage,
98
+ });
99
+ throw notFoundError("Agent", agentId);
100
+ }
101
+ throw internalError(error, "failed to load parent agent");
96
102
  }
97
103
 
98
104
  logger.debug("Loaded parent agent", {
@@ -13,10 +13,15 @@
13
13
  * cascade of same-org shares lives in the AGENT domain (ported with #6);
14
14
  * share delete itself cascades nothing.
15
15
  *
16
- * Authorization posture (OSS): single-user and local, so handlers perform
17
- * no authorization — a documented no-op, not a silent divergence. The
18
- * cloud edition enforces the same contracts via FGA plus app-level gates
19
- * for the anonymous resolution paths.
16
+ * Every chain opens with Authorize. create asks can_edit on the referenced
17
+ * agent, then can_create_agent_share (AuthorizeResolvedTarget); create and
18
+ * delete run the shared tuple-lifecycle steps against the composed
19
+ * lifecycle; getByReference authorizes the loaded share as `get` would;
20
+ * getByAgent and list narrow through the composed list read scope. The
21
+ * public share-link read skips the Authorizer through is_public, and the
22
+ * member read asks can_view on the organization before the share loads
23
+ * (AuthorizeMemberAudience). Per-RPC posture:
24
+ * docs/authorization-coverage.md §9.
20
25
  *
21
26
  * Proven by agentshare.conformance.test.ts (CONFORMANCE_TARGET=local),
22
27
  * __tests__/agentshare.test.ts and __tests__/store-faults.test.ts.
@@ -657,9 +662,9 @@ function newLoadSharesByAgentStep(
657
662
  const LIST_RESULT_KEY = "listResult";
658
663
 
659
664
  /**
660
- * List — org + labels filter (AND semantics), newest first. Versus Cloud,
661
- * OSS excludes authorization filtering AND pagination, exactly as Go's
662
- * list.go records.
665
+ * List — org + labels filter (AND semantics), newest first, narrowed to
666
+ * the caller's authorized rows by the composed list read scope.
667
+ * Unpaginated, exactly as Go's list.go records.
663
668
  */
664
669
  async function list(
665
670
  deps: AgentShareControllerDeps,
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * The artifact HTTP file server — ports the local-download lane from Go
3
3
  * pkg/server/server.go (artifactDownloadHandler + the boot block, lines
4
- * 849–926): a second listener on 127.0.0.1:ARTIFACT_HTTP_PORT (default
5
- * grpcPort+1), started only when artifact storage is LOCAL, serving
6
- * GET /<key> as the exact bytes LocalArtifactStorage wrote. The lane
4
+ * 849–926): a second listener on 127.0.0.1:ARTIFACT_HTTP_PORT (unset: the
5
+ * unified port + 1, ephemeral beside an ephemeral unified port — the rule is
6
+ * boot/artifact-lane.ts), started only when artifact storage is LOCAL,
7
+ * serving GET /<key> as the exact bytes LocalArtifactStorage wrote. Unlike
8
+ * Go, a bind failure fails the boot: the composition binds the lane before
9
+ * SERVING and lets listen()'s rejection stand (stigmer#1089). The lane
7
10
  * dissolved into its owning domain per the ratified D4 decision — it is
8
11
  * NOT a unified-port lane (Go runs it as a separate listener, and so does
9
12
  * this port).
@@ -39,7 +42,8 @@ export interface ArtifactFileServerOptions {
39
42
 
40
43
  export interface ArtifactFileServer {
41
44
  /**
42
- * Binds <host>:<port> (an explicit port 0 picks ephemeral for tests).
45
+ * Binds <host>:<port> (port 0 picks ephemeral) and resolves the port it
46
+ * bound; a bind failure rejects, and the composition fails the boot on it.
43
47
  * The host is the composition root's call: 127.0.0.1 everywhere by
44
48
  * default (ARTIFACT_HTTP_HOST, DD-013) — download URLs are minted for
45
49
  * the local machine — and 0.0.0.0 only inside the official container
@@ -14,9 +14,14 @@
14
14
  * not_search_indexed — configuration reached through its parent surface,
15
15
  * not a library artifact), so no index steps appear in any chain.
16
16
  *
17
- * Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies, and
18
- * Publish steps (single-user local posture); validation, secret handling,
19
- * and the referential delete block are byte-compatible.
17
+ * Every chain opens with Authorize; create and delete run the shared
18
+ * tuple-lifecycle steps against the composed lifecycle; getByReference
19
+ * loads, then authorizes the loaded app exactly as `get` would
20
+ * (AuthorizeResolvedTarget). listByOrg does not yet narrow through the
21
+ * list read scope, although the model restricts viewing to owners,
22
+ * explicit grants and organization admins
23
+ * (fga/model/agentic/channel_app.fga, stigmer/stigmer#1384). Per-RPC
24
+ * posture: docs/authorization-coverage.md §13.
20
25
  *
21
26
  * Proven by channelapp.conformance.test.ts (CONFORMANCE_TARGET=local)
22
27
  * and __tests__/channelapp.test.ts.
@@ -11,11 +11,14 @@
11
11
  * Proven by environment.conformance.test.ts (CONFORMANCE_TARGET=local),
12
12
  * __tests__/environment.test.ts and __tests__/store-faults.test.ts.
13
13
  *
14
- * Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies,
15
- * FGA-tuple, and Publish steps (no multi-tenant auth, IAM/FGA, or event
16
- * publishing here); the visibility ceiling (org — secrets never resolve
17
- * across the org boundary) and share restrictions run in BOTH editions
18
- * from the same proto config, keeping the error contract identical.
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 environment exactly as
17
+ * `get` would (AuthorizeResolvedTarget); list narrows through the composed
18
+ * list read scope. The visibility ceiling (org — secrets never resolve
19
+ * across the org boundary) and share restrictions come from the same proto
20
+ * config in every edition, keeping the error contract identical. Per-RPC
21
+ * posture: docs/authorization-coverage.md §3.
19
22
  */
20
23
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
21
24
  import { create, fromBinary } from "@bufbuild/protobuf";
@@ -734,9 +737,9 @@ async function getSecretValue(
734
737
  const LIST_RESULT_KEY = "listResult";
735
738
 
736
739
  /**
737
- * List — org + labels filter with per-item redaction. Versus Cloud, OSS
738
- * excludes authorization filtering AND pagination (returns all matches),
739
- * exactly as Go's list.go records.
740
+ * List — org + labels filter with per-item redaction, narrowed to the
741
+ * caller's authorized rows by the composed list read scope. Unpaginated
742
+ * (returns all matches), exactly as Go's list.go records.
740
743
  */
741
744
  async function list(
742
745
  deps: EnvironmentControllerDeps,
@@ -25,10 +25,13 @@
25
25
  * (the decrypt-lane matrix conformance deliberately never exercises —
26
26
  * its harness authenticates as a user).
27
27
  *
28
- * Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies,
29
- * and Publish steps (no multi-tenant auth, IAM/FGA, or event publishing
30
- * here); the redaction and decrypt-lane contracts run in BOTH editions,
31
- * keeping the error contract identical.
28
+ * Every chain opens with Authorize; create asks can_create_execution_in on
29
+ * the organization (AuthorizeCreate), and create and delete run the shared
30
+ * tuple-lifecycle steps against the composed lifecycle; getByReference
31
+ * loads, then authorizes the loaded context exactly as `get` would
32
+ * (AuthorizeResolvedTarget). The redaction and decrypt-lane contracts are
33
+ * the same in every edition, keeping the error contract identical. Per-RPC
34
+ * posture: docs/authorization-coverage.md §5.
32
35
  */
33
36
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
34
37