@stigmer/server 3.39.1-dev.20260928194002 → 3.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifactstorage/artifact-storage.d.ts +10 -2
- package/dist/artifactstorage/artifact-storage.d.ts.map +1 -1
- package/dist/artifactstorage/artifact-storage.js +6 -3
- package/dist/artifactstorage/artifact-storage.js.map +1 -1
- package/dist/boot/artifact-lane.d.ts +55 -0
- package/dist/boot/artifact-lane.d.ts.map +1 -0
- package/dist/boot/artifact-lane.js +74 -0
- package/dist/boot/artifact-lane.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +43 -19
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/config.d.ts +11 -6
- package/dist/boot/config.d.ts.map +1 -1
- package/dist/boot/config.js +10 -5
- package/dist/boot/config.js.map +1 -1
- package/dist/domain/agent/controller.d.ts +5 -2
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentexecution/controller.d.ts +18 -4
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +2 -2
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/create-steps.d.ts +29 -15
- package/dist/domain/agentexecution/create-steps.d.ts.map +1 -1
- package/dist/domain/agentexecution/create-steps.js +118 -51
- package/dist/domain/agentexecution/create-steps.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +8 -2
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts +9 -4
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +3 -3
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/artifact/file-server.d.ts +2 -1
- package/dist/domain/artifact/file-server.d.ts.map +1 -1
- package/dist/domain/artifact/file-server.js +6 -3
- package/dist/domain/artifact/file-server.js.map +1 -1
- package/dist/domain/channelapp/controller.d.ts +12 -3
- package/dist/domain/channelapp/controller.d.ts.map +1 -1
- package/dist/domain/channelapp/controller.js +19 -9
- package/dist/domain/channelapp/controller.js.map +1 -1
- package/dist/domain/environment/controller.d.ts +8 -5
- package/dist/domain/environment/controller.d.ts.map +1 -1
- package/dist/domain/environment/controller.js +3 -3
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/executioncontext/controller.d.ts +7 -4
- package/dist/domain/executioncontext/controller.d.ts.map +1 -1
- package/dist/domain/executioncontext/controller.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +6 -2
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/memory/constants.d.ts +9 -0
- package/dist/domain/memory/constants.d.ts.map +1 -1
- package/dist/domain/memory/constants.js +9 -0
- package/dist/domain/memory/constants.js.map +1 -1
- package/dist/domain/memory/controller.d.ts +18 -6
- package/dist/domain/memory/controller.d.ts.map +1 -1
- package/dist/domain/memory/controller.js +18 -21
- package/dist/domain/memory/controller.js.map +1 -1
- package/dist/domain/memory/steps.d.ts +38 -10
- package/dist/domain/memory/steps.d.ts.map +1 -1
- package/dist/domain/memory/steps.js +87 -17
- package/dist/domain/memory/steps.js.map +1 -1
- package/dist/domain/organization/controller.d.ts +7 -4
- package/dist/domain/organization/controller.d.ts.map +1 -1
- package/dist/domain/organization/controller.js.map +1 -1
- package/dist/domain/session/controller.d.ts +7 -3
- package/dist/domain/session/controller.d.ts.map +1 -1
- package/dist/domain/session/controller.js.map +1 -1
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +8 -2
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/workflow/controller.d.ts +6 -2
- package/dist/domain/workflow/controller.d.ts.map +1 -1
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts +8 -4
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowexecution/update-status.d.ts.map +1 -1
- package/dist/domain/workflowexecution/update-status.js +3 -2
- package/dist/domain/workflowexecution/update-status.js.map +1 -1
- package/dist/extensions/identity.d.ts +28 -14
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js +38 -0
- package/dist/extensions/identity.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +1 -13
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/package.json +6 -6
- package/src/artifactstorage/__tests__/artifact-storage.test.ts +15 -1
- package/src/artifactstorage/artifact-storage.ts +12 -4
- package/src/boot/__tests__/artifact-lane.test.ts +71 -0
- package/src/boot/__tests__/compose.test.ts +73 -3
- package/src/boot/__tests__/config.test.ts +19 -0
- package/src/boot/artifact-lane.ts +96 -0
- package/src/boot/compose.ts +58 -21
- package/src/boot/config.ts +25 -15
- package/src/domain/agent/controller.ts +5 -2
- package/src/domain/agentexecution/__tests__/create-steps.test.ts +230 -2
- package/src/domain/agentexecution/controller.ts +32 -6
- package/src/domain/agentexecution/create-steps.ts +162 -62
- package/src/domain/agentinstance/controller.ts +8 -2
- package/src/domain/agentshare/controller.ts +12 -7
- package/src/domain/artifact/file-server.ts +8 -4
- package/src/domain/channelapp/controller.ts +38 -11
- package/src/domain/environment/controller.ts +11 -8
- package/src/domain/executioncontext/controller.ts +7 -4
- package/src/domain/mcpserver/controller.ts +6 -2
- package/src/domain/memory/__tests__/capture-credential.test.ts +6 -6
- package/src/domain/memory/__tests__/person-consent-composed.test.ts +187 -0
- package/src/domain/memory/__tests__/person-consent.test.ts +274 -0
- package/src/domain/memory/constants.ts +11 -0
- package/src/domain/memory/controller.ts +36 -27
- package/src/domain/memory/steps.ts +112 -17
- package/src/domain/organization/controller.ts +7 -4
- package/src/domain/plugin/__tests__/plugin.test.ts +8 -0
- package/src/domain/session/controller.ts +7 -3
- package/src/domain/skill/__tests__/skill.test.ts +4 -0
- package/src/domain/skill/controller.ts +8 -2
- package/src/domain/workflow/controller.ts +6 -2
- package/src/domain/workflowexecution/controller.ts +8 -4
- package/src/domain/workflowexecution/update-status.ts +3 -2
- package/src/extensions/__tests__/identity.test.ts +82 -0
- package/src/extensions/__tests__/list-read-scope-composed.test.ts +39 -1
- package/src/extensions/identity.ts +47 -0
- package/src/pipeline/steps/guard-memory-capture.ts +4 -19
|
@@ -104,6 +104,10 @@ beforeAll(async () => {
|
|
|
104
104
|
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
105
105
|
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
106
106
|
GRPC_PORT: String(grpcPort),
|
|
107
|
+
// The reserved port is the unified one only; its +1 neighbour is not
|
|
108
|
+
// reserved, and a lane that cannot bind fails the boot (#1089), so
|
|
109
|
+
// the artifact lane binds ephemeral.
|
|
110
|
+
ARTIFACT_HTTP_PORT: "0",
|
|
107
111
|
DB_PATH: path.join(dir, "stigmer.db"),
|
|
108
112
|
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
109
113
|
STORAGE_PATH: path.join(dir, "storage"),
|
|
@@ -801,6 +805,10 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
|
|
|
801
805
|
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
802
806
|
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
803
807
|
GRPC_PORT: String(grpcPort),
|
|
808
|
+
// The reserved port is the unified one only; its +1 neighbour is not
|
|
809
|
+
// reserved, and a lane that cannot bind fails the boot (#1089), so
|
|
810
|
+
// the artifact lane binds ephemeral.
|
|
811
|
+
ARTIFACT_HTTP_PORT: "0",
|
|
804
812
|
DB_PATH: path.join(enforcingDir, "stigmer.db"),
|
|
805
813
|
ARTIFACT_LOCAL_BASE_PATH: path.join(enforcingDir, "artifacts"),
|
|
806
814
|
STORAGE_PATH: path.join(enforcingDir, "storage"),
|
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
* Proven by session.conformance.test.ts (CONFORMANCE_TARGET=local) and
|
|
14
14
|
* __tests__/session.test.ts.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Every chain opens with Authorize, and create also asks can_execute on
|
|
17
|
+
* the agent instance it binds to (AuthorizeRunTarget); create and delete
|
|
18
|
+
* run the shared tuple-lifecycle steps against the composed lifecycle;
|
|
19
|
+
* updateSubject evaluates its annotation through authorizeDirect after the
|
|
20
|
+
* load (#224); list, listByAgentInstance and listByChannel narrow through
|
|
21
|
+
* the composed list read scope. Per-RPC posture:
|
|
22
|
+
* docs/authorization-coverage.md §8.
|
|
19
23
|
*/
|
|
20
24
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
21
25
|
|
|
@@ -74,6 +74,10 @@ beforeAll(async () => {
|
|
|
74
74
|
// a live local Temporal (the conformance CRUD harness does the same).
|
|
75
75
|
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
76
76
|
GRPC_PORT: String(grpcPort),
|
|
77
|
+
// The reserved port is the unified one only; its +1 neighbour is not
|
|
78
|
+
// reserved, and a lane that cannot bind fails the boot (#1089), so
|
|
79
|
+
// the artifact lane binds ephemeral.
|
|
80
|
+
ARTIFACT_HTTP_PORT: "0",
|
|
77
81
|
DB_PATH: path.join(dir, "stigmer.db"),
|
|
78
82
|
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
79
83
|
STORAGE_PATH: path.join(dir, "storage"),
|
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
* optional deps; every absent-surface answer is a deliberate arm, not an
|
|
14
14
|
* accident.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Every chain opens with Authorize; push runs the tuple-lifecycle splice
|
|
17
|
+
* (SkillPushAuthorizationTuples), and updateVisibility and delete the
|
|
18
|
+
* shared tuple steps; getByReference and listVersions authorize the
|
|
19
|
+
* resolved skill as `get` would; getArtifact and getArtifactDownloadUrl
|
|
20
|
+
* skip authorization by design, the content-hash key being the capability
|
|
21
|
+
* (docs/authorization-coverage.md §21).
|
|
22
|
+
*
|
|
23
|
+
* The transfer lane's capability
|
|
18
24
|
* URLs come from the blob driver through the staging port
|
|
19
25
|
* (transfer/staging.ts): the local driver's point at this server's own
|
|
20
26
|
* HTTP lane, a bucket driver's straight at the bucket — same
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
* Proven by workflow.conformance.test.ts (CONFORMANCE_TARGET=local) and
|
|
14
14
|
* __tests__/.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Every chain opens with Authorize; create, delete and updateVisibility run
|
|
17
|
+
* the shared tuple-lifecycle steps against the composed lifecycle;
|
|
18
|
+
* getByReference and listVersions authorize the resolved workflow as `get`
|
|
19
|
+
* would. getVersion evaluates its annotation through authorizeDirect;
|
|
20
|
+
* validateSpec deliberately does not, since it persists nothing. Per-RPC
|
|
21
|
+
* posture: docs/authorization-coverage.md §17.
|
|
18
22
|
*/
|
|
19
23
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
20
24
|
import { create, enumToJson, fromBinary } from "@bufbuild/protobuf";
|
|
@@ -12,10 +12,14 @@
|
|
|
12
12
|
* Proven by workflowexecution.conformance.test.ts
|
|
13
13
|
* (CONFORMANCE_TARGET=local) and __tests__/.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Every chain opens with Authorize, and create also authorizes its run
|
|
16
|
+
* target (AuthorizeRunTarget); create and delete run the shared
|
|
17
|
+
* tuple-lifecycle steps against the composed lifecycle. The streams and
|
|
18
|
+
* the event log evaluate their annotation through authorizeDirect; the
|
|
19
|
+
* list, summary and pending-approval reads narrow through the composed
|
|
20
|
+
* list read scope. Subscribe streams ride the in-memory stream broker
|
|
21
|
+
* (ADR 011); there is no Redis. Per-RPC posture:
|
|
22
|
+
* docs/authorization-coverage.md §19.
|
|
19
23
|
*/
|
|
20
24
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
21
25
|
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
* phase-transition-only statusAudit bump (heartbeats must not perpetually
|
|
23
23
|
* re-sort long-running executions above new ones in the recents sidebar).
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* The chain opens with Authorize (can_edit on the execution); the
|
|
26
|
+
* broadcast rides the in-memory stream broker (ADR 011) — there is no
|
|
27
|
+
* Redis.
|
|
27
28
|
*/
|
|
28
29
|
import { create, toBinary } from "@bufbuild/protobuf";
|
|
29
30
|
import { timestampNow } from "@bufbuild/protobuf/wkt";
|
|
@@ -13,10 +13,18 @@
|
|
|
13
13
|
* in-process origin, and never a wire `machine` account — the one row
|
|
14
14
|
* where the two predicates part, pinned so a step trusting server-composed
|
|
15
15
|
* state can never be widened to the wire by picking the wrong one.
|
|
16
|
+
*
|
|
17
|
+
* And pins `isFirstPartyHumanOperator` (stigmer#1387), the allow-list
|
|
18
|
+
* memory recall and the person's declared preferences read: a wire `user`
|
|
19
|
+
* whose bearer is not a PlatformClient user token. Every other class, an
|
|
20
|
+
* in-process `user` (the open-source schedule fire's shape) and a
|
|
21
|
+
* platform token naming a PlatformClient are excluded; another issuer's
|
|
22
|
+
* token carrying the same claim is not a platform token (stigmer#1312).
|
|
16
23
|
*/
|
|
17
24
|
import { describe, expect, it } from "vitest";
|
|
18
25
|
|
|
19
26
|
import {
|
|
27
|
+
isFirstPartyHumanOperator,
|
|
20
28
|
isPlatformPipelineCaller,
|
|
21
29
|
isServerComposedRequest,
|
|
22
30
|
} from "../identity.js";
|
|
@@ -103,3 +111,77 @@ describe("isServerComposedRequest", () => {
|
|
|
103
111
|
);
|
|
104
112
|
});
|
|
105
113
|
});
|
|
114
|
+
|
|
115
|
+
/** A JWT carrying `payload`, unsigned: the predicate reads, never verifies. */
|
|
116
|
+
function jwtWith(payload: Record<string, unknown>): string {
|
|
117
|
+
const segment = (value: object) =>
|
|
118
|
+
Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
119
|
+
return `${segment({ alg: "RS256", typ: "JWT" })}.${segment(payload)}.signature`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
describe("isFirstPartyHumanOperator", () => {
|
|
123
|
+
it("is true for a wire user — an OIDC login, an API key, the laptop's operator", () => {
|
|
124
|
+
expect(isFirstPartyHumanOperator(caller({}))).toBe(true);
|
|
125
|
+
expect(isFirstPartyHumanOperator(caller({ origin: "wire" }))).toBe(true);
|
|
126
|
+
expect(
|
|
127
|
+
isFirstPartyHumanOperator(
|
|
128
|
+
caller({
|
|
129
|
+
issuer: "https://issuer.example",
|
|
130
|
+
rawToken: jwtWith({ iss: "https://issuer.example", sub: "carol" }),
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("is false for every other class, a composition's own included", () => {
|
|
137
|
+
for (const callerClass of [
|
|
138
|
+
"machine",
|
|
139
|
+
"runner",
|
|
140
|
+
"internal",
|
|
141
|
+
"guest",
|
|
142
|
+
"channel",
|
|
143
|
+
"schedule",
|
|
144
|
+
"sandbox",
|
|
145
|
+
]) {
|
|
146
|
+
expect(
|
|
147
|
+
isFirstPartyHumanOperator(caller({ callerClass })),
|
|
148
|
+
callerClass,
|
|
149
|
+
).toBe(false);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("is false for a user the server composed a request for (the schedule fire)", () => {
|
|
154
|
+
expect(
|
|
155
|
+
isFirstPartyHumanOperator(
|
|
156
|
+
caller({ callerClass: "user", origin: "in-process" }),
|
|
157
|
+
),
|
|
158
|
+
).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("is false for a PlatformClient user token, and true when another issuer stamps the claim", () => {
|
|
162
|
+
expect(
|
|
163
|
+
isFirstPartyHumanOperator(
|
|
164
|
+
caller({
|
|
165
|
+
issuer: "stigmer",
|
|
166
|
+
rawToken: jwtWith({
|
|
167
|
+
iss: "stigmer",
|
|
168
|
+
sub: "ida_x",
|
|
169
|
+
platform_client_id: "pc_1",
|
|
170
|
+
}),
|
|
171
|
+
}),
|
|
172
|
+
),
|
|
173
|
+
).toBe(false);
|
|
174
|
+
expect(
|
|
175
|
+
isFirstPartyHumanOperator(
|
|
176
|
+
caller({
|
|
177
|
+
issuer: "https://issuer.example",
|
|
178
|
+
rawToken: jwtWith({
|
|
179
|
+
iss: "https://issuer.example",
|
|
180
|
+
sub: "ida_x",
|
|
181
|
+
platform_client_id: "pc_1",
|
|
182
|
+
}),
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* - session.list (restrict verb, no org intersection — census lane 1),
|
|
8
8
|
* - apikey.findAll (restrict verb through the direct-read tail, lane 9),
|
|
9
|
+
* - channelapp.listByOrg (restrict verb behind the domain's own org
|
|
10
|
+
* predicate: the scope is offered the request org's rows only, the
|
|
11
|
+
* listByOrg family's shape; stigmer/stigmer#1384),
|
|
9
12
|
* - workflowexecution.list (restrict verb + the org arm: non-blank
|
|
10
13
|
* narrows, blank spans orgs — lane 6),
|
|
11
14
|
* - activity.listRecentActivity (enumeration verb, two kinds — lane 22),
|
|
@@ -40,6 +43,8 @@ import { ActivityQueryController } from "@stigmer/protos/ai/stigmer/activity/v1/
|
|
|
40
43
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
41
44
|
import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
42
45
|
import { AgentExecutionQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/query_pb";
|
|
46
|
+
import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
|
|
47
|
+
import { ChannelAppQueryController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/query_pb";
|
|
43
48
|
import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
44
49
|
import { SessionQueryController } from "@stigmer/protos/ai/stigmer/agentic/session/v1/query_pb";
|
|
45
50
|
import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
|
|
@@ -76,11 +81,13 @@ describe("list read scope (composed server, fake scope)", () => {
|
|
|
76
81
|
* The switchable fake: "keep" narrows to `allowed` (both verbs, the
|
|
77
82
|
* cloud driver's shape), "throw" simulates the FGA outage. Kinds seen
|
|
78
83
|
* are recorded so the enumeration-lane assertions can verify which
|
|
79
|
-
* kind each consumer asked for
|
|
84
|
+
* kind each consumer asked for, and the ids each restrict call was
|
|
85
|
+
* offered so a lane can show which rows reached the scope.
|
|
80
86
|
*/
|
|
81
87
|
let mode: "keep" | "throw" = "keep";
|
|
82
88
|
let allowed: ReadonlySet<string> = new Set();
|
|
83
89
|
const seenKinds: ApiResourceKind[] = [];
|
|
90
|
+
const offeredIds: string[][] = [];
|
|
84
91
|
const fakeScope: ListReadScope = {
|
|
85
92
|
authorizedResourceIds(_caller, kind) {
|
|
86
93
|
seenKinds.push(kind);
|
|
@@ -91,6 +98,7 @@ describe("list read scope (composed server, fake scope)", () => {
|
|
|
91
98
|
},
|
|
92
99
|
restrictListEntries(_caller, kind, entries) {
|
|
93
100
|
seenKinds.push(kind);
|
|
101
|
+
offeredIds.push(entries.map((e) => e.id));
|
|
94
102
|
if (mode === "throw") {
|
|
95
103
|
return Promise.reject(new Error("fga unreachable"));
|
|
96
104
|
}
|
|
@@ -202,6 +210,22 @@ describe("list read scope (composed server, fake scope)", () => {
|
|
|
202
210
|
}),
|
|
203
211
|
);
|
|
204
212
|
}
|
|
213
|
+
for (const [id, org] of [
|
|
214
|
+
["chap_mine", "acme"],
|
|
215
|
+
["chap_foreign", "acme"],
|
|
216
|
+
["chap_other_org", "rival"],
|
|
217
|
+
] as const) {
|
|
218
|
+
await server.store.saveResource(
|
|
219
|
+
ApiResourceKind.channel_app,
|
|
220
|
+
id,
|
|
221
|
+
ChannelAppSchema,
|
|
222
|
+
create(ChannelAppSchema, {
|
|
223
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
224
|
+
kind: "ChannelApp",
|
|
225
|
+
metadata: { id, name: id, org },
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
205
229
|
// The search lane: resources plus their index rows (list mode).
|
|
206
230
|
for (const id of ["agt_mine", "agt_foreign"]) {
|
|
207
231
|
await server.store.saveResource(
|
|
@@ -233,6 +257,7 @@ describe("list read scope (composed server, fake scope)", () => {
|
|
|
233
257
|
beforeEach(() => {
|
|
234
258
|
mode = "keep";
|
|
235
259
|
seenKinds.length = 0;
|
|
260
|
+
offeredIds.length = 0;
|
|
236
261
|
});
|
|
237
262
|
|
|
238
263
|
it("session.list narrows to the scope's kept ids (org not consulted — lane 1)", async () => {
|
|
@@ -249,6 +274,19 @@ describe("list read scope (composed server, fake scope)", () => {
|
|
|
249
274
|
expect(keys.entries.map((k) => k.metadata?.id)).toEqual(["key_mine"]);
|
|
250
275
|
});
|
|
251
276
|
|
|
277
|
+
it("channelapp.listByOrg narrows the request org's apps to the scope's kept ids; no other org's app is offered (stigmer/stigmer#1384)", async () => {
|
|
278
|
+
// Allowing the other org's app too proves the org predicate is the
|
|
279
|
+
// domain's own, applied before the scope ever sees a row.
|
|
280
|
+
allowed = new Set(["chap_mine", "chap_other_org"]);
|
|
281
|
+
const query = createClient(ChannelAppQueryController, transport);
|
|
282
|
+
const list = await query.listByOrg({ org: "acme" });
|
|
283
|
+
expect(list.entries.map((a) => a.metadata?.id)).toEqual(["chap_mine"]);
|
|
284
|
+
expect(seenKinds).toEqual([ApiResourceKind.channel_app]);
|
|
285
|
+
expect(offeredIds.map((ids) => [...ids].sort())).toEqual([
|
|
286
|
+
["chap_foreign", "chap_mine"],
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
|
|
252
290
|
it("workflowexecution.list: non-blank org narrows AFTER the scope; blank org spans orgs (lane 6)", async () => {
|
|
253
291
|
allowed = new Set(["wfe_mine", "wfe_other_org"]);
|
|
254
292
|
const query = createClient(WorkflowExecutionQueryController, transport);
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* HandlerContext.values (never ambient state — the every-dependency-
|
|
13
13
|
* explicit doctrine).
|
|
14
14
|
*/
|
|
15
|
+
import { USER_TOKEN_CLAIMS } from "../domain/platformclient/constants.js";
|
|
16
|
+
import {
|
|
17
|
+
decodeVerifiedPlatformTokenPayload,
|
|
18
|
+
stringClaim,
|
|
19
|
+
} from "../platformtoken/envelope.js";
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* The caller-class discriminant. OSS knows user / machine / runner plus
|
|
@@ -152,3 +157,45 @@ export function isPlatformPipelineCaller(caller: CallerIdentity): boolean {
|
|
|
152
157
|
export function isServerComposedRequest(caller: CallerIdentity): boolean {
|
|
153
158
|
return caller.callerClass === "internal" || caller.origin === "in-process";
|
|
154
159
|
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Whether `caller` is a FIRST-PARTY HUMAN OPERATOR: a person at the
|
|
163
|
+
* console, the CLI or an SDK, speaking for themselves on the wire — the
|
|
164
|
+
* TS rendering of the Java RequestCallerIdentity.isFirstPartyHumanOperator
|
|
165
|
+
* (not machine, not impersonated, no token_type, not a PlatformClient user
|
|
166
|
+
* token). Stated once for every consumer that must tell the person from a
|
|
167
|
+
* lane acting near them: memory recall and the person's declared
|
|
168
|
+
* preferences on execution create (domain/agentexecution/create-steps.ts).
|
|
169
|
+
*
|
|
170
|
+
* An ALLOW-list, so a lane this module has never heard of is excluded by
|
|
171
|
+
* construction: the class must be `user` (every human verifier stamps it —
|
|
172
|
+
* OIDC, API key, the cloud's direct and federated logins; runners, the
|
|
173
|
+
* cloud's token lanes and the platform's own pipelines each carry their
|
|
174
|
+
* own class), the request must come from the wire (the open-source
|
|
175
|
+
* schedule fire acts as its creator with class `user` but rides the
|
|
176
|
+
* in-process transport, which is exactly what `isServerComposedRequest`
|
|
177
|
+
* reads), and the bearer must not be a PlatformClient user token (a
|
|
178
|
+
* person, but spoken for by a third-party client — DD-002 D4 as amended).
|
|
179
|
+
*/
|
|
180
|
+
export function isFirstPartyHumanOperator(caller: CallerIdentity): boolean {
|
|
181
|
+
return (
|
|
182
|
+
caller.callerClass === "user" &&
|
|
183
|
+
!isServerComposedRequest(caller) &&
|
|
184
|
+
!carriesPlatformClientClaim(caller.rawToken)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether the bearer is a platform token naming a PlatformClient. Anything
|
|
190
|
+
* that is not a platform token (no token, an API key, another issuer's
|
|
191
|
+
* JWT) is not one, whatever claims it carries (stigmer#1312). Read from
|
|
192
|
+
* the raw token, never from `platformClientId`, which is audit provenance
|
|
193
|
+
* set by one verifier and not a guard vocabulary (see its field doc).
|
|
194
|
+
*/
|
|
195
|
+
export function carriesPlatformClientClaim(rawToken: string): boolean {
|
|
196
|
+
const payload = decodeVerifiedPlatformTokenPayload(rawToken);
|
|
197
|
+
return (
|
|
198
|
+
payload !== undefined &&
|
|
199
|
+
stringClaim(payload, USER_TOKEN_CLAIMS.platformClientId) !== undefined
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -42,14 +42,12 @@
|
|
|
42
42
|
import type { DescMessage } from "@bufbuild/protobuf";
|
|
43
43
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
44
44
|
|
|
45
|
-
import {
|
|
46
|
-
|
|
45
|
+
import {
|
|
46
|
+
carriesPlatformClientClaim,
|
|
47
|
+
isServerComposedRequest,
|
|
48
|
+
} from "../../extensions/identity.js";
|
|
47
49
|
import type { PipelineStep } from "../pipeline.js";
|
|
48
50
|
import type { RequestContext } from "../request-context.js";
|
|
49
|
-
import {
|
|
50
|
-
decodeVerifiedPlatformTokenPayload,
|
|
51
|
-
stringClaim,
|
|
52
|
-
} from "../../platformtoken/envelope.js";
|
|
53
51
|
import type { RunnerCredentialProvider } from "../../runnerauth/runner-credential-provider.js";
|
|
54
52
|
import { metadataOf } from "./shapes.js";
|
|
55
53
|
|
|
@@ -147,16 +145,3 @@ export function memoryCaptureCredentialOf<Desc extends DescMessage>(
|
|
|
147
145
|
}
|
|
148
146
|
return undefined;
|
|
149
147
|
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Whether the bearer is a platform token naming a PlatformClient. Anything
|
|
153
|
-
* that is not a platform token (no token, an API key, another issuer's
|
|
154
|
-
* JWT) is not one, whatever claims it carries.
|
|
155
|
-
*/
|
|
156
|
-
function carriesPlatformClientClaim(rawToken: string): boolean {
|
|
157
|
-
const payload = decodeVerifiedPlatformTokenPayload(rawToken);
|
|
158
|
-
return (
|
|
159
|
-
payload !== undefined &&
|
|
160
|
-
stringClaim(payload, USER_TOKEN_CLAIMS.platformClientId) !== undefined
|
|
161
|
-
);
|
|
162
|
-
}
|