@stigmer/server 3.15.3-dev.20260916211208 → 3.16.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.
- package/dist/authorization/lane-admission.d.ts +50 -0
- package/dist/authorization/lane-admission.d.ts.map +1 -0
- package/dist/authorization/lane-admission.js +13 -0
- package/dist/authorization/lane-admission.js.map +1 -0
- package/dist/authorization/schedule-fire-caller.d.ts +2 -2
- package/dist/authorization/schedule-fire-caller.d.ts.map +1 -1
- package/dist/authorization/schedule-fire-caller.js +8 -8
- package/dist/authorization/schedule-fire-caller.js.map +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +66 -14
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/inprocess.d.ts.map +1 -1
- package/dist/boot/inprocess.js +6 -1
- package/dist/boot/inprocess.js.map +1 -1
- package/dist/domain/executioncontext/resolve-values-for-caller.d.ts +33 -14
- package/dist/domain/executioncontext/resolve-values-for-caller.d.ts.map +1 -1
- package/dist/domain/executioncontext/resolve-values-for-caller.js +32 -8
- package/dist/domain/executioncontext/resolve-values-for-caller.js.map +1 -1
- package/dist/domain/identityaccount/actor.d.ts +33 -18
- package/dist/domain/identityaccount/actor.d.ts.map +1 -1
- package/dist/domain/identityaccount/actor.js +20 -3
- package/dist/domain/identityaccount/actor.js.map +1 -1
- package/dist/domain/identityaccount/resolve.d.ts +19 -0
- package/dist/domain/identityaccount/resolve.d.ts.map +1 -1
- package/dist/domain/identityaccount/resolve.js +12 -0
- package/dist/domain/identityaccount/resolve.js.map +1 -1
- package/dist/domain/mcpserver/connect-execution-id.d.ts +5 -0
- package/dist/domain/mcpserver/connect-execution-id.d.ts.map +1 -0
- package/dist/domain/mcpserver/connect-execution-id.js +32 -0
- package/dist/domain/mcpserver/connect-execution-id.js.map +1 -0
- package/dist/domain/mcpserver/connect.d.ts +28 -2
- package/dist/domain/mcpserver/connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/connect.js +27 -7
- package/dist/domain/mcpserver/connect.js.map +1 -1
- package/dist/domain/mcpserver/engine.d.ts +9 -3
- package/dist/domain/mcpserver/engine.d.ts.map +1 -1
- package/dist/domain/mcpserver/engine.js.map +1 -1
- package/dist/domain/mcpserver/start-connect.js +1 -1
- package/dist/domain/mcpserver/start-connect.js.map +1 -1
- package/dist/domain/platform/controller.d.ts +24 -11
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/plugin/overlay/sanitize.d.ts.map +1 -1
- package/dist/domain/plugin/overlay/sanitize.js +4 -2
- package/dist/domain/plugin/overlay/sanitize.js.map +1 -1
- package/dist/domain/workflowexecution/phases.d.ts +21 -0
- package/dist/domain/workflowexecution/phases.d.ts.map +1 -0
- package/dist/domain/workflowexecution/phases.js +31 -0
- package/dist/domain/workflowexecution/phases.js.map +1 -0
- package/dist/identity/oidc-verifier.js +8 -1
- package/dist/identity/oidc-verifier.js.map +1 -1
- package/dist/pipeline/apiresource-meta.d.ts +15 -0
- package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
- package/dist/pipeline/apiresource-meta.js +34 -0
- package/dist/pipeline/apiresource-meta.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +5 -3
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/dist/runnerauth/bound-execution.d.ts +27 -0
- package/dist/runnerauth/bound-execution.d.ts.map +1 -0
- package/dist/runnerauth/bound-execution.js +137 -0
- package/dist/runnerauth/bound-execution.js.map +1 -0
- package/dist/runnerauth/built-in-runner-credential-provider.d.ts +28 -0
- package/dist/runnerauth/built-in-runner-credential-provider.d.ts.map +1 -0
- package/dist/runnerauth/built-in-runner-credential-provider.js +217 -0
- package/dist/runnerauth/built-in-runner-credential-provider.js.map +1 -0
- package/dist/runnerauth/constants.d.ts +63 -0
- package/dist/runnerauth/constants.d.ts.map +1 -0
- package/dist/runnerauth/constants.js +63 -0
- package/dist/runnerauth/constants.js.map +1 -0
- package/dist/runnerauth/dispatch-credential.d.ts +38 -0
- package/dist/runnerauth/dispatch-credential.d.ts.map +1 -0
- package/dist/runnerauth/dispatch-credential.js +17 -0
- package/dist/runnerauth/dispatch-credential.js.map +1 -0
- package/dist/runnerauth/runner-credential-provider.d.ts +49 -11
- package/dist/runnerauth/runner-credential-provider.d.ts.map +1 -1
- package/dist/runnerauth/runner-credential-provider.js +16 -4
- package/dist/runnerauth/runner-credential-provider.js.map +1 -1
- package/dist/runnerauth/runner-subject-verifier.d.ts +14 -0
- package/dist/runnerauth/runner-subject-verifier.d.ts.map +1 -0
- package/dist/runnerauth/runner-subject-verifier.js +115 -0
- package/dist/runnerauth/runner-subject-verifier.js.map +1 -0
- package/dist/runnerauth/runnerauth.d.ts +33 -4
- package/dist/runnerauth/runnerauth.d.ts.map +1 -1
- package/dist/runnerauth/runnerauth.js +131 -35
- package/dist/runnerauth/runnerauth.js.map +1 -1
- package/dist/temporal/agentexecution/engine-client.d.ts +13 -0
- package/dist/temporal/agentexecution/engine-client.d.ts.map +1 -1
- package/dist/temporal/agentexecution/engine-client.js +10 -2
- package/dist/temporal/agentexecution/engine-client.js.map +1 -1
- package/dist/temporal/agentexecution/workflow-input.d.ts +13 -0
- package/dist/temporal/agentexecution/workflow-input.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +35 -31
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
- package/dist/temporal/workflowexecution/engine-client.d.ts +24 -0
- package/dist/temporal/workflowexecution/engine-client.d.ts.map +1 -1
- package/dist/temporal/workflowexecution/engine-client.js +26 -19
- package/dist/temporal/workflowexecution/engine-client.js.map +1 -1
- package/dist/temporal/workflowexecution/workflow-input.d.ts +14 -0
- package/dist/temporal/workflowexecution/workflow-input.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/authorization/__tests__/lane-admission.test.ts +115 -0
- package/src/authorization/lane-admission.ts +73 -0
- package/src/authorization/schedule-fire-caller.ts +10 -14
- package/src/boot/compose.ts +79 -22
- package/src/boot/inprocess.ts +7 -1
- package/src/domain/executioncontext/__tests__/executioncontext.test.ts +104 -0
- package/src/domain/executioncontext/__tests__/resolve-values-capability.test.ts +33 -4
- package/src/domain/executioncontext/resolve-values-for-caller.ts +79 -22
- package/src/domain/identityaccount/__tests__/account-for-stamp.test.ts +111 -0
- package/src/domain/identityaccount/actor.ts +61 -21
- package/src/domain/identityaccount/resolve.ts +31 -0
- package/src/domain/mcpserver/__tests__/connect.test.ts +17 -2
- package/src/domain/mcpserver/connect-execution-id.ts +34 -0
- package/src/domain/mcpserver/connect.ts +38 -3
- package/src/domain/mcpserver/engine.ts +9 -3
- package/src/domain/mcpserver/start-connect.ts +1 -1
- package/src/domain/platform/controller.ts +24 -11
- package/src/domain/plugin/__tests__/plugin.test.ts +116 -1
- package/src/domain/plugin/__tests__/sanitize.test.ts +2 -2
- package/src/domain/plugin/overlay/sanitize.ts +4 -2
- package/src/domain/workflowexecution/__tests__/phases.test.ts +58 -0
- package/src/domain/workflowexecution/phases.ts +33 -0
- package/src/extensions/__tests__/extension-composition.test.ts +7 -1
- package/src/identity/__tests__/oidc-verifier.test.ts +22 -0
- package/src/identity/oidc-verifier.ts +8 -1
- package/src/pipeline/__tests__/kind-by-id-prefix.test.ts +75 -0
- package/src/pipeline/apiresource-meta.ts +41 -0
- package/src/pipeline/steps/__tests__/guard-memory-capture.test.ts +55 -35
- package/src/pipeline/steps/guard-memory-capture.ts +5 -3
- package/src/runnerauth/__tests__/bound-execution.test.ts +334 -0
- package/src/runnerauth/__tests__/built-in-runner-credential-provider.test.ts +516 -0
- package/src/runnerauth/__tests__/dispatch-credential.test.ts +94 -0
- package/src/runnerauth/__tests__/runner-credential-provider.test.ts +27 -0
- package/src/runnerauth/__tests__/runner-subject-composed.test.ts +497 -0
- package/src/runnerauth/__tests__/runner-subject-verifier.test.ts +541 -0
- package/src/runnerauth/bound-execution.ts +270 -0
- package/src/runnerauth/built-in-runner-credential-provider.ts +300 -0
- package/src/runnerauth/constants.ts +74 -0
- package/src/runnerauth/dispatch-credential.ts +63 -0
- package/src/runnerauth/runner-credential-provider.ts +60 -11
- package/src/runnerauth/runner-subject-verifier.ts +152 -0
- package/src/runnerauth/runnerauth.ts +148 -38
- package/src/temporal/agentexecution/__tests__/engine-client.test.ts +107 -38
- package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +105 -2
- package/src/temporal/agentexecution/engine-client.ts +27 -2
- package/src/temporal/agentexecution/workflow-input.ts +13 -0
- package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +90 -44
- package/src/temporal/workflowexecution/__tests__/engine-client.test.ts +139 -33
- package/src/temporal/workflowexecution/engine-client.ts +48 -21
- package/src/temporal/workflowexecution/workflow-input.ts +14 -0
|
@@ -118,7 +118,7 @@ export async function startConnect(
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
const prepared = await prepareConnect(deps, mcpServer, input);
|
|
121
|
+
const prepared = await prepareConnect(deps, mcpServer, input, identity);
|
|
122
122
|
|
|
123
123
|
// Taken before the start so the advisory describes the queue the run is
|
|
124
124
|
// about to join. Warn-only by design: a worker may be booting (the
|
|
@@ -10,21 +10,34 @@
|
|
|
10
10
|
* composed-server pattern).
|
|
11
11
|
*
|
|
12
12
|
* getRunnerScopedToken is the mint side of the runner-token lane
|
|
13
|
-
* (oss#535) — the seam src/runnerauth
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* (oss#535) — the seam src/runnerauth/. What the token IS depends on the
|
|
14
|
+
* server's posture, and so does who may obtain one:
|
|
15
|
+
*
|
|
16
|
+
* - Under trusted-local (the laptop: one caller, nothing to separate)
|
|
17
|
+
* the arms below mint a clocked token for any caller naming an
|
|
18
|
+
* execution. The token is the LANE DISCRIMINATOR that lets the
|
|
19
|
+
* ExecutionContext read RPCs redact by default without breaking the
|
|
20
|
+
* runner, not a trust boundary — there is no identity for it to
|
|
21
|
+
* change.
|
|
22
|
+
* - Under the built-in authorization posture the same token is an
|
|
23
|
+
* IDENTITY: the runner-subject verifier admits its bearer as the
|
|
24
|
+
* human whose run it is (runnerauth/runner-subject-verifier.ts). A
|
|
25
|
+
* mint for anyone would be impersonation, so the built-in provider
|
|
26
|
+
* defines exchangeScopedToken and this controller delegates to it:
|
|
27
|
+
* the run credential is minted for the run's own person and nobody
|
|
28
|
+
* else (runnerauth/built-in-runner-credential-provider.ts). Runs get
|
|
29
|
+
* their credential from the dispatch itself (the engine clients put
|
|
30
|
+
* it on the workflow input); this exchange is the runner's fallback.
|
|
19
31
|
*
|
|
20
32
|
* The C4 capability delegation (20260827.09, gate ruling Q1): when the
|
|
21
33
|
* composed provider defines exchangeScopedToken / bootstrapCredentials,
|
|
22
34
|
* this controller delegates the exchange arms and the bootstrap
|
|
23
|
-
* credential fields to it wholesale —
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* suites pin that
|
|
35
|
+
* credential fields to it wholesale — per-arm caller gating, row
|
|
36
|
+
* authorization, and multi-lane minting are POLICY, not contract, and
|
|
37
|
+
* live behind the seam (the cloud's four-arm exchange; open source's
|
|
38
|
+
* built-in gate). With neither defined (the trusted-local default), every
|
|
39
|
+
* arm below behaves exactly as before; the conformance suites pin that
|
|
40
|
+
* byte-identity.
|
|
28
41
|
*/
|
|
29
42
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
30
43
|
import { create } from "@bufbuild/protobuf";
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* upgrade drops what the archive dropped; uninstall removes every member
|
|
11
11
|
* and is refused while a user's own agent still references one; a slug an
|
|
12
12
|
* unmanaged resource holds refuses the install; a bad overlay refuses
|
|
13
|
-
* before any write.
|
|
13
|
+
* before any write. A second composition, under an authorizer that denies
|
|
14
|
+
* `can_write_reserved_labels`, pins the reserved-label refusal the
|
|
15
|
+
* open-source posture allows by design.
|
|
14
16
|
*/
|
|
15
17
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
16
18
|
import net from "node:net";
|
|
@@ -46,11 +48,13 @@ import { SkillQueryController } from "@stigmer/protos/ai/stigmer/agentic/skill/v
|
|
|
46
48
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
47
49
|
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
48
50
|
import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
51
|
+
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
49
52
|
|
|
50
53
|
import { writeArchive } from "../../../archive/write.js";
|
|
51
54
|
import { loadConfig } from "../../../boot/config.js";
|
|
52
55
|
import { composeServer } from "../../../boot/compose.js";
|
|
53
56
|
import type { ComposedServer } from "../../../boot/compose.js";
|
|
57
|
+
import type { Authorizer, AuthzCheck } from "../../../extensions/authorizer.js";
|
|
54
58
|
import { createLogger } from "../../../boot/logger.js";
|
|
55
59
|
import {
|
|
56
60
|
PLUGIN_LABEL,
|
|
@@ -679,3 +683,114 @@ describe("Plugin push — refusals before any write", () => {
|
|
|
679
683
|
expect(agent.metadata?.labels[PLUGIN_LABEL]).toBe(installed.metadata?.id);
|
|
680
684
|
});
|
|
681
685
|
});
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The reserved-label refusal where an authorizer enforces. The open-source
|
|
689
|
+
* built-in authorizer allows `can_write_reserved_labels` by design, so the
|
|
690
|
+
* default composition above cannot show the refusal; this composition
|
|
691
|
+
* registers an authorizer that denies exactly that permission and allows
|
|
692
|
+
* everything else, the posture a hosted edition takes. An archive whose
|
|
693
|
+
* overlay carries a `stigmer.ai/*` label is refused before a byte is
|
|
694
|
+
* written, and a plain overlay under the same authorizer still installs —
|
|
695
|
+
* so the sanitiser asks the platform capability and nothing broader.
|
|
696
|
+
*/
|
|
697
|
+
describe("Plugin push under an authorizer that enforces reserved labels", () => {
|
|
698
|
+
let enforcingDir: string;
|
|
699
|
+
let enforcing: ComposedServer;
|
|
700
|
+
let enforcingPlugins: Client<typeof PluginCommandController>;
|
|
701
|
+
let enforcingQuery: Client<typeof PluginQueryController>;
|
|
702
|
+
const observed: AuthzCheck[] = [];
|
|
703
|
+
|
|
704
|
+
beforeAll(async () => {
|
|
705
|
+
enforcingDir = mkdtempSync(path.join(tmpdir(), "plugin-domain-enforcing-"));
|
|
706
|
+
const grpcPort = await reserveFreePort();
|
|
707
|
+
const authorizer: Authorizer = {
|
|
708
|
+
authorize(_caller, check) {
|
|
709
|
+
if (check.permission === IamPermission.can_write_reserved_labels) {
|
|
710
|
+
observed.push(check);
|
|
711
|
+
return Promise.resolve({
|
|
712
|
+
kind: "deny",
|
|
713
|
+
reason: "reserved labels are the platform's",
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return Promise.resolve({ kind: "allow" });
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
enforcing = await composeServer({
|
|
720
|
+
config: loadConfig({
|
|
721
|
+
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
722
|
+
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
723
|
+
GRPC_PORT: String(grpcPort),
|
|
724
|
+
DB_PATH: path.join(enforcingDir, "stigmer.db"),
|
|
725
|
+
ARTIFACT_LOCAL_BASE_PATH: path.join(enforcingDir, "artifacts"),
|
|
726
|
+
STORAGE_PATH: path.join(enforcingDir, "storage"),
|
|
727
|
+
}),
|
|
728
|
+
logger: silentLogger,
|
|
729
|
+
host: "127.0.0.1",
|
|
730
|
+
extensions: [{ name: "reserved-labels-enforced", authorizer }],
|
|
731
|
+
});
|
|
732
|
+
const port = await enforcing.start();
|
|
733
|
+
const transport: Transport = createGrpcTransport({
|
|
734
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
735
|
+
});
|
|
736
|
+
enforcingPlugins = createClient(PluginCommandController, transport);
|
|
737
|
+
enforcingQuery = createClient(PluginQueryController, transport);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
afterAll(async () => {
|
|
741
|
+
await enforcing.shutdown();
|
|
742
|
+
rmSync(enforcingDir, { recursive: true, force: true });
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it("refuses an archive whose overlay carries a reserved label, naming the document and the key, and writes nothing", async () => {
|
|
746
|
+
const name = uniqueName("plg-forged");
|
|
747
|
+
const forged = withFile(
|
|
748
|
+
thermosLike(name),
|
|
749
|
+
"ai.stigmer/agent.yaml",
|
|
750
|
+
[
|
|
751
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
752
|
+
"kind: Agent",
|
|
753
|
+
"metadata:",
|
|
754
|
+
` name: ${name}`,
|
|
755
|
+
" labels:",
|
|
756
|
+
" stigmer.ai/plugin: plg_somebody_else",
|
|
757
|
+
"spec:",
|
|
758
|
+
" instructions: Long enough instructions for the forged agent.",
|
|
759
|
+
"",
|
|
760
|
+
].join("\n"),
|
|
761
|
+
);
|
|
762
|
+
const error = await expectCode(
|
|
763
|
+
enforcingPlugins.push({ org: ORG, artifact: archiveOf(forged) }),
|
|
764
|
+
Code.InvalidArgument,
|
|
765
|
+
);
|
|
766
|
+
expect(error.rawMessage).toContain(
|
|
767
|
+
"cannot be set by a plugin (ai.stigmer/agent.yaml: stigmer.ai/plugin)",
|
|
768
|
+
);
|
|
769
|
+
expect(observed.map((c) => c.permission)).toEqual([
|
|
770
|
+
IamPermission.can_write_reserved_labels,
|
|
771
|
+
]);
|
|
772
|
+
await expectCode(
|
|
773
|
+
enforcingQuery.getByReference(
|
|
774
|
+
createMessage(ApiResourceReferenceSchema, {
|
|
775
|
+
org: ORG,
|
|
776
|
+
kind: ApiResourceKind.plugin,
|
|
777
|
+
slug: name,
|
|
778
|
+
}),
|
|
779
|
+
),
|
|
780
|
+
Code.NotFound,
|
|
781
|
+
);
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("installs a plain overlay under the same authorizer: the platform stamps its own labels through the in-process origin", async () => {
|
|
785
|
+
const name = uniqueName("plg-honest");
|
|
786
|
+
const installed = await enforcingPlugins.push({
|
|
787
|
+
org: ORG,
|
|
788
|
+
artifact: archiveOf(thermosLike(name)),
|
|
789
|
+
});
|
|
790
|
+
expect(installed.status?.state).toBe(PluginState.READY);
|
|
791
|
+
const { members } = await enforcingQuery.listMembers({
|
|
792
|
+
value: installed.metadata!.id,
|
|
793
|
+
});
|
|
794
|
+
expect(members).toHaveLength(3);
|
|
795
|
+
});
|
|
796
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pins the sanitiser, the one place plugin-provided metadata is judged
|
|
3
3
|
* before it rides the in-process lane: a `stigmer.ai/*` label in an
|
|
4
|
-
* overlay is refused
|
|
4
|
+
* overlay is refused INVALID_ARGUMENT naming the document and the keys
|
|
5
5
|
* under a denying authorizer and passes under an allowing one (the
|
|
6
6
|
* open-source posture), the operator check is consulted lazily and only
|
|
7
7
|
* for the platform capability, a preset id and a name that describes
|
|
@@ -86,7 +86,7 @@ describe("sanitizeOverlays", () => {
|
|
|
86
86
|
USER,
|
|
87
87
|
),
|
|
88
88
|
);
|
|
89
|
-
expect(error.code).toBe(Code.
|
|
89
|
+
expect(error.code).toBe(Code.InvalidArgument);
|
|
90
90
|
expect(error.rawMessage).toContain(
|
|
91
91
|
"ai.stigmer/agent.yaml: stigmer.ai/default-agent",
|
|
92
92
|
);
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* `platform:stigmer`, the same check GuardReservedLabels runs and the
|
|
14
14
|
* same lazy consultation of the one composed Authorizer (the
|
|
15
15
|
* open-source permissive default allows; an enforcing authorizer
|
|
16
|
-
* denies with
|
|
16
|
+
* denies with INVALID_ARGUMENT naming the keys, the code the
|
|
17
|
+
* reserved-label guard answers a client mutation with, so one refusal
|
|
18
|
+
* class carries one code wherever it is met);
|
|
17
19
|
* - `metadata.id` is the server's to mint, never an author's;
|
|
18
20
|
* - the agent overlay's name is the plugin's, a server overlay's name is
|
|
19
21
|
* its `mcpServers` key, a workflow overlay's name is its file stem: an
|
|
@@ -130,7 +132,7 @@ export async function sanitizeOverlays(
|
|
|
130
132
|
throw new ConnectError(
|
|
131
133
|
`Labels in the reserved '${RESERVED_LABEL_PREFIX}' namespace are platform-managed and ` +
|
|
132
134
|
`cannot be set by a plugin (${listed}). Remove them from the plugin's ai.stigmer/ documents.`,
|
|
133
|
-
Code.
|
|
135
|
+
Code.InvalidArgument,
|
|
134
136
|
);
|
|
135
137
|
}
|
|
136
138
|
case "unavailable":
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins `isTerminalWorkflowExecutionPhase` (phases.ts) over every member
|
|
3
|
+
* of the workflow ExecutionPhase enum, so the set is stated by
|
|
4
|
+
* enumeration and a new phase added to the contract lands in exactly one
|
|
5
|
+
* arm here. The contrast with subscribe.ts's `isWorkflowTerminalPhase`
|
|
6
|
+
* (TERMINATED omitted on purpose) is pinned so the two are never merged.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
|
|
11
|
+
|
|
12
|
+
import { isTerminalWorkflowExecutionPhase } from "../phases.js";
|
|
13
|
+
import { isWorkflowTerminalPhase } from "../subscribe.js";
|
|
14
|
+
|
|
15
|
+
const TERMINAL = [
|
|
16
|
+
ExecutionPhase.EXECUTION_COMPLETED,
|
|
17
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
18
|
+
ExecutionPhase.EXECUTION_CANCELLED,
|
|
19
|
+
ExecutionPhase.EXECUTION_TERMINATED,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
describe("isTerminalWorkflowExecutionPhase", () => {
|
|
23
|
+
it.each(TERMINAL.map((phase) => [ExecutionPhase[phase], phase] as const))(
|
|
24
|
+
"%s will never run again",
|
|
25
|
+
(_name, phase) => {
|
|
26
|
+
expect(isTerminalWorkflowExecutionPhase(phase)).toBe(true);
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
it.each([
|
|
31
|
+
["UNSPECIFIED", ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED],
|
|
32
|
+
["PENDING", ExecutionPhase.EXECUTION_PENDING],
|
|
33
|
+
["IN_PROGRESS", ExecutionPhase.EXECUTION_IN_PROGRESS],
|
|
34
|
+
["PAUSED", ExecutionPhase.EXECUTION_PAUSED],
|
|
35
|
+
])("%s may still run", (_name, phase) => {
|
|
36
|
+
expect(isTerminalWorkflowExecutionPhase(phase)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("covers every member of the enum — a new phase must be classified here", () => {
|
|
40
|
+
const members = Object.values(ExecutionPhase).filter(
|
|
41
|
+
(value): value is ExecutionPhase => typeof value === "number",
|
|
42
|
+
);
|
|
43
|
+
const classified = members.filter((phase) =>
|
|
44
|
+
isTerminalWorkflowExecutionPhase(phase),
|
|
45
|
+
);
|
|
46
|
+
expect(classified.sort()).toEqual([...TERMINAL].sort());
|
|
47
|
+
expect(members).toHaveLength(8);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("differs from the subscribe stream's close set on TERMINATED alone — deliberate, not to be merged", () => {
|
|
51
|
+
expect(isWorkflowTerminalPhase(ExecutionPhase.EXECUTION_TERMINATED)).toBe(
|
|
52
|
+
false,
|
|
53
|
+
);
|
|
54
|
+
expect(
|
|
55
|
+
isTerminalWorkflowExecutionPhase(ExecutionPhase.EXECUTION_TERMINATED),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workflow execution's "will this run ever run again?" predicate —
|
|
3
|
+
* the twin of domain/agentexecution/phases.ts `isTerminalExecutionPhase`,
|
|
4
|
+
* stated once so every consumer names the same set: COMPLETED, FAILED,
|
|
5
|
+
* CANCELLED and TERMINATED. PAUSED is NOT terminal (lifecycle.ts:
|
|
6
|
+
* `applyLifecyclePhaseTransition` keeps `completed_at` as-is on a pause
|
|
7
|
+
* and clears it on the way back to IN_PROGRESS), and neither is PENDING.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately distinct from `isWorkflowTerminalPhase` in subscribe.ts,
|
|
10
|
+
* which OMITS TERMINATED — the disclosed Go quirk the subscribe stream
|
|
11
|
+
* ports faithfully as its close set. Do not merge the two; that file's
|
|
12
|
+
* header carries the reason.
|
|
13
|
+
*
|
|
14
|
+
* The first consumer is the runner-credential lane (runnerauth/
|
|
15
|
+
* bound-execution.ts): a run credential is valid while its run may still
|
|
16
|
+
* run, so a terminated workflow's credential must die exactly as a
|
|
17
|
+
* completed one's does.
|
|
18
|
+
*/
|
|
19
|
+
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
|
|
20
|
+
|
|
21
|
+
export function isTerminalWorkflowExecutionPhase(
|
|
22
|
+
phase: ExecutionPhase,
|
|
23
|
+
): boolean {
|
|
24
|
+
switch (phase) {
|
|
25
|
+
case ExecutionPhase.EXECUTION_COMPLETED:
|
|
26
|
+
case ExecutionPhase.EXECUTION_FAILED:
|
|
27
|
+
case ExecutionPhase.EXECUTION_CANCELLED:
|
|
28
|
+
case ExecutionPhase.EXECUTION_TERMINATED:
|
|
29
|
+
return true;
|
|
30
|
+
default:
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -544,7 +544,13 @@ describe("extension composition (require-authentication posture)", () => {
|
|
|
544
544
|
};
|
|
545
545
|
expect(parsed.posture).toBe("required");
|
|
546
546
|
expect(parsed.source).toBe("extension 'fake-identity'");
|
|
547
|
-
|
|
547
|
+
// A unit that declares the posture and brings verifiers but NO
|
|
548
|
+
// Authorizer is the BUILT-IN authorization posture: open source
|
|
549
|
+
// composes its Authorizer, and with it the runner-subject verifier
|
|
550
|
+
// between the API-key lane and the unit's own (runnerauth/
|
|
551
|
+
// runner-subject-verifier.ts). A unit with its own Authorizer gets
|
|
552
|
+
// neither (runner-subject-composed.test.ts pins that chain).
|
|
553
|
+
expect(parsed.verifiers).toBe("apikey, runner, fake-verifier");
|
|
548
554
|
});
|
|
549
555
|
|
|
550
556
|
it("a declared posture whose unit registers no verifier of its own is a boot throw naming the unit — the API-key lane alone cannot bootstrap", async () => {
|
|
@@ -218,6 +218,28 @@ describe("the byte-pinned classifyAuthError arms", () => {
|
|
|
218
218
|
expect(ConnectError.from(error).code).toBe(Code.Unauthenticated);
|
|
219
219
|
expect(ConnectError.from(error).rawMessage).toBe(INVALID_TOKEN_MESSAGE);
|
|
220
220
|
});
|
|
221
|
+
|
|
222
|
+
it("a JWT whose algorithm the JWKS cannot serve (HS256 against RS256 keys) → 'invalid token', a CREDENTIAL rejection, never a plain error the chassis maps INTERNAL", async () => {
|
|
223
|
+
// The shape the server's own execution-scoped runner token has
|
|
224
|
+
// (runnerauth.ts). Whether this verifier should ever SEE it is the
|
|
225
|
+
// chain order's business (the runner verifier claims it first); that
|
|
226
|
+
// it must not answer INTERNAL when it does is this arm's. Until
|
|
227
|
+
// 2026-09-16 it threw jose's JOSENotSupported unclassified, and a
|
|
228
|
+
// self-host with sign-in on failed every agent execution with
|
|
229
|
+
// "internal server error" (stigmer#1137).
|
|
230
|
+
const segment = (value: unknown): string =>
|
|
231
|
+
Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
232
|
+
const hs256 = `${segment({ alg: "HS256", typ: "JWT" })}.${segment({
|
|
233
|
+
token_type: "execution_scoped",
|
|
234
|
+
execution_id: "aex_1",
|
|
235
|
+
iat: 1,
|
|
236
|
+
exp: 4102444800,
|
|
237
|
+
})}.${segment("a-mac-this-verifier-cannot-check")}`;
|
|
238
|
+
const error = await rejectionOf(verifier().verify(hs256));
|
|
239
|
+
expect(error).toBeInstanceOf(ConnectError);
|
|
240
|
+
expect(ConnectError.from(error).code).toBe(Code.Unauthenticated);
|
|
241
|
+
expect(ConnectError.from(error).rawMessage).toBe(INVALID_TOKEN_MESSAGE);
|
|
242
|
+
});
|
|
221
243
|
});
|
|
222
244
|
|
|
223
245
|
describe("subject → account resolution (20260911.11 Q-IA-2, A1)", () => {
|
|
@@ -159,7 +159,14 @@ function classifyJoseError(error: unknown): unknown {
|
|
|
159
159
|
}
|
|
160
160
|
if (
|
|
161
161
|
error instanceof joseErrors.JWTInvalid ||
|
|
162
|
-
error instanceof joseErrors.JWSInvalid
|
|
162
|
+
error instanceof joseErrors.JWSInvalid ||
|
|
163
|
+
// A JWT whose algorithm the issuer's key set cannot serve (an HS256
|
|
164
|
+
// token against an RS256 JWKS): a credential this verifier recognizes
|
|
165
|
+
// but cannot verify, so a credential rejection — never the
|
|
166
|
+
// infrastructure arm. Until 2026-09-16 it fell through to INTERNAL,
|
|
167
|
+
// and a self-host with sign-in on failed every agent execution on its
|
|
168
|
+
// own runner token (stigmer#1137).
|
|
169
|
+
error instanceof joseErrors.JOSENotSupported
|
|
163
170
|
) {
|
|
164
171
|
return new ConnectError(INVALID_TOKEN_MESSAGE, Code.Unauthenticated);
|
|
165
172
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins `kindByIdPrefix` (pipeline/apiresource-meta.ts), the inverse of
|
|
3
|
+
* `getIdPrefix` over the ids the server mints (`<prefix>_<ulid>`,
|
|
4
|
+
* pipeline/steps/defaults.ts). The runner-credential lane reads it to
|
|
5
|
+
* learn which execution kind a token's binding names — an agent
|
|
6
|
+
* execution (`aex_`) or a workflow execution (`wex_`) — without a second
|
|
7
|
+
* claim on the token or a guess.
|
|
8
|
+
*
|
|
9
|
+
* Two properties are load-bearing:
|
|
10
|
+
*
|
|
11
|
+
* - NEVER a throw. The id arrives inside a credential; the caller
|
|
12
|
+
* refuses with its own sentence, so anything that is not a known
|
|
13
|
+
* prefix followed by an underscore is the unknown kind.
|
|
14
|
+
* - Every `id_prefix` in the contract is UNIQUE. The lookup is a Map
|
|
15
|
+
* over the `kind_meta` table; a duplicate prefix would silently win
|
|
16
|
+
* for one kind and lose for the other. The pin below turns a future
|
|
17
|
+
* duplicate into a reviewed change.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
ApiResourceKind,
|
|
23
|
+
ApiResourceKindSchema,
|
|
24
|
+
kind_meta,
|
|
25
|
+
} from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
26
|
+
import { getOption, hasOption } from "@bufbuild/protobuf";
|
|
27
|
+
|
|
28
|
+
import { getIdPrefix, kindByIdPrefix } from "../apiresource-meta.js";
|
|
29
|
+
|
|
30
|
+
describe("kindByIdPrefix — a minted id names its kind by prefix", () => {
|
|
31
|
+
it.each([
|
|
32
|
+
["aex_01m2mp45efcjvq2z1e1yn4cyz7", ApiResourceKind.agent_execution],
|
|
33
|
+
["wex_01m2mp45efcjvq2z1e1yn4cyz7", ApiResourceKind.workflow_execution],
|
|
34
|
+
["ses_x", ApiResourceKind.session],
|
|
35
|
+
["ida_carol", ApiResourceKind.identity_account],
|
|
36
|
+
])("%s is %s", (id, kind) => {
|
|
37
|
+
expect(kindByIdPrefix(id)).toBe(kind);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it.each([
|
|
41
|
+
["zzz_unknown_kind", "an unknown prefix"],
|
|
42
|
+
["aex", "a prefix with no underscore"],
|
|
43
|
+
["_aex", "a leading underscore names no prefix"],
|
|
44
|
+
["", "empty"],
|
|
45
|
+
["AEX_upper", "prefixes are lowercase in the contract"],
|
|
46
|
+
["stk_notajwt", "an API key's prefix is not a kind"],
|
|
47
|
+
])("%s is the unknown kind (%s) — never a throw", (id) => {
|
|
48
|
+
expect(kindByIdPrefix(id)).toBe(ApiResourceKind.api_resource_kind_unknown);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("round-trips with getIdPrefix over every kind the contract declares", () => {
|
|
52
|
+
for (const value of ApiResourceKindSchema.values) {
|
|
53
|
+
if (!hasOption(value, kind_meta)) continue;
|
|
54
|
+
const kind = value.number as ApiResourceKind;
|
|
55
|
+
expect(kindByIdPrefix(`${getIdPrefix(kind)}_01anyulid`), value.name).toBe(
|
|
56
|
+
kind,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("every id_prefix in the contract is unique — the table pin a duplicate must change", () => {
|
|
62
|
+
const seen = new Map<string, string>();
|
|
63
|
+
for (const value of ApiResourceKindSchema.values) {
|
|
64
|
+
if (!hasOption(value, kind_meta)) continue;
|
|
65
|
+
const prefix = getOption(value, kind_meta).idPrefix;
|
|
66
|
+
expect(prefix, `${value.name} declares no id_prefix`).not.toBe("");
|
|
67
|
+
const holder = seen.get(prefix);
|
|
68
|
+
expect(
|
|
69
|
+
holder,
|
|
70
|
+
`id_prefix '${prefix}' is declared by both ${holder} and ${value.name}`,
|
|
71
|
+
).toBeUndefined();
|
|
72
|
+
seen.set(prefix, value.name);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -351,3 +351,44 @@ function kindsByEnumName(): Map<string, ApiResourceKind> {
|
|
|
351
351
|
}
|
|
352
352
|
return enumNameCache;
|
|
353
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The kind a minted resource id belongs to, read off its prefix — the
|
|
357
|
+
* inverse of `getIdPrefix` over the ids `generateId` mints
|
|
358
|
+
* (`<prefix>_<ulid>`, pipeline/steps/defaults.ts). The one consumer today
|
|
359
|
+
* is the runner-credential lane, whose token binds an execution by id and
|
|
360
|
+
* must know whether that id names an agent execution or a workflow
|
|
361
|
+
* execution without a second claim or a guess. Anything that is not
|
|
362
|
+
* `<known prefix>_<rest>` — no underscore, an unknown prefix, the empty
|
|
363
|
+
* string — is `api_resource_kind_unknown`; never a throw, because the id
|
|
364
|
+
* arrived inside a credential and the caller refuses with its own
|
|
365
|
+
* sentence. Every `id_prefix` in the contract is unique; the table pin in
|
|
366
|
+
* __tests__ makes a future duplicate a reviewed change, since this lookup
|
|
367
|
+
* would otherwise resolve it silently to one of the two.
|
|
368
|
+
*/
|
|
369
|
+
export function kindByIdPrefix(id: string): ApiResourceKind {
|
|
370
|
+
const separator = id.indexOf("_");
|
|
371
|
+
if (separator <= 0) {
|
|
372
|
+
return ApiResourceKind.api_resource_kind_unknown;
|
|
373
|
+
}
|
|
374
|
+
return (
|
|
375
|
+
kindsByIdPrefix().get(id.slice(0, separator)) ??
|
|
376
|
+
ApiResourceKind.api_resource_kind_unknown
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let idPrefixCache: Map<string, ApiResourceKind> | undefined;
|
|
381
|
+
|
|
382
|
+
function kindsByIdPrefix(): Map<string, ApiResourceKind> {
|
|
383
|
+
if (idPrefixCache === undefined) {
|
|
384
|
+
idPrefixCache = new Map(
|
|
385
|
+
ApiResourceKindSchema.values
|
|
386
|
+
.filter((value) => hasOption(value, kind_meta))
|
|
387
|
+
.map((value) => [
|
|
388
|
+
getOption(value, kind_meta).idPrefix,
|
|
389
|
+
value.number as ApiResourceKind,
|
|
390
|
+
]),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
return idPrefixCache;
|
|
394
|
+
}
|