@stigmer/server 3.38.2 → 3.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/domain/agent/controller.d.ts +2 -2
  2. package/dist/domain/agent/controller.d.ts.map +1 -1
  3. package/dist/domain/agent/controller.js +7 -3
  4. package/dist/domain/agent/controller.js.map +1 -1
  5. package/dist/domain/agentchannel/controller.d.ts +2 -2
  6. package/dist/domain/agentchannel/controller.d.ts.map +1 -1
  7. package/dist/domain/agentchannel/controller.js +10 -5
  8. package/dist/domain/agentchannel/controller.js.map +1 -1
  9. package/dist/domain/agentexecution/controller.js +7 -3
  10. package/dist/domain/agentexecution/controller.js.map +1 -1
  11. package/dist/domain/agentexecution/update-status.d.ts +9 -2
  12. package/dist/domain/agentexecution/update-status.d.ts.map +1 -1
  13. package/dist/domain/agentexecution/update-status.js +28 -5
  14. package/dist/domain/agentexecution/update-status.js.map +1 -1
  15. package/dist/domain/agentexecution/validate-thinking-mode.d.ts +31 -21
  16. package/dist/domain/agentexecution/validate-thinking-mode.d.ts.map +1 -1
  17. package/dist/domain/agentexecution/validate-thinking-mode.js +61 -18
  18. package/dist/domain/agentexecution/validate-thinking-mode.js.map +1 -1
  19. package/dist/domain/agentinstance/controller.d.ts +2 -2
  20. package/dist/domain/agentinstance/controller.d.ts.map +1 -1
  21. package/dist/domain/agentinstance/controller.js +7 -3
  22. package/dist/domain/agentinstance/controller.js.map +1 -1
  23. package/dist/domain/agentshare/controller.d.ts +2 -2
  24. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  25. package/dist/domain/agentshare/controller.js +7 -3
  26. package/dist/domain/agentshare/controller.js.map +1 -1
  27. package/dist/domain/artifact/controller.d.ts.map +1 -1
  28. package/dist/domain/artifact/controller.js +9 -6
  29. package/dist/domain/artifact/controller.js.map +1 -1
  30. package/dist/domain/environment/controller.d.ts +2 -2
  31. package/dist/domain/environment/controller.js +7 -3
  32. package/dist/domain/environment/controller.js.map +1 -1
  33. package/dist/domain/environment/steps.d.ts +2 -1
  34. package/dist/domain/environment/steps.d.ts.map +1 -1
  35. package/dist/domain/environment/steps.js +10 -7
  36. package/dist/domain/environment/steps.js.map +1 -1
  37. package/dist/domain/organization/controller.d.ts.map +1 -1
  38. package/dist/domain/organization/controller.js +33 -14
  39. package/dist/domain/organization/controller.js.map +1 -1
  40. package/dist/domain/organization/slug-ledger.d.ts +86 -0
  41. package/dist/domain/organization/slug-ledger.d.ts.map +1 -0
  42. package/dist/domain/organization/slug-ledger.js +128 -0
  43. package/dist/domain/organization/slug-ledger.js.map +1 -0
  44. package/dist/domain/organization/steps.d.ts +12 -3
  45. package/dist/domain/organization/steps.d.ts.map +1 -1
  46. package/dist/domain/organization/steps.js +28 -6
  47. package/dist/domain/organization/steps.js.map +1 -1
  48. package/dist/domain/workflow/registry/data/model-registry.json +23 -14
  49. package/dist/domain/workflow/registry/model-registry-store.d.ts +14 -3
  50. package/dist/domain/workflow/registry/model-registry-store.d.ts.map +1 -1
  51. package/dist/domain/workflow/registry/model-registry-store.js +14 -3
  52. package/dist/domain/workflow/registry/model-registry-store.js.map +1 -1
  53. package/dist/extensions/gate-slots.d.ts +7 -5
  54. package/dist/extensions/gate-slots.d.ts.map +1 -1
  55. package/dist/extensions/gate-slots.js.map +1 -1
  56. package/dist/index.d.ts +3 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +2 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/pipeline/errors.d.ts +8 -0
  61. package/dist/pipeline/errors.d.ts.map +1 -1
  62. package/dist/pipeline/errors.js +19 -0
  63. package/dist/pipeline/errors.js.map +1 -1
  64. package/dist/store/interface.d.ts +67 -0
  65. package/dist/store/interface.d.ts.map +1 -1
  66. package/dist/store/interface.js.map +1 -1
  67. package/dist/store/organization-slug-history.d.ts +71 -0
  68. package/dist/store/organization-slug-history.d.ts.map +1 -0
  69. package/dist/store/organization-slug-history.js +88 -0
  70. package/dist/store/organization-slug-history.js.map +1 -0
  71. package/dist/store/postgres/migrations.d.ts +3 -1
  72. package/dist/store/postgres/migrations.d.ts.map +1 -1
  73. package/dist/store/postgres/migrations.js +69 -1
  74. package/dist/store/postgres/migrations.js.map +1 -1
  75. package/dist/store/postgres/store.d.ts +2 -1
  76. package/dist/store/postgres/store.d.ts.map +1 -1
  77. package/dist/store/postgres/store.js +49 -0
  78. package/dist/store/postgres/store.js.map +1 -1
  79. package/dist/store/sqlite/migrations.d.ts +3 -1
  80. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  81. package/dist/store/sqlite/migrations.js +54 -1
  82. package/dist/store/sqlite/migrations.js.map +1 -1
  83. package/dist/store/sqlite/store.d.ts +2 -1
  84. package/dist/store/sqlite/store.d.ts.map +1 -1
  85. package/dist/store/sqlite/store.js +55 -0
  86. package/dist/store/sqlite/store.js.map +1 -1
  87. package/package.json +6 -6
  88. package/src/domain/agent/__tests__/store-faults.test.ts +105 -0
  89. package/src/domain/agent/controller.ts +9 -5
  90. package/src/domain/agentchannel/__tests__/store-faults.test.ts +126 -0
  91. package/src/domain/agentchannel/controller.ts +12 -7
  92. package/src/domain/agentexecution/__tests__/run-gate.test.ts +42 -1
  93. package/src/domain/agentexecution/__tests__/update-status.test.ts +58 -2
  94. package/src/domain/agentexecution/__tests__/validate-thinking-mode.test.ts +209 -61
  95. package/src/domain/agentexecution/controller.ts +7 -3
  96. package/src/domain/agentexecution/update-status.ts +43 -3
  97. package/src/domain/agentexecution/validate-thinking-mode.ts +102 -47
  98. package/src/domain/agentinstance/__tests__/store-faults.test.ts +110 -0
  99. package/src/domain/agentinstance/controller.ts +9 -5
  100. package/src/domain/agentshare/__tests__/store-faults.test.ts +101 -0
  101. package/src/domain/agentshare/controller.ts +9 -5
  102. package/src/domain/artifact/__tests__/store-faults.test.ts +117 -0
  103. package/src/domain/artifact/controller.ts +12 -6
  104. package/src/domain/environment/__tests__/store-faults.test.ts +150 -0
  105. package/src/domain/environment/controller.ts +8 -5
  106. package/src/domain/environment/steps.ts +10 -7
  107. package/src/domain/organization/__tests__/organization-delete.test.ts +73 -18
  108. package/src/domain/organization/__tests__/organization-slugs.test.ts +386 -0
  109. package/src/domain/organization/controller.ts +36 -14
  110. package/src/domain/organization/slug-ledger.ts +209 -0
  111. package/src/domain/organization/steps.ts +28 -6
  112. package/src/domain/workflow/registry/data/model-registry.json +23 -14
  113. package/src/domain/workflow/registry/model-registry-store.ts +16 -3
  114. package/src/extensions/gate-slots.ts +7 -5
  115. package/src/index.ts +11 -0
  116. package/src/pipeline/__tests__/blind-not-found.test.ts +0 -7
  117. package/src/pipeline/errors.ts +23 -0
  118. package/src/store/__tests__/organization-slug-history.test.ts +109 -0
  119. package/src/store/__tests__/store-contract.ts +86 -1
  120. package/src/store/interface.ts +70 -0
  121. package/src/store/organization-slug-history.ts +159 -0
  122. package/src/store/postgres/__tests__/migrations.test.ts +138 -2
  123. package/src/store/postgres/__tests__/store-contract.test.ts +1 -0
  124. package/src/store/postgres/migrations.ts +84 -1
  125. package/src/store/postgres/store.ts +84 -0
  126. package/src/store/sqlite/__tests__/migrations.test.ts +139 -10
  127. package/src/store/sqlite/migrations.ts +69 -1
  128. package/src/store/sqlite/store.ts +84 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/server",
