@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.
- package/dist/artifactstorage/artifact-storage.d.ts +10 -2
- package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
- package/dist/artifactstorage/artifact-storage.js +6 -3
- package/dist/artifactstorage/artifact-storage.js.map +1 -1
- package/dist/boot/artifact-lane.d.ts +55 -0
- package/dist/boot/artifact-lane.d.ts.map +1 -0
- package/dist/boot/artifact-lane.js +74 -0
- package/dist/boot/artifact-lane.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +43 -19
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +11 -6
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js +10 -5
- package/dist/boot/config.js.map +1 -1
- package/dist/domain/agent/controller.d.ts +5 -2
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentexecution/controller.d.ts +18 -4
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +2 -2
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/create-steps.d.ts +29 -15
- package/dist/domain/agentexecution/create-steps.d.ts.map +1 -1
- package/dist/domain/agentexecution/create-steps.js +118 -51
- package/dist/domain/agentexecution/create-steps.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +8 -2
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts +9 -4
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +3 -3
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/artifact/file-server.d.ts +2 -1
- package/dist/domain/artifact/file-server.d.ts.map +1 -1
- package/dist/domain/artifact/file-server.js +6 -3
- package/dist/domain/artifact/file-server.js.map +1 -1
- package/dist/domain/channelapp/controller.d.ts +12 -3
- package/dist/domain/channelapp/controller.d.ts.map +1 -1
- package/dist/domain/channelapp/controller.js +19 -9
- package/dist/domain/channelapp/controller.js.map +1 -1
- package/dist/domain/environment/controller.d.ts +8 -5
- package/dist/domain/environment/controller.d.ts.map +1 -1
- package/dist/domain/environment/controller.js +3 -3
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/executioncontext/controller.d.ts +7 -4
- package/dist/domain/executioncontext/controller.d.ts.map +1 -1
- package/dist/domain/executioncontext/controller.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +6 -2
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/memory/constants.d.ts +9 -0
- package/dist/domain/memory/constants.d.ts.map +1 -1
- package/dist/domain/memory/constants.js +9 -0
- package/dist/domain/memory/constants.js.map +1 -1
- package/dist/domain/memory/controller.d.ts +18 -6
- package/dist/domain/memory/controller.d.ts.map +1 -1
- package/dist/domain/memory/controller.js +18 -21
- package/dist/domain/memory/controller.js.map +1 -1
- package/dist/domain/memory/steps.d.ts +38 -10
- package/dist/domain/memory/steps.d.ts.map +1 -1
- package/dist/domain/memory/steps.js +87 -17
- package/dist/domain/memory/steps.js.map +1 -1
- package/dist/domain/organization/controller.d.ts +7 -4
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/session/controller.d.ts +7 -3
- package/dist/domain/session/controller.d.ts.map +1 -1
- package/dist/domain/session/controller.js.map +1 -1
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +8 -2
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/workflow/controller.d.ts +6 -2
- package/dist/domain/workflow/controller.d.ts.map +1 -1
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts +8 -4
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowexecution/update-status.d.ts.map +1 -1
- package/dist/domain/workflowexecution/update-status.js +3 -2
- package/dist/domain/workflowexecution/update-status.js.map +1 -1
- package/dist/extensions/identity.d.ts +28 -14
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js +38 -0
- package/dist/extensions/identity.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +1 -13
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/package.json +6 -6
- package/src/artifactstorage/__tests__/artifact-storage.test.ts +15 -1
- package/src/artifactstorage/artifact-storage.ts +12 -4
- package/src/boot/__tests__/artifact-lane.test.ts +71 -0
- package/src/boot/__tests__/compose.test.ts +73 -3
- package/src/boot/__tests__/config.test.ts +19 -0
- package/src/boot/artifact-lane.ts +96 -0
- package/src/boot/compose.ts +58 -21
- package/src/boot/config.ts +25 -15
- package/src/domain/agent/controller.ts +5 -2
- package/src/domain/agentexecution/__tests__/create-steps.test.ts +230 -2
- package/src/domain/agentexecution/controller.ts +32 -6
- package/src/domain/agentexecution/create-steps.ts +162 -62
- package/src/domain/agentinstance/controller.ts +8 -2
- package/src/domain/agentshare/controller.ts +12 -7
- package/src/domain/artifact/file-server.ts +8 -4
- package/src/domain/channelapp/controller.ts +38 -11
- package/src/domain/environment/controller.ts +11 -8
- package/src/domain/executioncontext/controller.ts +7 -4
- package/src/domain/mcpserver/controller.ts +6 -2
- package/src/domain/memory/__tests__/capture-credential.test.ts +6 -6
- package/src/domain/memory/__tests__/person-consent-composed.test.ts +187 -0
- package/src/domain/memory/__tests__/person-consent.test.ts +274 -0
- package/src/domain/memory/constants.ts +11 -0
- package/src/domain/memory/controller.ts +36 -27
- package/src/domain/memory/steps.ts +112 -17
- package/src/domain/organization/controller.ts +7 -4
- package/src/domain/plugin/__tests__/plugin.test.ts +8 -0
- package/src/domain/session/controller.ts +7 -3
- package/src/domain/skill/__tests__/skill.test.ts +4 -0
- package/src/domain/skill/controller.ts +8 -2
- package/src/domain/workflow/controller.ts +6 -2
- package/src/domain/workflowexecution/controller.ts +8 -4
- package/src/domain/workflowexecution/update-status.ts +3 -2
- package/src/extensions/__tests__/identity.test.ts +82 -0
- package/src/extensions/__tests__/list-read-scope-composed.test.ts +39 -1
- package/src/extensions/identity.ts +47 -0
- 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 {
|
|
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
|
+
}
|
package/src/boot/compose.ts
CHANGED
|
@@ -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:
|
|
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
|
|
968
|
-
//
|
|
969
|
-
//
|
|
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
|
|
package/src/boot/config.ts
CHANGED
|
@@ -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
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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
|
|
105
|
-
*
|
|
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
|
-
//
|
|
298
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
-
*
|
|
15
|
-
*
|
|
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
|
|