@stigmer/server 3.18.1-dev.20260919070736 → 3.19.1-dev.20260920100552
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/authorization/model/index.d.ts +1 -1
- package/dist/authorization/model/index.d.ts.map +1 -1
- package/dist/authorization/model/index.js +0 -2
- package/dist/authorization/model/index.js.map +1 -1
- package/dist/authorization/model/organization.d.ts.map +1 -1
- package/dist/authorization/model/organization.js +0 -1
- package/dist/authorization/model/organization.js.map +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +2 -15
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/inprocess.d.ts +2 -13
- package/dist/boot/inprocess.d.ts.map +1 -1
- package/dist/boot/inprocess.js +6 -16
- package/dist/boot/inprocess.js.map +1 -1
- package/dist/domain/agentexecution/lifecycle.js +1 -1
- package/dist/domain/agentexecution/lifecycle.js.map +1 -1
- package/dist/domain/mcpserver/initiate-oauth-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/initiate-oauth-connect.js +22 -1
- package/dist/domain/mcpserver/initiate-oauth-connect.js.map +1 -1
- package/dist/domain/workflow/registry/data/task-kind-registry.json +0 -1
- package/dist/domain/workflowexecution/lifecycle.js +1 -1
- package/dist/domain/workflowexecution/lifecycle.js.map +1 -1
- package/dist/pipeline/interceptors/auth.d.ts +1 -1
- package/dist/pipeline/interceptors/auth.js +1 -1
- package/dist/query/search/registry.d.ts +3 -5
- package/dist/query/search/registry.d.ts.map +1 -1
- package/dist/query/search/registry.js +3 -7
- package/dist/query/search/registry.js.map +1 -1
- package/dist/sandbox/kubernetes.d.ts.map +1 -1
- package/dist/sandbox/kubernetes.js +8 -1
- package/dist/sandbox/kubernetes.js.map +1 -1
- package/dist/sandbox/provisioner.d.ts +20 -5
- package/dist/sandbox/provisioner.d.ts.map +1 -1
- package/dist/sandbox/provisioner.js.map +1 -1
- package/dist/sandbox/steps.d.ts +22 -8
- package/dist/sandbox/steps.d.ts.map +1 -1
- package/dist/sandbox/steps.js +17 -12
- package/dist/sandbox/steps.js.map +1 -1
- package/dist/store/postgres/migrations.d.ts +3 -1
- package/dist/store/postgres/migrations.d.ts.map +1 -1
- package/dist/store/postgres/migrations.js +19 -1
- package/dist/store/postgres/migrations.js.map +1 -1
- package/dist/store/sqlite/migrations.d.ts +3 -1
- package/dist/store/sqlite/migrations.d.ts.map +1 -1
- package/dist/store/sqlite/migrations.js +22 -1
- package/dist/store/sqlite/migrations.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/__tests__/derived-tuples.test.ts +1 -1
- package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +5 -5
- package/src/authorization/__tests__/wire-permissions.test.ts +0 -1
- package/src/authorization/model/__tests__/registry.test.ts +0 -9
- package/src/authorization/model/index.ts +1 -3
- package/src/authorization/model/organization.ts +0 -1
- package/src/boot/compose.ts +2 -15
- package/src/boot/inprocess.ts +8 -29
- package/src/domain/agentexecution/lifecycle.ts +1 -1
- package/src/domain/iampolicy/__tests__/role-lifecycle.test.ts +0 -1
- package/src/domain/mcpserver/__tests__/oauth-handshake.test.ts +1 -1
- package/src/domain/mcpserver/initiate-oauth-connect.ts +21 -1
- package/src/domain/workflow/registry/data/task-kind-registry.json +0 -1
- package/src/domain/workflowexecution/lifecycle.ts +1 -1
- package/src/pipeline/__tests__/router-transport.test.ts +1 -1
- package/src/pipeline/interceptors/auth.ts +1 -1
- package/src/query/__tests__/query-services.test.ts +16 -18
- package/src/query/search/__tests__/criteria.test.ts +3 -6
- package/src/query/search/__tests__/query-store.test.ts +21 -21
- package/src/query/search/registry.ts +3 -7
- package/src/sandbox/__tests__/docker.integration.test.ts +2 -0
- package/src/sandbox/__tests__/kubernetes.test.ts +10 -2
- package/src/sandbox/__tests__/local-process.integration.test.ts +3 -0
- package/src/sandbox/__tests__/steps.test.ts +72 -19
- package/src/sandbox/kubernetes.ts +8 -1
- package/src/sandbox/provisioner.ts +20 -5
- package/src/sandbox/steps.ts +29 -12
- package/src/store/postgres/__tests__/migrations.test.ts +23 -4
- package/src/store/postgres/migrations.ts +20 -1
- package/src/store/sqlite/__tests__/migrations.test.ts +26 -2
- package/src/store/sqlite/migrations.ts +23 -1
- package/dist/authorization/model/project.d.ts +0 -2
- package/dist/authorization/model/project.d.ts.map +0 -1
- package/dist/authorization/model/project.js +0 -32
- package/dist/authorization/model/project.js.map +0 -1
- package/dist/domain/project/controller.d.ts +0 -48
- package/dist/domain/project/controller.d.ts.map +0 -1
- package/dist/domain/project/controller.js +0 -192
- package/dist/domain/project/controller.js.map +0 -1
- package/dist/domain/project/reconcile.d.ts +0 -77
- package/dist/domain/project/reconcile.d.ts.map +0 -1
- package/dist/domain/project/reconcile.js +0 -179
- package/dist/domain/project/reconcile.js.map +0 -1
- package/dist/domain/project/search-extractor.d.ts +0 -3
- package/dist/domain/project/search-extractor.d.ts.map +0 -1
- package/dist/domain/project/search-extractor.js +0 -43
- package/dist/domain/project/search-extractor.js.map +0 -1
- package/src/authorization/model/project.ts +0 -47
- package/src/domain/project/__tests__/project.composed.test.ts +0 -381
- package/src/domain/project/__tests__/reconcile.test.ts +0 -447
- package/src/domain/project/controller.ts +0 -387
- package/src/domain/project/reconcile.ts +0 -296
- package/src/domain/project/search-extractor.ts +0 -71
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* #439 newly-searchable-kind arm), plus the scope-filter matrix
|
|
7
7
|
* (org strict / crossOrgPublic / excludePublic) and RebuildIndex —
|
|
8
8
|
* including the DD-D proof: rebuilding over an ADOPTED Go-created
|
|
9
|
-
* database re-indexes
|
|
9
|
+
* database re-indexes the rows it already holds. The escaping and
|
|
10
10
|
* score-normalization tables moved with their code into the sqlite
|
|
11
11
|
* driver (DD-009 seam redraw): see store/sqlite/__tests__/fts5.test.ts.
|
|
12
12
|
* These tests still run the REAL driver through tempStore — the
|
|
@@ -18,9 +18,9 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
18
18
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
19
19
|
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
20
20
|
import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
21
|
+
import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
21
22
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
22
23
|
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
23
|
-
import { ProjectSchema } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/api_pb";
|
|
24
24
|
|
|
25
25
|
import type { DescMessage, MessageShape } from "@bufbuild/protobuf";
|
|
26
26
|
|
|
@@ -426,33 +426,33 @@ describe("rebuildIndex", () => {
|
|
|
426
426
|
expect(after.results[0]?.id).toBe("ses-1");
|
|
427
427
|
});
|
|
428
428
|
|
|
429
|
-
it("re-indexes
|
|
429
|
+
it("re-indexes existing rows on an ADOPTED Go-created database (DD-D)", async () => {
|
|
430
430
|
// The committed Go-v6 fixture, opened through the driver (migrates
|
|
431
|
-
// v6→
|
|
431
|
+
// v6→current — real adoption), then given a skill row the way a Go
|
|
432
432
|
// server would have written one: raw resource bytes.
|
|
433
433
|
const fixture = materializeGoFixture();
|
|
434
434
|
const adopted = SqliteStore.open(fixture.dbPath);
|
|
435
435
|
try {
|
|
436
|
-
const
|
|
436
|
+
const skill = create(SkillSchema, {
|
|
437
437
|
metadata: {
|
|
438
|
-
id: "
|
|
438
|
+
id: "skl_1",
|
|
439
439
|
name: "billing",
|
|
440
440
|
slug: "billing",
|
|
441
441
|
org: "acme",
|
|
442
442
|
},
|
|
443
|
-
spec: { description: "the billing
|
|
443
|
+
spec: { description: "the billing skill" },
|
|
444
444
|
status: {
|
|
445
445
|
audit: { specAudit: { createdAt: { seconds: 1_700_000_000n } } },
|
|
446
446
|
},
|
|
447
447
|
});
|
|
448
448
|
await adopted.saveResource(
|
|
449
|
-
ApiResourceKind.
|
|
450
|
-
"
|
|
451
|
-
|
|
452
|
-
|
|
449
|
+
ApiResourceKind.skill,
|
|
450
|
+
"skl_1",
|
|
451
|
+
SkillSchema,
|
|
452
|
+
skill,
|
|
453
453
|
);
|
|
454
|
-
// Boot state: the TS server wipes and rebuilds — a
|
|
455
|
-
// would erase this row from search forever.
|
|
454
|
+
// Boot state: the TS server wipes and rebuilds — a registry without
|
|
455
|
+
// the kind would erase this row from search forever.
|
|
456
456
|
const adoptedQueryStore = new SqliteSearchQueryStore(
|
|
457
457
|
adopted,
|
|
458
458
|
registry,
|
|
@@ -461,21 +461,21 @@ describe("rebuildIndex", () => {
|
|
|
461
461
|
await adoptedQueryStore.rebuildIndex();
|
|
462
462
|
|
|
463
463
|
const result = await adoptedQueryStore.search(
|
|
464
|
-
criteria({ kinds: [ApiResourceKind.
|
|
464
|
+
criteria({ kinds: [ApiResourceKind.skill], org: "acme" }),
|
|
465
465
|
);
|
|
466
466
|
expect(result.totalCount).toBe(1);
|
|
467
|
-
expect(result.results[0]?.id).toBe("
|
|
468
|
-
expect(result.results[0]?.description).toBe("the billing
|
|
467
|
+
expect(result.results[0]?.id).toBe("skl_1");
|
|
468
|
+
expect(result.results[0]?.description).toBe("the billing skill");
|
|
469
469
|
|
|
470
470
|
// toBinary sanity: the row loaded back is byte-equal to what a Go
|
|
471
471
|
// server holds (protobuf wire format is shared).
|
|
472
472
|
const raw = await adopted.getResource(
|
|
473
|
-
ApiResourceKind.
|
|
474
|
-
"
|
|
475
|
-
|
|
473
|
+
ApiResourceKind.skill,
|
|
474
|
+
"skl_1",
|
|
475
|
+
SkillSchema,
|
|
476
476
|
);
|
|
477
|
-
expect(toBinary(
|
|
478
|
-
toBinary(
|
|
477
|
+
expect(toBinary(SkillSchema, raw)).toEqual(
|
|
478
|
+
toBinary(SkillSchema, skill),
|
|
479
479
|
);
|
|
480
480
|
} finally {
|
|
481
481
|
await adopted.close();
|
|
@@ -12,11 +12,9 @@
|
|
|
12
12
|
* (registry.go ValidateExpectedKinds; server.go:509): a missing extractor
|
|
13
13
|
* degrades that kind to unsearchable, never refuses boot.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* database may already hold projects; without the extractor those rows
|
|
19
|
-
* would silently vanish from search (D4 #14 DD-D, owner-ratified).
|
|
15
|
+
* Boot RebuildIndex re-indexes every registered kind from the resources
|
|
16
|
+
* table, so a kind with no extractor here is a kind whose rows vanish from
|
|
17
|
+
* search on the next boot; a domain and its extractor land together.
|
|
20
18
|
*/
|
|
21
19
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
22
20
|
|
|
@@ -28,7 +26,6 @@ import { executionContextSearchExtractor } from "../../domain/executioncontext/s
|
|
|
28
26
|
import { mcpServerSearchExtractor } from "../../domain/mcpserver/search-extractor.js";
|
|
29
27
|
import { organizationSearchExtractor } from "../../domain/organization/search-extractor.js";
|
|
30
28
|
import { pluginSearchExtractor } from "../../domain/plugin/search-extractor.js";
|
|
31
|
-
import { projectSearchExtractor } from "../../domain/project/search-extractor.js";
|
|
32
29
|
import { sessionSearchExtractor } from "../../domain/session/search-extractor.js";
|
|
33
30
|
import { skillSearchExtractor } from "../../domain/skill/search-extractor.js";
|
|
34
31
|
import { workflowSearchExtractor } from "../../domain/workflow/search-extractor.js";
|
|
@@ -116,7 +113,6 @@ export function newSearchableResourceRegistry(): SearchableResourceRegistry {
|
|
|
116
113
|
mcpServerSearchExtractor,
|
|
117
114
|
organizationSearchExtractor,
|
|
118
115
|
pluginSearchExtractor,
|
|
119
|
-
projectSearchExtractor,
|
|
120
116
|
sessionSearchExtractor,
|
|
121
117
|
skillSearchExtractor,
|
|
122
118
|
workflowSearchExtractor,
|
|
@@ -68,6 +68,7 @@ describe.skipIf(!optedIn || !dockerAnswers())(
|
|
|
68
68
|
await driver.ensureSessionSandbox(sessionId, {
|
|
69
69
|
taskQueue: `session:${sessionId}`,
|
|
70
70
|
stigmerToken: "tok-docker-smoke",
|
|
71
|
+
callerClass: "user",
|
|
71
72
|
});
|
|
72
73
|
expect(await driver.probe("session", sessionId)).toBe("running");
|
|
73
74
|
|
|
@@ -91,6 +92,7 @@ describe.skipIf(!optedIn || !dockerAnswers())(
|
|
|
91
92
|
await driver.ensureSessionSandbox(sessionId, {
|
|
92
93
|
taskQueue: `session:${sessionId}`,
|
|
93
94
|
stigmerToken: "tok-docker-smoke",
|
|
95
|
+
callerClass: "user",
|
|
94
96
|
});
|
|
95
97
|
expect(await driver.probe("session", sessionId)).toBe("running");
|
|
96
98
|
|
|
@@ -89,7 +89,11 @@ function fakeGateway(
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
const env = {
|
|
92
|
+
const env = {
|
|
93
|
+
taskQueue: "session:ses_1",
|
|
94
|
+
stigmerToken: "tok-1",
|
|
95
|
+
callerClass: "user",
|
|
96
|
+
};
|
|
93
97
|
|
|
94
98
|
describe("the ensure state machine", () => {
|
|
95
99
|
it("absent → Secret, PVC, Deployment — in that order, no readiness wait", async () => {
|
|
@@ -211,7 +215,11 @@ describe("the manifest shapes (the Java SandboxManifestFactory pins)", () => {
|
|
|
211
215
|
});
|
|
212
216
|
|
|
213
217
|
it("a token-less sandbox omits the token env but keeps the (empty) Secret", () => {
|
|
214
|
-
const tokenless = {
|
|
218
|
+
const tokenless = {
|
|
219
|
+
taskQueue: "session:ses_1",
|
|
220
|
+
stigmerToken: "",
|
|
221
|
+
callerClass: "user",
|
|
222
|
+
};
|
|
215
223
|
const deployment = buildSandboxDeployment(
|
|
216
224
|
"session",
|
|
217
225
|
"ses_1",
|
|
@@ -81,6 +81,7 @@ describe.skipIf(process.platform === "win32")(
|
|
|
81
81
|
await driver.ensureSessionSandbox("ses_smoke", {
|
|
82
82
|
taskQueue: "session:ses_smoke",
|
|
83
83
|
stigmerToken: "tok-smoke",
|
|
84
|
+
callerClass: "user",
|
|
84
85
|
});
|
|
85
86
|
await vi.waitFor(() => expect(spawnCount()).toBe(1), {
|
|
86
87
|
timeout: CHILD_SETTLE_TIMEOUT_MS,
|
|
@@ -91,6 +92,7 @@ describe.skipIf(process.platform === "win32")(
|
|
|
91
92
|
await driver.ensureSessionSandbox("ses_smoke", {
|
|
92
93
|
taskQueue: "session:ses_smoke",
|
|
93
94
|
stigmerToken: "tok-smoke",
|
|
95
|
+
callerClass: "user",
|
|
94
96
|
});
|
|
95
97
|
expect(spawnCount()).toBe(1);
|
|
96
98
|
|
|
@@ -124,6 +126,7 @@ describe.skipIf(process.platform === "win32")(
|
|
|
124
126
|
await driver.ensureSessionSandbox("ses_smoke", {
|
|
125
127
|
taskQueue: "session:ses_smoke",
|
|
126
128
|
stigmerToken: "",
|
|
129
|
+
callerClass: "user",
|
|
127
130
|
});
|
|
128
131
|
await vi.waitFor(() => expect(spawnCount()).toBe(2), {
|
|
129
132
|
timeout: CHILD_SETTLE_TIMEOUT_MS,
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* - the terminal observer deprovisions exactly on transitions INTO a
|
|
12
12
|
* terminal phase, fire-and-forget, and swallows (logs) teardown
|
|
13
13
|
* failures;
|
|
14
|
-
* - a disabled mint lane launches token-less rather than failing
|
|
14
|
+
* - a disabled mint lane launches token-less rather than failing;
|
|
15
|
+
* - the caller splits two ways on both lanes: the identity id reaches
|
|
16
|
+
* the credential mint, the caller class reaches the driver's
|
|
17
|
+
* environment unchanged (a composed lane such as `guest` included).
|
|
15
18
|
*/
|
|
16
19
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
17
20
|
import { tmpdir } from "node:os";
|
|
@@ -53,14 +56,25 @@ import {
|
|
|
53
56
|
ensureWorkflowSandboxForExecution,
|
|
54
57
|
newWorkflowSandboxTerminalObserver,
|
|
55
58
|
SANDBOX_PROVISIONING_FAILED_PREFIX,
|
|
59
|
+
type SandboxCaller,
|
|
56
60
|
} from "../steps.js";
|
|
57
61
|
|
|
58
62
|
/**
|
|
59
|
-
* The caller
|
|
60
|
-
* (C4
|
|
61
|
-
*
|
|
63
|
+
* The caller the ensure bodies split two ways: the identity id into the
|
|
64
|
+
* credential mint (C4 — the OSS execution-scoped mint ignores it, so the
|
|
65
|
+
* token assertions stay binding-shaped) and the class onto the driver's
|
|
66
|
+
* environment. `user` is the plain lane; the class-propagation cases hand
|
|
67
|
+
* in a composed lane's word to prove the body copies, never normalises.
|
|
62
68
|
*/
|
|
63
69
|
const TEST_CALLER_IDENTITY_ID = "ida_test_caller";
|
|
70
|
+
const TEST_CALLER: SandboxCaller = {
|
|
71
|
+
identityId: TEST_CALLER_IDENTITY_ID,
|
|
72
|
+
callerClass: "user",
|
|
73
|
+
};
|
|
74
|
+
const GUEST_CALLER: SandboxCaller = {
|
|
75
|
+
identityId: "ida_guest_caller",
|
|
76
|
+
callerClass: "guest",
|
|
77
|
+
};
|
|
64
78
|
|
|
65
79
|
const silentLogger = createLogger({
|
|
66
80
|
level: "error",
|
|
@@ -235,7 +249,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
235
249
|
temporalConfig: sessionRoutingCloudDefault,
|
|
236
250
|
},
|
|
237
251
|
execution,
|
|
238
|
-
|
|
252
|
+
TEST_CALLER,
|
|
239
253
|
);
|
|
240
254
|
expect(provisioner.ensured).toEqual([
|
|
241
255
|
{
|
|
@@ -244,11 +258,32 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
244
258
|
env: {
|
|
245
259
|
taskQueue: `session:${sessionId}`,
|
|
246
260
|
stigmerToken: `tok-${execution.metadata?.id ?? ""}`,
|
|
261
|
+
callerClass: "user",
|
|
247
262
|
},
|
|
248
263
|
},
|
|
249
264
|
]);
|
|
250
265
|
});
|
|
251
266
|
|
|
267
|
+
it("hands the caller's class to the driver unchanged and the caller's id to the mint", async () => {
|
|
268
|
+
const provisioner = fakeProvisioner();
|
|
269
|
+
const credentials = capabilityCredentials();
|
|
270
|
+
const { execution } = await seed(ExecutionTarget.CLOUD);
|
|
271
|
+
await ensureSessionSandboxForExecution(
|
|
272
|
+
{
|
|
273
|
+
store,
|
|
274
|
+
logger: silentLogger,
|
|
275
|
+
lane: lane(provisioner, credentials),
|
|
276
|
+
temporalConfig: sessionRoutingCloudDefault,
|
|
277
|
+
},
|
|
278
|
+
execution,
|
|
279
|
+
GUEST_CALLER,
|
|
280
|
+
);
|
|
281
|
+
expect(provisioner.ensured[0]?.env.callerClass).toBe("guest");
|
|
282
|
+
expect(credentials.minted[0]?.callerIdentityId).toBe(
|
|
283
|
+
GUEST_CALLER.identityId,
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
252
287
|
it("delegates the mint to the capability provider with the full provisioning context (C4)", async () => {
|
|
253
288
|
const provisioner = fakeProvisioner();
|
|
254
289
|
const credentials = capabilityCredentials();
|
|
@@ -277,7 +312,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
277
312
|
temporalConfig: sessionRoutingCloudDefault,
|
|
278
313
|
},
|
|
279
314
|
execution,
|
|
280
|
-
|
|
315
|
+
TEST_CALLER,
|
|
281
316
|
);
|
|
282
317
|
|
|
283
318
|
expect(credentials.minted).toEqual([
|
|
@@ -303,7 +338,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
303
338
|
temporalConfig: sessionRoutingCloudDefault,
|
|
304
339
|
},
|
|
305
340
|
execution,
|
|
306
|
-
|
|
341
|
+
TEST_CALLER,
|
|
307
342
|
);
|
|
308
343
|
expect(provisioner.ensured).toEqual([]);
|
|
309
344
|
});
|
|
@@ -327,7 +362,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
327
362
|
temporalConfig: sessionRoutingCloudDefault,
|
|
328
363
|
},
|
|
329
364
|
execution,
|
|
330
|
-
|
|
365
|
+
TEST_CALLER,
|
|
331
366
|
);
|
|
332
367
|
});
|
|
333
368
|
|
|
@@ -345,7 +380,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
345
380
|
temporalConfig: sessionRoutingCloudDefault,
|
|
346
381
|
},
|
|
347
382
|
execution,
|
|
348
|
-
|
|
383
|
+
TEST_CALLER,
|
|
349
384
|
);
|
|
350
385
|
expect(provisioner.ensured).toEqual([]);
|
|
351
386
|
});
|
|
@@ -366,7 +401,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
366
401
|
),
|
|
367
402
|
},
|
|
368
403
|
execution,
|
|
369
|
-
|
|
404
|
+
TEST_CALLER,
|
|
370
405
|
);
|
|
371
406
|
expect(provisioner.ensured).toEqual([]);
|
|
372
407
|
});
|
|
@@ -382,7 +417,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
382
417
|
temporalConfig: sessionRoutingCloudDefault,
|
|
383
418
|
},
|
|
384
419
|
execution,
|
|
385
|
-
|
|
420
|
+
TEST_CALLER,
|
|
386
421
|
);
|
|
387
422
|
expect(provisioner.ensured[0]?.env.stigmerToken).toBe("");
|
|
388
423
|
});
|
|
@@ -401,7 +436,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
401
436
|
temporalConfig: sessionRoutingCloudDefault,
|
|
402
437
|
},
|
|
403
438
|
execution,
|
|
404
|
-
|
|
439
|
+
TEST_CALLER,
|
|
405
440
|
);
|
|
406
441
|
const stamped = await store.getResource(
|
|
407
442
|
ApiResourceKind.agent_execution,
|
|
@@ -439,7 +474,7 @@ describe("the session lane (ensureSessionSandboxForExecution)", () => {
|
|
|
439
474
|
temporalConfig: sessionRoutingCloudDefault,
|
|
440
475
|
},
|
|
441
476
|
execution,
|
|
442
|
-
|
|
477
|
+
TEST_CALLER,
|
|
443
478
|
);
|
|
444
479
|
const after = await store.getResource(
|
|
445
480
|
ApiResourceKind.agent_execution,
|
|
@@ -467,17 +502,35 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
467
502
|
temporalConfig: executionRoutingConfig,
|
|
468
503
|
},
|
|
469
504
|
workflowExecution(ExecutionTarget.CLOUD),
|
|
470
|
-
|
|
505
|
+
TEST_CALLER,
|
|
471
506
|
);
|
|
472
507
|
expect(provisioner.ensured).toEqual([
|
|
473
508
|
{
|
|
474
509
|
scope: "workflow",
|
|
475
510
|
id: "wfx_sbx_1",
|
|
476
|
-
env: {
|
|
511
|
+
env: {
|
|
512
|
+
taskQueue: "wfexec:wfx_sbx_1",
|
|
513
|
+
stigmerToken: "tok-wfx_sbx_1",
|
|
514
|
+
callerClass: "user",
|
|
515
|
+
},
|
|
477
516
|
},
|
|
478
517
|
]);
|
|
479
518
|
});
|
|
480
519
|
|
|
520
|
+
it("hands the caller's class to the driver unchanged on the workflow lane", async () => {
|
|
521
|
+
const provisioner = fakeProvisioner();
|
|
522
|
+
await ensureWorkflowSandboxForExecution(
|
|
523
|
+
{
|
|
524
|
+
logger: silentLogger,
|
|
525
|
+
lane: lane(provisioner),
|
|
526
|
+
temporalConfig: executionRoutingConfig,
|
|
527
|
+
},
|
|
528
|
+
workflowExecution(ExecutionTarget.CLOUD),
|
|
529
|
+
GUEST_CALLER,
|
|
530
|
+
);
|
|
531
|
+
expect(provisioner.ensured[0]?.env.callerClass).toBe("guest");
|
|
532
|
+
});
|
|
533
|
+
|
|
481
534
|
it("delegates the mint to the capability provider on the workflow scope (C4)", async () => {
|
|
482
535
|
const provisioner = fakeProvisioner();
|
|
483
536
|
const credentials = capabilityCredentials();
|
|
@@ -491,7 +544,7 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
491
544
|
metadata: { id: "wfx_sbx_cap", name: "wfx_sbx_cap", org: "org-test" },
|
|
492
545
|
spec: { executionTarget: ExecutionTarget.CLOUD },
|
|
493
546
|
}),
|
|
494
|
-
|
|
547
|
+
TEST_CALLER,
|
|
495
548
|
);
|
|
496
549
|
expect(credentials.minted).toEqual([
|
|
497
550
|
{
|
|
@@ -514,7 +567,7 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
514
567
|
temporalConfig: executionRoutingConfig,
|
|
515
568
|
},
|
|
516
569
|
workflowExecution(ExecutionTarget.LOCAL),
|
|
517
|
-
|
|
570
|
+
TEST_CALLER,
|
|
518
571
|
);
|
|
519
572
|
await ensureWorkflowSandboxForExecution(
|
|
520
573
|
{
|
|
@@ -528,7 +581,7 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
528
581
|
),
|
|
529
582
|
},
|
|
530
583
|
workflowExecution(ExecutionTarget.CLOUD),
|
|
531
|
-
|
|
584
|
+
TEST_CALLER,
|
|
532
585
|
);
|
|
533
586
|
expect(provisioner.ensured).toEqual([]);
|
|
534
587
|
});
|
|
@@ -545,7 +598,7 @@ describe("the workflow lane (ensureWorkflowSandboxForExecution)", () => {
|
|
|
545
598
|
temporalConfig: executionRoutingConfig,
|
|
546
599
|
},
|
|
547
600
|
workflowExecution(ExecutionTarget.CLOUD),
|
|
548
|
-
|
|
601
|
+
TEST_CALLER,
|
|
549
602
|
);
|
|
550
603
|
expect.unreachable("the workflow lane must throw on failure");
|
|
551
604
|
} catch (error) {
|
|
@@ -85,7 +85,14 @@ const RUNNER_RESOURCES = {
|
|
|
85
85
|
limits: { cpu: "2", memory: "2Gi" },
|
|
86
86
|
} as const;
|
|
87
87
|
|
|
88
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* The persistent-workspace scopes (SandboxScope.persistentWorkspace).
|
|
90
|
+
* Durability here is decided by SCOPE alone: this driver reads neither the
|
|
91
|
+
* environment's caller class nor any per-session policy, so every session
|
|
92
|
+
* keeps its PVC whoever asked. A driver that gives some caller classes a
|
|
93
|
+
* workspace that dies with the conversation is a composition's choice
|
|
94
|
+
* built on SandboxEnvironment.callerClass, not a built-in behaviour.
|
|
95
|
+
*/
|
|
89
96
|
const PERSISTENT_SCOPES: ReadonlySet<SandboxScope> = new Set([
|
|
90
97
|
"session",
|
|
91
98
|
"workflow",
|
|
@@ -39,13 +39,16 @@
|
|
|
39
39
|
* runner process polls the queues.
|
|
40
40
|
*/
|
|
41
41
|
import type { Logger } from "../boot/logger.js";
|
|
42
|
+
import type { CallerClass } from "../extensions/identity.js";
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
-
* sandboxed runner must poll
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* What is TRUE about one provisioning request, handed to a driver: which
|
|
46
|
+
* queue the sandboxed runner must poll, the credential it authenticates
|
|
47
|
+
* with, and the class of the caller whose request is being served. Facts
|
|
48
|
+
* only — what a driver DOES with them (the workspace's durability, the
|
|
49
|
+
* pod's shape, whether a warm pool is consulted) is driver policy and
|
|
50
|
+
* never travels here. Endpoints, images and resource shapes are driver
|
|
51
|
+
* configuration, not per-sandbox state.
|
|
49
52
|
*/
|
|
50
53
|
export interface SandboxEnvironment {
|
|
51
54
|
/** The Temporal task queue the sandboxed runner polls (session:{id} / wfexec:{id}). */
|
|
@@ -56,6 +59,18 @@ export interface SandboxEnvironment {
|
|
|
56
59
|
* and EC decrypt falls back to redaction, the oss#535 posture.
|
|
57
60
|
*/
|
|
58
61
|
readonly stigmerToken: string;
|
|
62
|
+
/**
|
|
63
|
+
* The class of the caller whose request provisioned this sandbox — the
|
|
64
|
+
* identity interceptor's word for it (`user`, `internal`, a composed
|
|
65
|
+
* verifier's own lane such as a guest or a scheduled fire). A driver
|
|
66
|
+
* that gives some session classes a workspace that does not outlive the
|
|
67
|
+
* conversation reads it here; the built-in drivers (`local-process`,
|
|
68
|
+
* `docker`, `kubernetes`) ignore it and keep every session persistent.
|
|
69
|
+
* Required rather than optional so a construction site that forgets it
|
|
70
|
+
* fails to compile instead of silently reading as a persistent user
|
|
71
|
+
* session to every driver.
|
|
72
|
+
*/
|
|
73
|
+
readonly callerClass: CallerClass;
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
/** One sandbox's observed live state (the Q5 probe result). */
|
package/src/sandbox/steps.ts
CHANGED
|
@@ -50,6 +50,7 @@ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/
|
|
|
50
50
|
|
|
51
51
|
import type { Logger } from "../boot/logger.js";
|
|
52
52
|
import type { AgentExecutionTemporalConfig } from "../domain/agentexecution/temporal/config.js";
|
|
53
|
+
import type { CallerIdentity } from "../extensions/identity.js";
|
|
53
54
|
import {
|
|
54
55
|
WORKFLOW_ROUTING_EXECUTION,
|
|
55
56
|
type WorkflowExecutionTemporalConfig,
|
|
@@ -72,6 +73,17 @@ import { mintSandboxToken, type SandboxLane } from "./lane.js";
|
|
|
72
73
|
export const SANDBOX_PROVISIONING_FAILED_PREFIX =
|
|
73
74
|
"Sandbox provisioning failed: ";
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The two facts about the requesting caller a sandbox ensure carries, cut
|
|
78
|
+
* from the chain's CallerIdentity (the domain/identityaccount/actor.ts
|
|
79
|
+
* shape): the id rides into the credential mint, so the session token is
|
|
80
|
+
* minted FOR the caller; the class rides onto the driver's environment,
|
|
81
|
+
* so a driver can decide the workspace's durability by who asked. A step
|
|
82
|
+
* hands `ctx.callerIdentity` in whole; the pick keeps a test's fixture to
|
|
83
|
+
* the two fields the body reads.
|
|
84
|
+
*/
|
|
85
|
+
export type SandboxCaller = Pick<CallerIdentity, "identityId" | "callerClass">;
|
|
86
|
+
|
|
75
87
|
type AgentExecutionCreateDesc = typeof AgentExecutionSchema;
|
|
76
88
|
|
|
77
89
|
export interface EnsureSessionSandboxDeps {
|
|
@@ -101,7 +113,7 @@ export function newEnsureSessionSandboxStep(
|
|
|
101
113
|
await ensureSessionSandboxForExecution(
|
|
102
114
|
deps,
|
|
103
115
|
ctx.newState,
|
|
104
|
-
ctx.callerIdentity
|
|
116
|
+
ctx.callerIdentity,
|
|
105
117
|
);
|
|
106
118
|
},
|
|
107
119
|
};
|
|
@@ -114,15 +126,18 @@ export function newEnsureSessionSandboxStep(
|
|
|
114
126
|
* chains' differing context shapes (newState vs loaded execution) meet
|
|
115
127
|
* at this seam instead.
|
|
116
128
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
129
|
+
* The caller splits two ways (SandboxCaller): `identityId` rides into the
|
|
130
|
+
* credential mint (lane.ts) — the Java ensure step mints session tokens
|
|
131
|
+
* FOR the requesting caller, so the capability mint needs the identity
|
|
132
|
+
* the OSS execution-scoped mint never did — and `callerClass` rides onto
|
|
133
|
+
* the driver's environment as the one request fact a driver may decide
|
|
134
|
+
* durability by. The chain always has a caller: the in-process transport
|
|
135
|
+
* mints `internal` for the server's own calls.
|
|
121
136
|
*/
|
|
122
137
|
export async function ensureSessionSandboxForExecution(
|
|
123
138
|
deps: EnsureSessionSandboxDeps,
|
|
124
139
|
execution: AgentExecution,
|
|
125
|
-
|
|
140
|
+
caller: SandboxCaller,
|
|
126
141
|
): Promise<void> {
|
|
127
142
|
if (!deps.lane.enabled) {
|
|
128
143
|
return;
|
|
@@ -167,10 +182,11 @@ export async function ensureSessionSandboxForExecution(
|
|
|
167
182
|
sessionId,
|
|
168
183
|
executionId,
|
|
169
184
|
org: execution.metadata?.org ?? "",
|
|
170
|
-
callerIdentityId,
|
|
185
|
+
callerIdentityId: caller.identityId,
|
|
171
186
|
},
|
|
172
187
|
deps.logger,
|
|
173
188
|
),
|
|
189
|
+
callerClass: caller.callerClass,
|
|
174
190
|
});
|
|
175
191
|
} catch (error) {
|
|
176
192
|
// Non-critical: never fail the launch — but never silent either
|
|
@@ -247,7 +263,7 @@ export function newEnsureWorkflowSandboxStep(
|
|
|
247
263
|
await ensureWorkflowSandboxForExecution(
|
|
248
264
|
deps,
|
|
249
265
|
ctx.newState,
|
|
250
|
-
ctx.callerIdentity
|
|
266
|
+
ctx.callerIdentity,
|
|
251
267
|
);
|
|
252
268
|
},
|
|
253
269
|
};
|
|
@@ -258,13 +274,13 @@ export function newEnsureWorkflowSandboxStep(
|
|
|
258
274
|
* re-provision (lifecycle.ts — the previous sandbox was deprovisioned at
|
|
259
275
|
* the terminal FAILED, so a recovered execution needs a fresh one before
|
|
260
276
|
* its fresh workflow starts). Throws Unavailable on provisioning failure
|
|
261
|
-
* — the critical posture in both chains.
|
|
262
|
-
* the
|
|
277
|
+
* — the critical posture in both chains. The caller splits exactly as on
|
|
278
|
+
* the session body: id to the mint, class to the environment.
|
|
263
279
|
*/
|
|
264
280
|
export async function ensureWorkflowSandboxForExecution(
|
|
265
281
|
deps: EnsureWorkflowSandboxDeps,
|
|
266
282
|
execution: WorkflowExecution,
|
|
267
|
-
|
|
283
|
+
caller: SandboxCaller,
|
|
268
284
|
): Promise<void> {
|
|
269
285
|
if (!deps.lane.enabled) {
|
|
270
286
|
return;
|
|
@@ -296,10 +312,11 @@ export async function ensureWorkflowSandboxForExecution(
|
|
|
296
312
|
sessionId: "",
|
|
297
313
|
executionId,
|
|
298
314
|
org: execution.metadata?.org ?? "",
|
|
299
|
-
callerIdentityId,
|
|
315
|
+
callerIdentityId: caller.identityId,
|
|
300
316
|
},
|
|
301
317
|
deps.logger,
|
|
302
318
|
),
|
|
319
|
+
callerClass: caller.callerClass,
|
|
303
320
|
});
|
|
304
321
|
} catch (error) {
|
|
305
322
|
deps.logger.error(
|
|
@@ -91,24 +91,43 @@ describe.skipIf(testDatabaseAdminUrl() === undefined)(
|
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
it("a v1 database resumes the chain on reopen: the v2/v3 indexes arrive", async () => {
|
|
94
|
+
it("a v1 database resumes the chain on reopen: the v2/v3 indexes arrive and v4 removes the Project rows", async () => {
|
|
95
95
|
const store = await PostgresStore.open(db.databaseUrl);
|
|
96
96
|
await store.close();
|
|
97
97
|
|
|
98
|
-
// Rewind to a v1 database: drop exactly what v2+
|
|
99
|
-
// version rows — the state any pre-v2 deployment is actually in
|
|
98
|
+
// Rewind to a v1 database: drop exactly what v2+v4 created and their
|
|
99
|
+
// version rows — the state any pre-v2 deployment is actually in —
|
|
100
|
+
// and seed one row of the Project kind an earlier release wrote
|
|
101
|
+
// (the kind column holds the enum NAME) beside a row v4 must keep.
|
|
100
102
|
const client = new pg.Client({ connectionString: db.databaseUrl });
|
|
101
103
|
await client.connect();
|
|
102
104
|
try {
|
|
103
105
|
await client.query(`DROP INDEX idx_wfee_created_at`);
|
|
104
106
|
await client.query(`DROP INDEX idx_oauth_grant_resource`);
|
|
105
107
|
await client.query(
|
|
106
|
-
`DELETE FROM schema_version WHERE version IN (2, 3)`,
|
|
108
|
+
`DELETE FROM schema_version WHERE version IN (2, 3, 4)`,
|
|
109
|
+
);
|
|
110
|
+
await client.query(
|
|
111
|
+
`INSERT INTO resources (kind, id, data) VALUES ('project', 'prj_seeded', '\\x00'), ('organization', 'acme', '\\x00')`,
|
|
112
|
+
);
|
|
113
|
+
await client.query(
|
|
114
|
+
`INSERT INTO resource_audit (kind, resource_id, data, version_hash, tag) VALUES ('project', 'prj_seeded', '\\x00', '', '')`,
|
|
107
115
|
);
|
|
108
116
|
|
|
109
117
|
const reopened = await PostgresStore.open(db.databaseUrl);
|
|
110
118
|
await reopened.close();
|
|
111
119
|
|
|
120
|
+
const remaining = await client.query(
|
|
121
|
+
`SELECT kind, id FROM resources ORDER BY kind`,
|
|
122
|
+
);
|
|
123
|
+
expect(remaining.rows, "v4 removed the Project row and kept the rest").toEqual([
|
|
124
|
+
{ kind: "organization", id: "acme" },
|
|
125
|
+
]);
|
|
126
|
+
const projectAudit = await client.query(
|
|
127
|
+
`SELECT count(*)::int AS n FROM resource_audit WHERE kind = 'project'`,
|
|
128
|
+
);
|
|
129
|
+
expect((projectAudit.rows[0] as { n: number }).n).toBe(0);
|
|
130
|
+
|
|
112
131
|
const sweepIndex = await client.query(
|
|
113
132
|
`SELECT indexname FROM pg_indexes
|
|
114
133
|
WHERE tablename = 'workflow_execution_events'
|