@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,7 +7,12 @@
7
7
  * - start() flips SERVING BEFORE the port binds, so the first probe that
8
8
  * reaches the port already sees a serving server;
9
9
  * - shutdown flips NOT_SERVING FIRST, then drains — and the port stops
10
- * answering.
10
+ * answering;
11
+ * - the artifact download lane binds before SERVING, so a lane that
12
+ * cannot bind fails start() with the address and the setting named and
13
+ * the server never reports itself serving (stigmer#1089), and a server
14
+ * on an ephemeral port gets an ephemeral lane whose minted download
15
+ * URLs reach it.
11
16
  */
12
17
  import { createClient } from "@connectrpc/connect";
13
18
  import { createGrpcTransport } from "@connectrpc/connect-node";
@@ -15,7 +20,13 @@ import {
15
20
  Health,
16
21
  HealthCheckResponse_ServingStatus as ServingStatus,
17
22
  } from "@stigmer/protos/grpc/health/v1/health_pb";
18
- import { connect as netConnect } from "node:net";
23
+ import { ArtifactCommandController } from "@stigmer/protos/ai/stigmer/agentic/artifact/v1/command_pb";
24
+ import { ArtifactQueryController } from "@stigmer/protos/ai/stigmer/agentic/artifact/v1/query_pb";
25
+ import {
26
+ createServer as netCreateServer,
27
+ connect as netConnect,
28
+ } from "node:net";
29
+ import type { AddressInfo } from "node:net";
19
30
  import { describe, expect, it } from "vitest";
20
31
 
21
32
  import { mkdtempSync } from "node:fs";
@@ -32,11 +43,12 @@ const silentLogger = createLogger({
32
43
  write: () => {},
33
44
  });
34
45
 
35
- function compose() {
46
+ function compose(env: NodeJS.ProcessEnv = {}) {
36
47
  // Each composed server gets a throwaway database — the storage stage
37
48
  // opens DB_PATH for real (never the developer's ~/.stigmer).
38
49
  const testDir = mkdtempSync(path.join(tmpdir(), "compose-test-"));
39
50
  const config = loadConfig({
51
+ ...env,
40
52
  STIGMER_MODEL_REGISTRY_REFRESH: "off",
41
53
  // No engine behind composed tests: 127.0.0.1:1 is deterministically
42
54
  // closed, so boots fail the non-fatal connect fast and can never touch
@@ -95,3 +107,61 @@ describe("composition-root boot ordering", () => {
95
107
  expect(refused, "the drained port must refuse new connections").toBe(true);
96
108
  });
97
109
  });
110
+
111
+ describe("the artifact download lane", () => {
112
+ it("fails start() when the lane cannot bind, naming the address and the setting, and never serves", async () => {
113
+ const holder = netCreateServer();
114
+ await new Promise<void>((resolve) =>
115
+ holder.listen(0, "127.0.0.1", resolve),
116
+ );
117
+ const held = (holder.address() as AddressInfo).port;
118
+ try {
119
+ const server = await compose({ ARTIFACT_HTTP_PORT: String(held) });
120
+ try {
121
+ await expect(server.start()).rejects.toThrow(
122
+ `the artifact file server could not bind '127.0.0.1:${held}' (EADDRINUSE): ARTIFACT_HTTP_PORT=${held} cannot be bound; set it to a free port`,
123
+ );
124
+ // SERVING is set before the unified port binds, so a server that is
125
+ // still NOT_SERVING never opened it.
126
+ expect(server.healthState.status("")).toBe(ServingStatus.NOT_SERVING);
127
+ } finally {
128
+ await server.shutdown();
129
+ }
130
+ } finally {
131
+ await new Promise<void>((resolve) => holder.close(() => resolve()));
132
+ }
133
+ });
134
+
135
+ it("binds an ephemeral lane beside an ephemeral unified port, and a minted download URL reaches it", async () => {
136
+ const server = await compose();
137
+ const port = await server.start();
138
+ try {
139
+ const transport = createGrpcTransport({
140
+ baseUrl: `http://127.0.0.1:${port}`,
141
+ });
142
+ const command = createClient(ArtifactCommandController, transport);
143
+ const query = createClient(ArtifactQueryController, transport);
144
+ const content = new TextEncoder().encode("ephemeral lane body\n");
145
+ const created = await command.create({
146
+ spec: {
147
+ displayName: "ephemeral-lane.txt",
148
+ contentType: "text/plain",
149
+ source: { agentExecutionId: "aexec_01ephemerallane" },
150
+ },
151
+ content,
152
+ });
153
+ const download = await query.getDownloadUrl({
154
+ value: created.metadata!.id,
155
+ });
156
+
157
+ // An OS-assigned port, never the privileged port 1 that +1 derived
158
+ // from 0; the bytes coming back prove it is THIS server's lane.
159
+ expect(Number(new URL(download.url).port)).toBeGreaterThan(1024);
160
+ const served = await fetch(download.url);
161
+ expect(served.status).toBe(200);
162
+ expect(new Uint8Array(await served.arrayBuffer())).toEqual(content);
163
+ } finally {
164
+ await server.shutdown();
165
+ }
166
+ });
167
+ });
@@ -71,6 +71,25 @@ describe("loadConfig", () => {
71
71
  );
72
72
  });
73
73
 
74
+ // stigmer#1089: the artifact lane's port and serve URL follow the unified
75
+ // port the composition binds (boot/artifact-lane.ts), so the loader keeps
76
+ // only what the operator set and derives nothing.
77
+ it("leaves an unset or malformed ARTIFACT_HTTP_PORT unset, for the lane to derive", () => {
78
+ expect(loadConfig({}).artifactHttpPort).toBeUndefined();
79
+ expect(loadConfig({ GRPC_PORT: "0" }).artifactHttpPort).toBeUndefined();
80
+ expect(loadConfig({ ARTIFACT_HTTP_PORT: "7300x" }).artifactHttpPort).toBeUndefined();
81
+ expect(loadConfig({ ARTIFACT_HTTP_PORT: "7300" }).artifactHttpPort).toBe(7300);
82
+ expect(loadConfig({ ARTIFACT_HTTP_PORT: "0" }).artifactHttpPort).toBe(0);
83
+ });
84
+
85
+ it("leaves an unset ARTIFACT_LOCAL_SERVE_URL empty, for the bound lane to answer", () => {
86
+ expect(loadConfig({}).artifactLocalServeUrl).toBe("");
87
+ expect(
88
+ loadConfig({ ARTIFACT_LOCAL_SERVE_URL: "https://artifacts.stigmer.test" })
89
+ .artifactLocalServeUrl,
90
+ ).toBe("https://artifacts.stigmer.test");
91
+ });
92
+
74
93
  // 20260913.02 (sp.console-login, Q-CL-1): the console's public PKCE client
75
94
  // id is lenient on purpose — a CLI-only self-host that set the issuer
76
95
  // before this knob existed must keep booting on upgrade, and the served
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Where the artifact file server listens, and the URL local artifact
3
+ * storage mints downloads under, on a server that was not told.
4
+ *
5
+ * The artifact download lane is a second loopback listener beside the
6
+ * unified port (domain/artifact/file-server.ts), bound only when artifact
7
+ * storage is local. `ARTIFACT_HTTP_PORT` and `ARTIFACT_LOCAL_SERVE_URL`
8
+ * place it explicitly; unset, both follow the unified port, and this module
9
+ * is the one place that says how.
10
+ *
11
+ * The port rules, in order: a configured port always wins (0 included — an
12
+ * explicit ephemeral bind); a unified port of 0 (`GRPC_PORT=0`, and the
13
+ * `portOverride: 0` test seam) is not known until listen, so the lane binds
14
+ * ephemeral too; otherwise the lane takes the unified port + 1, the default
15
+ * every install has always had. Deriving +1 from an ephemeral 0 gave port 1,
16
+ * a privileged port, and a server that reported healthy with a dead download
17
+ * lane (stigmer#1089). The caller passes the port the unified listener will
18
+ * bind (`portOverride ?? grpcPort`), the input the OAuth redirect derivation
19
+ * reads (boot/oauth-redirect-uri.ts).
20
+ *
21
+ * The serve URL: a configured value always wins; unset, it is the lane's own
22
+ * origin on localhost, read from the port the lane actually bound. The
23
+ * composition binds the lane before the unified port opens, so no request can
24
+ * mint a URL before that port exists; a read before the bind is a
25
+ * construction-order fault and throws rather than minting a URL for a port
26
+ * nothing listens on.
27
+ *
28
+ * A lane that cannot bind fails the boot, beside the storage probe it
29
+ * belongs with; artifactLaneBindError is the copy the operator reads, naming
30
+ * the address, the cause and the setting that moves the lane.
31
+ *
32
+ * Proven by __tests__/artifact-lane.test.ts and the lane cases of
33
+ * __tests__/compose.test.ts.
34
+ */
35
+
36
+ export interface ArtifactLanePortInputs {
37
+ /** `ARTIFACT_HTTP_PORT` as loaded; undefined when unset. */
38
+ readonly configured: number | undefined;
39
+ /** The port the unified listener binds; 0 means "not known until listen". */
40
+ readonly unifiedPort: number;
41
+ }
42
+
43
+ /** The port the artifact file server binds; 0 asks the OS for an ephemeral one. */
44
+ export function resolveArtifactLanePort(
45
+ inputs: ArtifactLanePortInputs,
46
+ ): number {
47
+ if (inputs.configured !== undefined) return inputs.configured;
48
+ if (inputs.unifiedPort === 0) return 0;
49
+ return inputs.unifiedPort + 1;
50
+ }
51
+
52
+ /**
53
+ * The serve URL local artifact storage mints download URLs under: the
54
+ * configured value as given, or a resolver over the lane's bound port.
55
+ */
56
+ export function resolveArtifactServeUrl(
57
+ configured: string,
58
+ boundPort: () => number | undefined,
59
+ ): string | (() => string) {
60
+ if (configured !== "") return configured;
61
+ return () => {
62
+ const port = boundPort();
63
+ if (port === undefined) {
64
+ throw new Error(
65
+ "artifact download URL requested before the artifact file server bound its port",
66
+ );
67
+ }
68
+ return `http://localhost:${port}`;
69
+ };
70
+ }
71
+
72
+ /**
73
+ * The boot failure for a lane that could not bind: the address, the socket
74
+ * error's code, and the setting that moves the lane — named as the value
75
+ * that failed when the operator set it, as the default's escape hatch when
76
+ * the port was derived.
77
+ */
78
+ export function artifactLaneBindError(
79
+ cause: unknown,
80
+ host: string,
81
+ port: number,
82
+ configured: boolean,
83
+ ): Error {
84
+ const code =
85
+ cause instanceof Error &&
86
+ typeof (cause as NodeJS.ErrnoException).code === "string"
87
+ ? ` (${(cause as NodeJS.ErrnoException).code})`
88
+ : "";
89
+ const remedy = configured
90
+ ? `ARTIFACT_HTTP_PORT=${port} cannot be bound; set it to a free port`
91
+ : "set ARTIFACT_HTTP_PORT to a free port (unset, the lane takes the unified port + 1)";
92
+ return new Error(
93
+ `the artifact file server could not bind '${host}:${port}'${code}: ${remedy}`,
94
+ { cause },
95
+ );
96
+ }
@@ -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({
@@ -1439,6 +1467,7 @@ export async function composeServer(
1439
1467
  authorizer,
1440
1468
  secretService,
1441
1469
  authorizationLifecycle,
1470
+ listReadScope,
1442
1471
  });
1443
1472
  // Schedule registers between channelapp and memory (Go server.go
1444
1473
  // 417 → 426 → 436). The clock and runner ride constant providers —
@@ -1461,6 +1490,7 @@ export async function composeServer(
1461
1490
  authorizer,
1462
1491
  authorizationLifecycle,
1463
1492
  listReadScope,
1493
+ personAccounts,
1464
1494
  runnerCredentialProvider: runnerCredentials,
1465
1495
  });
1466
1496
  registerAgentExecutionServices(router, {
@@ -1469,6 +1499,7 @@ export async function composeServer(
1469
1499
  authorizer,
1470
1500
  authorizationLifecycle,
1471
1501
  listReadScope,
1502
+ personAccounts,
1472
1503
  runnerCredentialProvider: runnerCredentials,
1473
1504
  broker: agentExecutionStreamBroker,
1474
1505
  engineState: executionEngineState,
@@ -1846,6 +1877,28 @@ export async function composeServer(
1846
1877
  // Artifact storage must be reachable and writable before the server
1847
1878
  // answers (Go server.go boots-fatal on the same probe).
1848
1879
  await artifactStorage.health();
1880
+ // The download lane binds with the store it serves, and on the same
1881
+ // terms: a lane that cannot bind fails the boot, where the retired Go
1882
+ // server logged the failure and served on with every download dead
1883
+ // (stigmer#1089). Binding here, before SERVING and the unified port,
1884
+ // also means the derived serve URL exists before a request can mint
1885
+ // one.
1886
+ if (artifactFileServer !== undefined) {
1887
+ await warnOnLegacyArtifactLayout(config.artifactLocalBasePath, logger);
1888
+ try {
1889
+ artifactLaneBoundPort = await artifactFileServer.listen(
1890
+ artifactLanePort,
1891
+ config.artifactHttpHost,
1892
+ );
1893
+ } catch (error) {
1894
+ throw artifactLaneBindError(
1895
+ error,
1896
+ config.artifactHttpHost,
1897
+ artifactLanePort,
1898
+ config.artifactHttpPort !== undefined,
1899
+ );
1900
+ }
1901
+ }
1849
1902
  // Rebuild the search index before the port binds (Go
1850
1903
  // server.go:617): the index is separate from the resources table,
1851
1904
  // and rebuilding here makes every resource — including rows an
@@ -1884,22 +1937,6 @@ export async function composeServer(
1884
1937
  options.host,
1885
1938
  );
1886
1939
  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
1940
  return port;
1904
1941
  },
1905
1942
 
@@ -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