@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.
Files changed (100) hide show
  1. package/dist/authorization/model/index.d.ts +1 -1
  2. package/dist/authorization/model/index.d.ts.map +1 -1
  3. package/dist/authorization/model/index.js +0 -2
  4. package/dist/authorization/model/index.js.map +1 -1
  5. package/dist/authorization/model/organization.d.ts.map +1 -1
  6. package/dist/authorization/model/organization.js +0 -1
  7. package/dist/authorization/model/organization.js.map +1 -1
  8. package/dist/boot/compose.d.ts.map +1 -1
  9. package/dist/boot/compose.js +2 -15
  10. package/dist/boot/compose.js.map +1 -1
  11. package/dist/boot/inprocess.d.ts +2 -13
  12. package/dist/boot/inprocess.d.ts.map +1 -1
  13. package/dist/boot/inprocess.js +6 -16
  14. package/dist/boot/inprocess.js.map +1 -1
  15. package/dist/domain/agentexecution/lifecycle.js +1 -1
  16. package/dist/domain/agentexecution/lifecycle.js.map +1 -1
  17. package/dist/domain/mcpserver/initiate-oauth-connect.d.ts.map +1 -1
  18. package/dist/domain/mcpserver/initiate-oauth-connect.js +22 -1
  19. package/dist/domain/mcpserver/initiate-oauth-connect.js.map +1 -1
  20. package/dist/domain/workflow/registry/data/task-kind-registry.json +0 -1
  21. package/dist/domain/workflowexecution/lifecycle.js +1 -1
  22. package/dist/domain/workflowexecution/lifecycle.js.map +1 -1
  23. package/dist/pipeline/interceptors/auth.d.ts +1 -1
  24. package/dist/pipeline/interceptors/auth.js +1 -1
  25. package/dist/query/search/registry.d.ts +3 -5
  26. package/dist/query/search/registry.d.ts.map +1 -1
  27. package/dist/query/search/registry.js +3 -7
  28. package/dist/query/search/registry.js.map +1 -1
  29. package/dist/sandbox/kubernetes.d.ts.map +1 -1
  30. package/dist/sandbox/kubernetes.js +8 -1
  31. package/dist/sandbox/kubernetes.js.map +1 -1
  32. package/dist/sandbox/provisioner.d.ts +20 -5
  33. package/dist/sandbox/provisioner.d.ts.map +1 -1
  34. package/dist/sandbox/provisioner.js.map +1 -1
  35. package/dist/sandbox/steps.d.ts +22 -8
  36. package/dist/sandbox/steps.d.ts.map +1 -1
  37. package/dist/sandbox/steps.js +17 -12
  38. package/dist/sandbox/steps.js.map +1 -1
  39. package/dist/store/postgres/migrations.d.ts +3 -1
  40. package/dist/store/postgres/migrations.d.ts.map +1 -1
  41. package/dist/store/postgres/migrations.js +19 -1
  42. package/dist/store/postgres/migrations.js.map +1 -1
  43. package/dist/store/sqlite/migrations.d.ts +3 -1
  44. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  45. package/dist/store/sqlite/migrations.js +22 -1
  46. package/dist/store/sqlite/migrations.js.map +1 -1
  47. package/package.json +6 -6
  48. package/src/authorization/__tests__/derived-tuples.test.ts +1 -1
  49. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +5 -5
  50. package/src/authorization/__tests__/wire-permissions.test.ts +0 -1
  51. package/src/authorization/model/__tests__/registry.test.ts +0 -9
  52. package/src/authorization/model/index.ts +1 -3
  53. package/src/authorization/model/organization.ts +0 -1
  54. package/src/boot/compose.ts +2 -15
  55. package/src/boot/inprocess.ts +8 -29
  56. package/src/domain/agentexecution/lifecycle.ts +1 -1
  57. package/src/domain/iampolicy/__tests__/role-lifecycle.test.ts +0 -1
  58. package/src/domain/mcpserver/__tests__/oauth-handshake.test.ts +1 -1
  59. package/src/domain/mcpserver/initiate-oauth-connect.ts +21 -1
  60. package/src/domain/workflow/registry/data/task-kind-registry.json +0 -1
  61. package/src/domain/workflowexecution/lifecycle.ts +1 -1
  62. package/src/pipeline/__tests__/router-transport.test.ts +1 -1
  63. package/src/pipeline/interceptors/auth.ts +1 -1
  64. package/src/query/__tests__/query-services.test.ts +16 -18
  65. package/src/query/search/__tests__/criteria.test.ts +3 -6
  66. package/src/query/search/__tests__/query-store.test.ts +21 -21
  67. package/src/query/search/registry.ts +3 -7
  68. package/src/sandbox/__tests__/docker.integration.test.ts +2 -0
  69. package/src/sandbox/__tests__/kubernetes.test.ts +10 -2
  70. package/src/sandbox/__tests__/local-process.integration.test.ts +3 -0
  71. package/src/sandbox/__tests__/steps.test.ts +72 -19
  72. package/src/sandbox/kubernetes.ts +8 -1
  73. package/src/sandbox/provisioner.ts +20 -5
  74. package/src/sandbox/steps.ts +29 -12
  75. package/src/store/postgres/__tests__/migrations.test.ts +23 -4
  76. package/src/store/postgres/migrations.ts +20 -1
  77. package/src/store/sqlite/__tests__/migrations.test.ts +26 -2
  78. package/src/store/sqlite/migrations.ts +23 -1
  79. package/dist/authorization/model/project.d.ts +0 -2
  80. package/dist/authorization/model/project.d.ts.map +0 -1
  81. package/dist/authorization/model/project.js +0 -32
  82. package/dist/authorization/model/project.js.map +0 -1
  83. package/dist/domain/project/controller.d.ts +0 -48
  84. package/dist/domain/project/controller.d.ts.map +0 -1
  85. package/dist/domain/project/controller.js +0 -192
  86. package/dist/domain/project/controller.js.map +0 -1
  87. package/dist/domain/project/reconcile.d.ts +0 -77
  88. package/dist/domain/project/reconcile.d.ts.map +0 -1
  89. package/dist/domain/project/reconcile.js +0 -179
  90. package/dist/domain/project/reconcile.js.map +0 -1
  91. package/dist/domain/project/search-extractor.d.ts +0 -3
  92. package/dist/domain/project/search-extractor.d.ts.map +0 -1
  93. package/dist/domain/project/search-extractor.js +0 -43
  94. package/dist/domain/project/search-extractor.js.map +0 -1
  95. package/src/authorization/model/project.ts +0 -47
  96. package/src/domain/project/__tests__/project.composed.test.ts +0 -381
  97. package/src/domain/project/__tests__/reconcile.test.ts +0 -447
  98. package/src/domain/project/controller.ts +0 -387
  99. package/src/domain/project/reconcile.ts +0 -296
  100. 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 its project rows. The escaping and
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 project rows on an ADOPTED Go-created database (DD-D)", async () => {
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→v7 — real adoption), then given a project row the way a Go
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 project = create(ProjectSchema, {
436
+ const skill = create(SkillSchema, {
437
437
  metadata: {
438
- id: "prj_1",
438
+ id: "skl_1",
439
439
  name: "billing",
440
440
  slug: "billing",
441
441
  org: "acme",
442
442
  },
443
- spec: { description: "the billing project" },
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.project,
450
- "prj_1",
451
- ProjectSchema,
452
- project,
449
+ ApiResourceKind.skill,
450
+ "skl_1",
451
+ SkillSchema,
452
+ skill,
453
453
  );
454
- // Boot state: the TS server wipes and rebuilds — a 12-kind registry
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.project], org: "acme" }),
464
+ criteria({ kinds: [ApiResourceKind.skill], org: "acme" }),
465
465
  );