3
- "version": "3.38.2",
3
+ "version": "3.39.0",
4
4
  "description": "The Stigmer OSS control plane as a composable library: composeServer, the extension registry and the driver seams a composition builds on. The self-contained deployable of the same server is @stigmer/server-slim.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -63,11 +63,11 @@
63
63
  "@connectrpc/connect-node": "^2.0.0",
64
64
  "@kubernetes/client-node": "2.0.0",
65
65
  "@opentelemetry/api": "^1.9.0",
66
- "@stigmer/outbound": "3.38.2",
67
- "@stigmer/plugin-package": "3.38.2",
68
- "@stigmer/protos": "3.38.2",
69
- "@stigmer/temporal-codecs": "3.38.2",
70
- "@stigmer/zip-structure": "3.38.2",
66
+ "@stigmer/outbound": "3.39.0",
67
+ "@stigmer/plugin-package": "3.39.0",
68
+ "@stigmer/protos": "3.39.0",
69
+ "@stigmer/temporal-codecs": "3.39.0",
70
+ "@stigmer/zip-structure": "3.39.0",
71
71
  "@temporalio/activity": "1.16.2",
72
72
  "@temporalio/client": "1.16.2",
73
73
  "@temporalio/common": "1.16.2",
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Pins the store-fault contract of the agent's visibility-update load: a
3
+ * typed ResourceNotFoundError answers NotFound with the domain's pinned copy
4
+ * (`agent not found: <id>`), and any other store failure is an
5
+ * infrastructure fault answered as a sanitized Internal, never a NotFound
6
+ * that tells a client the agent does not exist (stigmer/stigmer#1345).
7
+ *
8
+ * The surface is updateVisibility, reached through the registered handler on
9
+ * an in-process router. The composed suites reach only a real store, which
10
+ * cannot fail selectively, so it runs here against a store whose read throws.
11
+ * The default-instance applier is untouchable: a load that fails must stop
12
+ * the call before anything past it runs.
13
+ *
14
+ * Out of scope: the NotFound copy itself (wire contract) and getByAgent's
15
+ * empty-list answer on an agent-read fault in the channel and share domains
16
+ * (stigmer/stigmer#1375).
17
+ */
18
+ import type { Client, ConnectError } from "@connectrpc/connect";
19
+ import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
20
+ import { describe, expect, it } from "vitest";
21
+
22
+ import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
23
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
24
+
25
+ import { createLogger } from "../../../boot/logger.js";
26
+ import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
27
+ import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
28
+ import {
29
+ errorOf,
30
+ failingStore,
31
+ untouchable,
32
+ } from "../../../pipeline/__tests__/support.js";
33
+ import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
34
+ import { ResourceNotFoundError } from "../../../store/interface.js";
35
+ import type { Store } from "../../../store/interface.js";
36
+
37
+ import { registerAgentServices } from "../controller.js";
38
+
39
+ const silentLogger = createLogger({
40
+ level: "error",
41
+ pretty: false,
42
+ write: () => {},
43
+ });
44
+
45
+ const AGENT_ID = "agt_storefault";
46
+
47
+ const NOT_FOUND_COPY = `agent not found: ${AGENT_ID}`;
48
+ const LOAD_FAULT_COPY = "failed to load agent";
49
+
50
+ const MISSING = (): Error => new ResourceNotFoundError(`agent/${AGENT_ID}`);
51
+ const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
52
+
53
+ /**
54
+ * The registered handlers on an in-process router: the verifier chain stamps
55
+ * the trusted-local caller and the apiresource interceptor the kind, then
56
+ * Authorize and ValidateProto run and the load is the first step that reads
57
+ * the store.
58
+ */
59
+ function agentCommand(store: Store): Client<typeof AgentCommandController> {
60
+ const transport = createRouterTransport(
61
+ (router) => {
62
+ registerAgentServices(router, {
63
+ store,
64
+ logger: silentLogger,
65
+ authorizer: newPermissiveSingleTeamAuthorizer(),
66
+ authorizationLifecycle: undefined,
67
+ agentInstanceApplier: untouchable("agentInstanceApplier"),
68
+ });
69
+ },
70
+ {
71
+ router: {
72
+ interceptors: [
73
+ createVerifierChainInterceptor([], [], silentLogger),
74
+ createApiResourceInterceptor(),
75
+ ],
76
+ },
77
+ },
78
+ );
79
+ return createClient(AgentCommandController, transport);
80
+ }
81
+
82
+ describe("updateVisibility — LoadAgentForVisibilityUpdate", () => {
83
+ function surfaceError(store: Store): Promise<ConnectError> {
84
+ return errorOf(() =>
85
+ agentCommand(store).updateVisibility({
86
+ resourceId: AGENT_ID,
87
+ visibility: ApiResourceVisibility.visibility_private,
88
+ }),
89
+ );
90
+ }
91
+
92
+ it("a missing agent answers NotFound with the domain's copy", async () => {
93
+ const error = await surfaceError(failingStore(MISSING()));
94
+
95
+ expect(error.code).toBe(Code.NotFound);
96
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
97
+ });
98
+
99
+ it("any other store failure answers a sanitized Internal", async () => {
100
+ const error = await surfaceError(failingStore(LOCKED()));
101
+
102
+ expect(error.code).toBe(Code.Internal);
103
+ expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
104
+ });
105
+ });
@@ -8,8 +8,8 @@
8
8
  * agent to serve: a session with no agent runs the built-in assistant.
9
9
  *
10
10
  * Pipeline per RPC mirrors the Go step chains character-for-character.
11
- * Proven by agent.conformance.test.ts (CONFORMANCE_TARGET=local) and
12
- * __tests__/agent.test.ts.
11
+ * Proven by agent.conformance.test.ts (CONFORMANCE_TARGET=local),
12
+ * __tests__/agent.test.ts and __tests__/store-faults.test.ts.
13
13
  *
14
14
  * Versus Stigmer Cloud, OSS excludes the Authorize, CreateIamPolicies, and
15
15
  * Publish steps (no multi-tenant auth, IAM/FGA, or event publishing here).
@@ -91,6 +91,7 @@ import {
91
91
  newValidateVisibilityStep,
92
92
  newValidateVisibilityUpdateStep,
93
93
  } from "../../pipeline/steps/validate-visibility.js";
94
+ import { ResourceNotFoundError } from "../../store/interface.js";
94
95
  import type { Store } from "../../store/interface.js";
95
96
  import { agentSearchExtractor } from "./search-extractor.js";
96
97
  import {
@@ -382,7 +383,7 @@ async function updateVisibility(
382
383
  return reqCtx.get(UPDATE_VISIBILITY_AGENT_KEY) as Agent;
383
384
  }
384
385
 
385
- /** Loads the agent by resource_id; ANY load failure → NotFound. */
386
+ /** Loads the agent by resource_id; a missing one answers NotFound, a store fault Internal. */
386
387
  function newLoadAgentForVisibilityUpdateStep(
387
388
  store: Store,
388
389
  ): PipelineStep<UpdateVisibilityDesc> {
@@ -397,8 +398,11 @@ function newLoadAgentForVisibilityUpdateStep(
397
398
  input.resourceId,
398
399
  AgentSchema,
399
400
  );
400
- } catch {
401
- throw notFoundError("agent", input.resourceId);
401
+ } catch (error) {
402
+ if (error instanceof ResourceNotFoundError) {
403
+ throw notFoundError("agent", input.resourceId);
404
+ }
405
+ throw internalError(error, "failed to load agent");
402
406
  }
403
407
  ctx.set(UPDATE_VISIBILITY_AGENT_KEY, agent);
404
408
  },
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Pins the store-fault contract of the install lane's channel load, which
3
+ * initiateInstall and completeInstall share: a typed ResourceNotFoundError
4
+ * answers NotFound with the domain's pinned copy
5
+ * (`AgentChannel not found: <id>`), and any other store failure is an
6
+ * infrastructure fault answered as a sanitized Internal, never a NotFound
7
+ * that tells a client the channel does not exist (stigmer/stigmer#1345).
8
+ *
9
+ * Both surfaces are reached through the registered handler on an in-process
10
+ * router. The composed suites reach only a real store, which cannot fail
11
+ * selectively, so each runs here against a store whose read throws. The
12
+ * channel runtime and the model registry are untouchable: the load comes
13
+ * before authorization and before the runtime, so a load that fails must
14
+ * stop the call before either.
15
+ *
16
+ * Out of scope: the NotFound copy itself (wire contract, pinned by
17
+ * agentchannel.test.ts and the agentchannel conformance suite) and
18
+ * getByAgent's empty-list answer on an agent-read fault
19
+ * (stigmer/stigmer#1375).
20
+ */
21
+ import type { Client, ConnectError } from "@connectrpc/connect";
22
+ import { Code, createClient, createRouterTransport } from "@connectrpc/connect";
23
+ import { describe, expect, it } from "vitest";
24
+
25
+ import { AgentChannelCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/command_pb";
26
+
27
+ import { createLogger } from "../../../boot/logger.js";
28
+ import { createApiResourceInterceptor } from "../../../pipeline/interceptors/apiresource.js";
29
+ import { createVerifierChainInterceptor } from "../../../pipeline/interceptors/auth.js";
30
+ import {
31
+ errorOf,
32
+ failingStore,
33
+ untouchable,
34
+ } from "../../../pipeline/__tests__/support.js";
35
+ import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
36
+ import { ResourceNotFoundError } from "../../../store/interface.js";
37
+ import type { Store } from "../../../store/interface.js";
38
+
39
+ import type { ChannelRuntime } from "../channel-runtime.js";
40
+ import { registerAgentChannelServices } from "../controller.js";
41
+
42
+ const silentLogger = createLogger({
43
+ level: "error",
44
+ pretty: false,
45
+ write: () => {},
46
+ });
47
+
48
+ const CHANNEL_ID = "ach_storefault";
49
+
50
+ const NOT_FOUND_COPY = `AgentChannel not found: ${CHANNEL_ID}`;
51
+ const LOAD_FAULT_COPY = "failed to load agent channel";
52
+
53
+ const MISSING = (): Error =>
54
+ new ResourceNotFoundError(`agent_channel/${CHANNEL_ID}`);
55
+ const LOCKED = (): Error => new Error("SQLITE_BUSY: database is locked");
56
+
57
+ type ChannelCommand = Client<typeof AgentChannelCommandController>;
58
+
59
+ /**
60
+ * The registered handlers on an in-process router: the verifier chain stamps
61
+ * the trusted-local caller and the apiresource interceptor the kind the
62
+ * install load reads; the load is the first thing either handler does.
63
+ */
64
+ function channelCommand(store: Store): ChannelCommand {
65
+ const transport = createRouterTransport(
66
+ (router) => {
67
+ registerAgentChannelServices(router, {
68
+ store,
69
+ logger: silentLogger,
70
+ authorizer: newPermissiveSingleTeamAuthorizer(),
71
+ authorizationLifecycle: undefined,
72
+ modelRegistry: untouchable("modelRegistry"),
73
+ channelRuntime: untouchable<ChannelRuntime>("channelRuntime"),
74
+ listReadScope: undefined,
75
+ });
76
+ },
77
+ {
78
+ router: {
79
+ interceptors: [
80
+ createVerifierChainInterceptor([], [], silentLogger),
81
+ createApiResourceInterceptor(),
82
+ ],
83
+ },
84
+ },
85
+ );
86
+ return createClient(AgentChannelCommandController, transport);
87
+ }
88
+
89
+ /** Each surface with the input its proto requires. */
90
+ const SURFACES: ReadonlyArray<
91
+ readonly [string, (command: ChannelCommand) => Promise<unknown>]
92
+ > = [
93
+ [
94
+ "initiateInstall — the install load",
95
+ (command) => command.initiateInstall({ resourceId: CHANNEL_ID }),
96
+ ],
97
+ [
98
+ "completeInstall — the install load",
99
+ (command) =>
100
+ command.completeInstall({
101
+ resourceId: CHANNEL_ID,
102
+ state: "install-state",
103
+ code: "provider-code",
104
+ }),
105
+ ],
106
+ ];
107
+
108
+ describe.each(SURFACES)("%s", (_name, call) => {
109
+ function surfaceError(store: Store): Promise<ConnectError> {
110
+ return errorOf(() => call(channelCommand(store)));
111
+ }
112
+
113
+ it("a missing channel answers NotFound with the domain's copy", async () => {
114
+ const error = await surfaceError(failingStore(MISSING()));
115
+
116
+ expect(error.code).toBe(Code.NotFound);
117
+ expect(error.rawMessage).toBe(NOT_FOUND_COPY);
118
+ });
119
+
120
+ it("any other store failure answers a sanitized Internal", async () => {
121
+ const error = await surfaceError(failingStore(LOCKED()));
122
+
123
+ expect(error.code).toBe(Code.Internal);
124
+ expect(error.rawMessage).toBe(LOAD_FAULT_COPY);
125
+ });
126
+ });
@@ -23,8 +23,8 @@
23
23
  * (only same-org shares are) — a dangling channel is tolerated because
24
24
  * OSS has no serving runtime and cloud fails closed elsewhere.
25
25
  *
26
- * Proven by agentchannel.conformance.test.ts (CONFORMANCE_TARGET=local)
27
- * and __tests__/agentchannel.test.ts.
26
+ * Proven by agentchannel.conformance.test.ts (CONFORMANCE_TARGET=local),
27
+ * __tests__/agentchannel.test.ts and __tests__/store-faults.test.ts.
28
28
  */
29
29
  import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
30
30
  import { create, fromBinary } from "@bufbuild/protobuf";
@@ -108,6 +108,7 @@ import { newPersistStep } from "../../pipeline/steps/persist.js";
108
108
  import { newResolveSlugStep } from "../../pipeline/steps/slug.js";
109
109
  import { newValidateProtoStep } from "../../pipeline/steps/validation.js";
110
110
  import { newValidateVisibilityStep } from "../../pipeline/steps/validate-visibility.js";
111
+ import { ResourceNotFoundError } from "../../store/interface.js";
111
112
  import type { Store } from "../../store/interface.js";
112
113
  import type { ModelCatalogProvider } from "../workflow/registry/model-catalog-provider.js";
113
114
  import type { ChannelRuntime } from "./channel-runtime.js";
@@ -395,9 +396,10 @@ async function completeInstall(
395
396
  }
396
397
 
397
398
  /**
398
- * Loads the install target — cloud's LoadChannel NOT_FOUND, verbatim.
399
- * Returns the loaded channel so a composed runtime receives it without a
400
- * second load (the load-then-X contract stays OSS-owned).
399
+ * Loads the install target — cloud's LoadChannel NOT_FOUND, verbatim, for
400
+ * a missing channel; a store fault answers Internal. Returns the loaded
401
+ * channel so a composed runtime receives it without a second load (the
402
+ * load-then-X contract stays OSS-owned).
401
403
  */
402
404
  async function loadChannelForInstall(
403
405
  deps: AgentChannelControllerDeps,
@@ -410,8 +412,11 @@ async function loadChannelForInstall(
410
412
  resourceId,
411
413
  AgentChannelSchema,
412
414
  );
413
- } catch {
414
- throw notFoundError("AgentChannel", resourceId);
415
+ } catch (error) {
416
+ if (error instanceof ResourceNotFoundError) {
417
+ throw notFoundError("AgentChannel", resourceId);
418
+ }
419
+ throw internalError(error, "failed to load agent channel");
415
420
  }
416
421
  }
417
422
 
@@ -7,7 +7,10 @@
7
7
  * denial leaves no execution row AND creates no default instance — the
8
8
  * "no side effect precedes the check" property; (3) the gate precedes the
9
9
  * engine gate: an ALLOWED create on this engineless server answers the
10
- * engine's UNAVAILABLE, a DENIED one PERMISSION_DENIED. The authorizer
10
+ * engine's UNAVAILABLE, a DENIED one PERMISSION_DENIED; (4) the
11
+ * thinking-mode validation, which may read the stored session, runs behind
12
+ * the gate (stigmer/stigmer#1280): a DENIED create with an invalid thinking
13
+ * mode answers PERMISSION_DENIED, an ALLOWED one INVALID_ARGUMENT. The authorizer
11
14
  * under test answers only run-gate checks (runGateOnlyAuthorizer), so every
12
15
  * refusal here is the run gate's and nobody else's.
13
16
  */
@@ -25,6 +28,7 @@ import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb"
25
28
  import { AgentExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/command_pb";
26
29
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
27
30
  import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
31
+ import { ThinkingMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
28
32
 
29
33
  import { loadConfig } from "../../../boot/config.js";
30
34
  import { composeServer } from "../../../boot/compose.js";
@@ -134,6 +138,7 @@ function createInput(spec: {
134
138
  agentId?: string;
135
139
  sessionId?: string;
136
140
  sessionSpec?: { agentInstanceId: string };
141
+ executionConfig?: { modelName?: string; thinkingMode?: ThinkingMode };
137
142
  }) {
138
143
  return {
139
144
  apiVersion: API_VERSION,
@@ -242,4 +247,40 @@ describe("agent-execution-create run gate (composed server)", () => {
242
247
  expect(err.rawMessage).toBe(ENGINE_UNAVAILABLE_MESSAGE);
243
248
  expect(gate.runGateChecks.map((c) => c.resourceId)).toEqual([agentId]);
244
249
  });
250
+
251
+ it("thinking-mode validation runs behind the gate: a denied caller learns nothing about the session", async () => {
252
+ decision = { kind: "deny", reason: "" };
253
+ gate.runGateChecks.length = 0;
254
+
255
+ const err = await captureError(() =>
256
+ command.create(
257
+ createInput({
258
+ sessionId: "ses_01someoneelses",
259
+ executionConfig: { modelName: "composer-2.5", thinkingMode: ThinkingMode.ENABLED },
260
+ }),
261
+ ),
262
+ );
263
+
264
+ expect(err.code).toBe(Code.PermissionDenied);
265
+ expect(err.rawMessage).toBe(addExecutionToSessionDeniedMessage("ses_01someoneelses"));
266
+ });
267
+
268
+ it("an allowed create with an invalid thinking mode is refused by the validation, before the engine gate", async () => {
269
+ decision = { kind: "allow" };
270
+ const agentId = await createAgentWithoutDefaultInstance("Thinking agent");
271
+ const instancesBefore = await count(ApiResourceKind.agent_instance);
272
+
273
+ const err = await captureError(() =>
274
+ command.create(
275
+ createInput({
276
+ agentId,
277
+ executionConfig: { modelName: "composer-2.5", thinkingMode: ThinkingMode.ENABLED },
278
+ }),
279
+ ),
280
+ );
281
+
282
+ expect(err.code).toBe(Code.InvalidArgument);
283
+ expect(err.rawMessage).toContain("no thinking capability");
284
+ expect(await count(ApiResourceKind.agent_instance)).toBe(instancesBefore);
285
+ });
245
286
  });
@@ -8,7 +8,9 @@
8
8
  * Also pins the platform-row rules (platform-rows.ts, stigmer#980): a
9
9
  * platform marker write is appended to the transcript and never replaces
10
10
  * it, idempotently, and a PAUSED or CANCELLED execution's stop row stays
11
- * last across the runner's late writes, whatever order they land in.
11
+ * last across the runner's late writes, whatever order they land in; and a
12
+ * paused execution's phase moves only on the platform's own write
13
+ * (stigmer#1370), so the runner's straggler streaming cannot un-pause it.
12
14
  * These exercise applyUpdateStatusMerge directly on a clone — mirroring
13
15
  * the freshly-loaded resource the merge mutates in place inside the
14
16
  * updateResource write lock.
@@ -37,6 +39,7 @@ import { createLogger } from "../../../boot/logger.js";
37
39
  import { findToolCallInExecution } from "../submit-approval.js";
38
40
  import { CANCEL_STOP_ROW, PAUSE_STOP_ROW } from "../platform-rows.js";
39
41
  import { applyUpdateStatusMerge } from "../update-status.js";
42
+ import type { StatusWriter } from "../update-status.js";
40
43
 
41
44
  const silentLogger = createLogger({
42
45
  level: "error",
@@ -51,13 +54,14 @@ const silentLogger = createLogger({
51
54
  function runBuildStep(
52
55
  existing: AgentExecution,
53
56
  incoming: MessageInitShape<typeof AgentExecutionStatusSchema>,
57
+ writer: StatusWriter = "wire",
54
58
  ): AgentExecution {
55
59
  const input = create(AgentExecutionUpdateStatusInputSchema, {
56
60
  executionId: existing.metadata?.id ?? "",
57
61
  status: incoming,
58
62
  });
59
63
  const merged = clone(AgentExecutionSchema, existing);
60
- applyUpdateStatusMerge(merged, input, silentLogger);
64
+ applyUpdateStatusMerge(merged, input, silentLogger, writer);
61
65
  return merged;
62
66
  }
63
67
 
@@ -584,3 +588,55 @@ describe("platform rows (stigmer#980)", () => {
584
588
  });
585
589
  });
586
590
  });
591
+
592
+ describe("a paused execution stays paused until Resume (stigmer#1370)", () => {
593
+ function pausedWith(...contents: string[]): AgentExecution {
594
+ return create(AgentExecutionSchema, {
595
+ metadata: { id: "exec-paused", name: "exec-paused" },
596
+ spec: {},
597
+ status: {
598
+ phase: ExecutionPhase.EXECUTION_PAUSED,
599
+ messages: [
600
+ ...contents.map((content) => ({ type: MessageType.MESSAGE_AI, content })),
601
+ { type: MessageType.MESSAGE_SYSTEM, content: PAUSE_STOP_ROW },
602
+ ],
603
+ },
604
+ });
605
+ }
606
+ const contentsOf = (e: AgentExecution) => (e.status?.messages ?? []).map((m) => m.content);
607
+
608
+ it("keeps PAUSED across the runner's straggler streaming write, and merges its text beneath the pause row", () => {
609
+ // The Pause RPC and the workflow's row landed; the runner learns of the
610
+ // pause on a later heartbeat and keeps streaming IN_PROGRESS writes that
611
+ // carry its growing transcript without the row.
612
+ const merged = runBuildStep(pausedWith("The press, part"), {
613
+ phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
614
+ messages: [{ type: MessageType.MESSAGE_AI, content: "The press, part one and two" }],
615
+ });
616
+ expect(merged.status?.phase, "a wire write cannot un-pause").toBe(ExecutionPhase.EXECUTION_PAUSED);
617
+ expect(contentsOf(merged)).toEqual(["The press, part one and two", PAUSE_STOP_ROW]);
618
+ });
619
+
620
+ it("keeps PAUSED against a wire phase-only IN_PROGRESS write", () => {
621
+ const merged = runBuildStep(pausedWith("a1"), { phase: ExecutionPhase.EXECUTION_IN_PROGRESS });
622
+ expect(merged.status?.phase).toBe(ExecutionPhase.EXECUTION_PAUSED);
623
+ });
624
+
625
+ it("lets the platform's own write move a paused execution (the workflow's resume heal)", () => {
626
+ const merged = runBuildStep(
627
+ pausedWith("a1"),
628
+ { phase: ExecutionPhase.EXECUTION_IN_PROGRESS },
629
+ "platform",
630
+ );
631
+ expect(merged.status?.phase).toBe(ExecutionPhase.EXECUTION_IN_PROGRESS);
632
+ expect(contentsOf(merged)).toEqual(["a1", PAUSE_STOP_ROW]);
633
+ });
634
+
635
+ it("leaves a wire write on a running execution as it was (the latch is PAUSED's alone)", () => {
636
+ const merged = runBuildStep(existingWith(ExecutionPhase.EXECUTION_IN_PROGRESS, "m1"), {
637
+ phase: ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
638
+ messages: [{ content: "m1" }],
639
+ });
640
+ expect(merged.status?.phase).toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
641
+ });
642
+ });