466
466
  expect(result.totalCount).toBe(1);
467
- expect(result.results[0]?.id).toBe("prj_1");
468
- expect(result.results[0]?.description).toBe("the billing project");
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.project,
474
- "prj_1",
475
- ProjectSchema,
473
+ ApiResourceKind.skill,
474
+ "skl_1",
475
+ SkillSchema,
476
476
  );
477
- expect(toBinary(ProjectSchema, raw)).toEqual(
478
- toBinary(ProjectSchema, project),
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
- * The project extractor is registered here by THIS sub-project (#14)
16
- * although the project DOMAIN ports with #16 — boot RebuildIndex re-indexes
17
- * every registered kind from the resources table, and an adopted Go
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 = { taskQueue: "session:ses_1", stigmerToken: "tok-1" };
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 = { taskQueue: "session:ses_1", stigmerToken: "" };
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 identity the ensure bodies thread into the credential mint
60
- * (C4): the OSS execution-scoped mint ignores it, so these tests pass a
61
- * fixed value and the token assertions stay binding-shaped.
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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: { taskQueue: "wfexec:wfx_sbx_1", stigmerToken: "tok-wfx_sbx_1" },
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- TEST_CALLER_IDENTITY_ID,
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
- /** The persistent-workspace scopes (SandboxScope.persistentWorkspace). */
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
- * The runtime handed to a provisioner for one sandbox: which queue the
45
- * sandboxed runner must poll and the credential it authenticates with.
46
- * Deliberately minimal (the Java SandboxEnvironment carries the same two
47
- * load-bearing fields plus cloud-only accounting) — endpoints, images,
48
- * and resource shapes are DRIVER configuration, not per-sandbox state.
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). */
@@ -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.identityId,
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
- * `callerIdentityId` rides into the credential mint (lane.ts): the Java
118
- * ensure step mints session tokens FOR the requesting caller, so the
119
- * capability mint needs the identity the OSS execution-scoped mint never
120
- * did. Empty when the invoking site has no caller.
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
- callerIdentityId: string,
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.identityId,
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. `callerIdentityId` rides into
262
- * the credential mint exactly as on the session body.
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
- callerIdentityId: string,
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+v3 created and their
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'