@stigmer/server 3.21.0 → 3.22.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/evaluator.d.ts +7 -1
- package/dist/authorization/evaluator.d.ts.map +1 -1
- package/dist/authorization/evaluator.js.map +1 -1
- package/dist/authorization/list-read-scope.d.ts.map +1 -1
- package/dist/authorization/list-read-scope.js +19 -11
- package/dist/authorization/list-read-scope.js.map +1 -1
- package/dist/authorization/model/organization.d.ts.map +1 -1
- package/dist/authorization/model/organization.js +2 -0
- package/dist/authorization/model/organization.js.map +1 -1
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js +2 -0
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentchannel/controller.d.ts.map +1 -1
- package/dist/domain/agentchannel/controller.js +6 -1
- package/dist/domain/agentchannel/controller.js.map +1 -1
- package/dist/domain/agentchannel/steps.d.ts +19 -0
- package/dist/domain/agentchannel/steps.d.ts.map +1 -1
- package/dist/domain/agentchannel/steps.js +32 -0
- package/dist/domain/agentchannel/steps.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +5 -5
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js +6 -1
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentinstance/steps.d.ts +43 -8
- package/dist/domain/agentinstance/steps.d.ts.map +1 -1
- package/dist/domain/agentinstance/steps.js +71 -8
- package/dist/domain/agentinstance/steps.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +7 -1
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/agentshare/steps.d.ts +58 -0
- package/dist/domain/agentshare/steps.d.ts.map +1 -1
- package/dist/domain/agentshare/steps.js +109 -4
- package/dist/domain/agentshare/steps.js.map +1 -1
- package/dist/domain/artifact/controller.js +21 -25
- package/dist/domain/artifact/controller.js.map +1 -1
- package/dist/domain/channelapp/controller.d.ts.map +1 -1
- package/dist/domain/channelapp/controller.js +2 -0
- package/dist/domain/channelapp/controller.js.map +1 -1
- package/dist/domain/environment/controller.d.ts.map +1 -1
- package/dist/domain/environment/controller.js +2 -0
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/executioncontext/controller.d.ts.map +1 -1
- package/dist/domain/executioncontext/controller.js +2 -0
- package/dist/domain/executioncontext/controller.js.map +1 -1
- package/dist/domain/executioncontext/steps.d.ts.map +1 -1
- package/dist/domain/executioncontext/steps.js +2 -1
- package/dist/domain/executioncontext/steps.js.map +1 -1
- package/dist/domain/mcpserver/connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/connect.js +8 -7
- package/dist/domain/mcpserver/connect.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +2 -0
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/mcpserver/start-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/start-connect.js +8 -7
- package/dist/domain/mcpserver/start-connect.js.map +1 -1
- package/dist/domain/memory/controller.d.ts.map +1 -1
- package/dist/domain/memory/controller.js +6 -1
- package/dist/domain/memory/controller.js.map +1 -1
- package/dist/domain/memory/steps.d.ts +23 -0
- package/dist/domain/memory/steps.d.ts.map +1 -1
- package/dist/domain/memory/steps.js +35 -0
- package/dist/domain/memory/steps.js.map +1 -1
- package/dist/domain/oauthapp/controller.d.ts.map +1 -1
- package/dist/domain/oauthapp/controller.js +2 -0
- package/dist/domain/oauthapp/controller.js.map +1 -1
- package/dist/domain/plugin/controller.d.ts.map +1 -1
- package/dist/domain/plugin/controller.js +5 -24
- package/dist/domain/plugin/controller.js.map +1 -1
- package/dist/domain/schedule/controller.d.ts.map +1 -1
- package/dist/domain/schedule/controller.js +7 -1
- package/dist/domain/schedule/controller.js.map +1 -1
- package/dist/domain/schedule/steps.d.ts +19 -0
- package/dist/domain/schedule/steps.d.ts.map +1 -1
- package/dist/domain/schedule/steps.js +32 -0
- package/dist/domain/schedule/steps.js.map +1 -1
- package/dist/domain/session/steps.d.ts +8 -9
- package/dist/domain/session/steps.d.ts.map +1 -1
- package/dist/domain/session/steps.js +17 -21
- package/dist/domain/session/steps.js.map +1 -1
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +8 -17
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/workflow/controller.d.ts.map +1 -1
- package/dist/domain/workflow/controller.js +30 -107
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflow/version-resolution.d.ts +5 -0
- package/dist/domain/workflow/version-resolution.d.ts.map +1 -0
- package/dist/domain/workflow/version-resolution.js +113 -0
- package/dist/domain/workflow/version-resolution.js.map +1 -0
- package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
- package/dist/domain/workflowinstance/controller.js +6 -1
- package/dist/domain/workflowinstance/controller.js.map +1 -1
- package/dist/domain/workflowinstance/steps.d.ts +20 -0
- package/dist/domain/workflowinstance/steps.d.ts.map +1 -1
- package/dist/domain/workflowinstance/steps.js +39 -0
- package/dist/domain/workflowinstance/steps.js.map +1 -1
- package/dist/extensions/identity.d.ts +19 -0
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js +22 -3
- package/dist/extensions/identity.js.map +1 -1
- package/dist/extensions/list-read-scope.d.ts +41 -7
- package/dist/extensions/list-read-scope.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.js +13 -2
- package/dist/extensions/list-read-scope.js.map +1 -1
- package/dist/pipeline/steps/authorize-resolved-target.d.ts +88 -0
- package/dist/pipeline/steps/authorize-resolved-target.d.ts.map +1 -0
- package/dist/pipeline/steps/authorize-resolved-target.js +36 -0
- package/dist/pipeline/steps/authorize-resolved-target.js.map +1 -0
- package/dist/pipeline/steps/authorize.d.ts +27 -0
- package/dist/pipeline/steps/authorize.d.ts.map +1 -1
- package/dist/pipeline/steps/authorize.js +49 -10
- package/dist/pipeline/steps/authorize.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +2 -1
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/dist/pipeline/steps/guard-reserved-labels.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-reserved-labels.js +6 -7
- package/dist/pipeline/steps/guard-reserved-labels.js.map +1 -1
- package/dist/pipeline/steps/helpers.d.ts +9 -5
- package/dist/pipeline/steps/helpers.d.ts.map +1 -1
- package/dist/pipeline/steps/helpers.js +11 -15
- package/dist/pipeline/steps/helpers.js.map +1 -1
- package/dist/pipeline/steps/version-history.d.ts +11 -0
- package/dist/pipeline/steps/version-history.d.ts.map +1 -1
- package/dist/pipeline/steps/version-history.js +26 -0
- package/dist/pipeline/steps/version-history.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/__tests__/list-read-scope.test.ts +23 -12
- package/src/authorization/__tests__/skip-lane-inventory.test.ts +160 -0
- package/src/authorization/__tests__/wire-permissions.test.ts +5 -5
- package/src/authorization/evaluator.ts +7 -0
- package/src/authorization/list-read-scope.ts +22 -11
- package/src/authorization/model/__tests__/registry.test.ts +2 -0
- package/src/authorization/model/organization.ts +2 -0
- package/src/domain/agent/controller.ts +10 -0
- package/src/domain/agentchannel/__tests__/create-authorization.test.ts +58 -0
- package/src/domain/agentchannel/controller.ts +19 -0
- package/src/domain/agentchannel/steps.ts +40 -0
- package/src/domain/agentinstance/__tests__/create-authorization.composed.test.ts +243 -0
- package/src/domain/agentinstance/__tests__/create-authorization.test.ts +144 -0
- package/src/domain/agentinstance/controller.ts +24 -5
- package/src/domain/agentinstance/steps.ts +82 -8
- package/src/domain/agentshare/__tests__/create-authorization.test.ts +86 -0
- package/src/domain/agentshare/__tests__/member-audience.test.ts +113 -0
- package/src/domain/agentshare/controller.ts +21 -0
- package/src/domain/agentshare/steps.ts +181 -15
- package/src/domain/artifact/controller.ts +18 -22
- package/src/domain/channelapp/controller.ts +10 -0
- package/src/domain/environment/controller.ts +10 -0
- package/src/domain/executioncontext/__tests__/executioncontext.test.ts +1 -0
- package/src/domain/executioncontext/controller.ts +10 -0
- package/src/domain/executioncontext/steps.ts +2 -1
- package/src/domain/mcpserver/connect.ts +11 -10
- package/src/domain/mcpserver/controller.ts +13 -1
- package/src/domain/mcpserver/start-connect.ts +17 -11
- package/src/domain/memory/__tests__/create-authorization.test.ts +59 -0
- package/src/domain/memory/controller.ts +11 -0
- package/src/domain/memory/steps.ts +41 -0
- package/src/domain/oauthapp/controller.ts +10 -0
- package/src/domain/plugin/controller.ts +22 -36
- package/src/domain/schedule/__tests__/create-authorization.test.ts +58 -0
- package/src/domain/schedule/controller.ts +20 -0
- package/src/domain/schedule/steps.ts +45 -2
- package/src/domain/session/steps.ts +24 -32
- package/src/domain/skill/controller.ts +24 -23
- package/src/domain/workflow/controller.ts +58 -145
- package/src/domain/workflow/version-resolution.ts +166 -0
- package/src/domain/workflowinstance/__tests__/create-authorization.test.ts +109 -0
- package/src/domain/workflowinstance/controller.ts +19 -0
- package/src/domain/workflowinstance/steps.ts +49 -1
- package/src/extensions/__tests__/composed-support.ts +38 -4
- package/src/extensions/__tests__/identity.test.ts +44 -1
- package/src/extensions/__tests__/list-read-scope-composed.test.ts +87 -1
- package/src/extensions/__tests__/list-read-scope.test.ts +63 -0
- package/src/extensions/identity.ts +23 -5
- package/src/extensions/list-read-scope.ts +47 -7
- package/src/pipeline/steps/__tests__/authorize-resolved-target.test.ts +285 -0
- package/src/pipeline/steps/authorize-resolved-target.ts +129 -0
- package/src/pipeline/steps/authorize.ts +70 -13
- package/src/pipeline/steps/guard-memory-capture.ts +2 -1
- package/src/pipeline/steps/guard-reserved-labels.ts +6 -9
- package/src/pipeline/steps/helpers.ts +11 -15
- package/src/pipeline/steps/version-history.ts +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.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",
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
"@connectrpc/connect-node": "^2.0.0",
|
|
63
63
|
"@kubernetes/client-node": "2.0.0",
|
|
64
64
|
"@opentelemetry/api": "^1.9.0",
|
|
65
|
-
"@stigmer/outbound": "3.
|
|
66
|
-
"@stigmer/plugin-package": "3.
|
|
67
|
-
"@stigmer/protos": "3.
|
|
68
|
-
"@stigmer/temporal-codecs": "3.
|
|
69
|
-
"@stigmer/zip-structure": "3.
|
|
65
|
+
"@stigmer/outbound": "3.22.0",
|
|
66
|
+
"@stigmer/plugin-package": "3.22.0",
|
|
67
|
+
"@stigmer/protos": "3.22.0",
|
|
68
|
+
"@stigmer/temporal-codecs": "3.22.0",
|
|
69
|
+
"@stigmer/zip-structure": "3.22.0",
|
|
70
70
|
"@temporalio/activity": "1.16.2",
|
|
71
71
|
"@temporalio/client": "1.16.2",
|
|
72
72
|
"@temporalio/common": "1.16.2",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
* org-observable reaches the organization's viewers; a memory with no
|
|
18
18
|
* subject is nobody's; an instance reaches whoever reads its blueprint
|
|
19
19
|
* only when it is the default. Then the contract: a scope only narrows
|
|
20
|
-
* and never reorders; the `internal` class is the in-process skip
|
|
20
|
+
* and never reorders; the `internal` class is the in-process skip on the
|
|
21
|
+
* enumeration verb and a loud consumer bug on the restrict verb (the
|
|
22
|
+
* shared helper answers the class before any driver, stigmer#1207); an
|
|
21
23
|
* unprovisioned subject is their raw subject (the 3.14 verifiers' stamp); a
|
|
22
24
|
* fault throws and is never an empty answer; an undeclared kind is a
|
|
23
25
|
* consumer bug, loud; the enumeration verb equals the restrict verb over
|
|
@@ -583,17 +585,7 @@ describe.each(driverFixtures(SEEDED_KINDS))(
|
|
|
583
585
|
expect(accountReads).toBe(0);
|
|
584
586
|
});
|
|
585
587
|
|
|
586
|
-
it("the `internal` class — the server acting as itself —
|
|
587
|
-
const offered = rowsOf(ApiResourceKind.agent).map((row) =>
|
|
588
|
-
entryOf(ApiResourceKind.agent, row),
|
|
589
|
-
);
|
|
590
|
-
expect([
|
|
591
|
-
...(await scope().restrictListEntries(
|
|
592
|
-
INTERNAL,
|
|
593
|
-
ApiResourceKind.agent,
|
|
594
|
-
offered,
|
|
595
|
-
)),
|
|
596
|
-
]).toEqual(offered.map((entry) => entry.id));
|
|
588
|
+
it("the `internal` class on the enumeration verb — the server acting as itself — is every id of the kind, with no account even asked for (the in-process skip; no helper stands before this verb)", async () => {
|
|
597
589
|
expect(
|
|
598
590
|
[
|
|
599
591
|
...(await scope().authorizedResourceIds(
|
|
@@ -605,6 +597,25 @@ describe.each(driverFixtures(SEEDED_KINDS))(
|
|
|
605
597
|
expect(accountReads).toBe(0);
|
|
606
598
|
});
|
|
607
599
|
|
|
600
|
+
it("the `internal` class on the restrict verb is a consumer bug, loud: the shared helper answers that class before any driver, so a driver offered it was reached around the idiom", async () => {
|
|
601
|
+
const offered = rowsOf(ApiResourceKind.agent).map((row) =>
|
|
602
|
+
entryOf(ApiResourceKind.agent, row),
|
|
603
|
+
);
|
|
604
|
+
const refusal = await scope()
|
|
605
|
+
.restrictListEntries(INTERNAL, ApiResourceKind.agent, offered)
|
|
606
|
+
.then(
|
|
607
|
+
() => undefined,
|
|
608
|
+
(error: unknown) => error,
|
|
609
|
+
);
|
|
610
|
+
expect(refusal).toBeInstanceOf(AuthorizationEvaluationError);
|
|
611
|
+
expect((refusal as AuthorizationEvaluationError).reason).toBe(
|
|
612
|
+
"internal-caller-offered",
|
|
613
|
+
);
|
|
614
|
+
// Refused before any read: the server was never evaluated as a person.
|
|
615
|
+
expect(accountReads).toBe(0);
|
|
616
|
+
expect(storeReads).toEqual({ rows: 0, scans: 0 });
|
|
617
|
+
});
|
|
618
|
+
|
|
608
619
|
it("an unprovisioned subject is their raw subject and nothing else: the 3.14-stamped row is theirs, the organization's rows are not", async () => {
|
|
609
620
|
// `agt_legacy` is stamped with LEGACY's raw subject; a caller
|
|
610
621
|
// who IS that subject and holds no account reaches it through
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the coverage inventory's own rule for `is_skip_authorization` lanes
|
|
3
|
+
* (docs/authorization-coverage.md, "How to read the tables"): a skip lane
|
|
4
|
+
* reaches no Authorizer in either edition, so what guards it is whatever
|
|
5
|
+
* its Handler cell names — and a cell that names nothing is a lane open to
|
|
6
|
+
* every authenticated caller that nobody has said so about. Two things are
|
|
7
|
+
* held here, so the doc cannot drift from the server it describes:
|
|
8
|
+
*
|
|
9
|
+
* 1. The set of skip-annotated methods the open-source server SERVES
|
|
10
|
+
* (the empty composition's routes replayed into a recorder, a method
|
|
11
|
+
* counted only when its service's implementation map carries it — a
|
|
12
|
+
* partially implemented service lists more on its descriptor than it
|
|
13
|
+
* serves) equals the set of skip rows in the doc, service by method.
|
|
14
|
+
* A new skip lane without a row, or a row for a lane that is no
|
|
15
|
+
* longer a skip, fails here.
|
|
16
|
+
* 2. Every skip row's Handler cell carries one of the three markers the
|
|
17
|
+
* doc defines — `guard: <Name>`, `driver: <Seam>`,
|
|
18
|
+
* `by design: <reason>` — and every `guard:` names a pipeline step or
|
|
19
|
+
* a handler's guard function that exists by that name in `src/`
|
|
20
|
+
* outside the tests, so a renamed or removed guard cannot leave the
|
|
21
|
+
* doc pointing at nothing.
|
|
22
|
+
*
|
|
23
|
+
* The rule lives in the doc; this test only enforces it. The doc is the
|
|
24
|
+
* one inventory (the package guide mandates updating it with every
|
|
25
|
+
* annotation change), and a second list here would be the drift this
|
|
26
|
+
* test exists to prevent.
|
|
27
|
+
*/
|
|
28
|
+
import {
|
|
29
|
+
mkdtempSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
readdirSync,
|
|
32
|
+
rmSync,
|
|
33
|
+
statSync,
|
|
34
|
+
} from "node:fs";
|
|
35
|
+
import { tmpdir } from "node:os";
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
import { fileURLToPath } from "node:url";
|
|
38
|
+
|
|
39
|
+
import { getOption } from "@bufbuild/protobuf";
|
|
40
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
41
|
+
|
|
42
|
+
import { is_skip_authorization } from "@stigmer/protos/ai/stigmer/commons/rpc/method_options_pb";
|
|
43
|
+
|
|
44
|
+
import { loadConfig } from "../../boot/config.js";
|
|
45
|
+
import { composeServer } from "../../boot/compose.js";
|
|
46
|
+
import type { ComposedServer } from "../../boot/compose.js";
|
|
47
|
+
import {
|
|
48
|
+
baseConfig,
|
|
49
|
+
servedMethods,
|
|
50
|
+
silentLogger,
|
|
51
|
+
} from "../../extensions/__tests__/composed-support.js";
|
|
52
|
+
|
|
53
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
const PACKAGE_ROOT = path.resolve(HERE, "../../..");
|
|
55
|
+
const COVERAGE_DOC = path.join(PACKAGE_ROOT, "docs/authorization-coverage.md");
|
|
56
|
+
const SRC = path.join(PACKAGE_ROOT, "src");
|
|
57
|
+
|
|
58
|
+
/** A skip row: `| Service.method | is_skip_authorization | <handler cell> |`. */
|
|
59
|
+
const SKIP_ROW =
|
|
60
|
+
/^\| ([A-Za-z]+)\.([A-Za-z]+) \| is_skip_authorization \| (.*) \|$/;
|
|
61
|
+
|
|
62
|
+
const MARKERS = ["guard:", "driver:", "by design:"] as const;
|
|
63
|
+
|
|
64
|
+
interface SkipRow {
|
|
65
|
+
readonly method: string;
|
|
66
|
+
readonly handler: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function skipRowsOf(doc: string): SkipRow[] {
|
|
70
|
+
const rows: SkipRow[] = [];
|
|
71
|
+
for (const line of doc.split("\n")) {
|
|
72
|
+
const m = SKIP_ROW.exec(line);
|
|
73
|
+
if (m !== null) {
|
|
74
|
+
rows.push({ method: `${m[1]}.${m[2]}`, handler: m[3] ?? "" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return rows;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Every `guard: <Name>` a handler cell names. */
|
|
81
|
+
function guardsOf(handler: string): string[] {
|
|
82
|
+
return [...handler.matchAll(/guard: ([A-Za-z]+)/g)].map((m) => m[1] ?? "");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every non-test `.ts` source under `src/`, read once. */
|
|
86
|
+
function sourceText(): string {
|
|
87
|
+
const parts: string[] = [];
|
|
88
|
+
const walk = (dir: string): void => {
|
|
89
|
+
for (const entry of readdirSync(dir)) {
|
|
90
|
+
const full = path.join(dir, entry);
|
|
91
|
+
if (statSync(full).isDirectory()) {
|
|
92
|
+
if (entry !== "__tests__") {
|
|
93
|
+
walk(full);
|
|
94
|
+
}
|
|
95
|
+
} else if (full.endsWith(".ts") && !full.endsWith(".test.ts")) {
|
|
96
|
+
parts.push(readFileSync(full, "utf8"));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
walk(SRC);
|
|
101
|
+
return parts.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe("the skip-lane inventory (docs/authorization-coverage.md) is true to the served server", () => {
|
|
105
|
+
let dir: string;
|
|
106
|
+
let server: ComposedServer;
|
|
107
|
+
|
|
108
|
+
beforeAll(async () => {
|
|
109
|
+
dir = mkdtempSync(path.join(tmpdir(), "skip-lane-inventory-test-"));
|
|
110
|
+
server = await composeServer({
|
|
111
|
+
config: loadConfig(baseConfig(dir)),
|
|
112
|
+
logger: silentLogger,
|
|
113
|
+
portOverride: 0,
|
|
114
|
+
host: "127.0.0.1",
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterAll(async () => {
|
|
119
|
+
await server.shutdown();
|
|
120
|
+
rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("the doc's skip rows are exactly the skip-annotated methods the server serves", () => {
|
|
124
|
+
const served = new Set<string>();
|
|
125
|
+
for (const method of servedMethods(server.routes)) {
|
|
126
|
+
if (getOption(method, is_skip_authorization)) {
|
|
127
|
+
const serviceName = method.parent.typeName.split(".").at(-1) ?? "";
|
|
128
|
+
served.add(`${serviceName}.${method.name}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const documented = new Set(
|
|
132
|
+
skipRowsOf(readFileSync(COVERAGE_DOC, "utf8")).map((r) => r.method),
|
|
133
|
+
);
|
|
134
|
+
expect([...served].sort()).toEqual([...documented].sort());
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("every skip row names its guard, its driver or says the lane is open by design", () => {
|
|
138
|
+
const bare = skipRowsOf(readFileSync(COVERAGE_DOC, "utf8"))
|
|
139
|
+
.filter((r) => !MARKERS.some((marker) => r.handler.includes(marker)))
|
|
140
|
+
.map((r) => r.method);
|
|
141
|
+
expect(bare).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("every guard a skip row names is a step or a guard function that exists by that name in src/", () => {
|
|
145
|
+
const source = sourceText();
|
|
146
|
+
const dangling: string[] = [];
|
|
147
|
+
for (const row of skipRowsOf(readFileSync(COVERAGE_DOC, "utf8"))) {
|
|
148
|
+
for (const guard of guardsOf(row.handler)) {
|
|
149
|
+
// A step is registered by its quoted name; a guard function by its
|
|
150
|
+
// declaration.
|
|
151
|
+
const isStep = source.includes(`"${guard}"`);
|
|
152
|
+
const isFunction = new RegExp(`function ${guard}\\(`).test(source);
|
|
153
|
+
if (!isStep && !isFunction) {
|
|
154
|
+
dangling.push(`${row.method} names guard ${guard}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
expect(dangling).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -117,12 +117,11 @@ describe("every static (kind, permission) the served RPCs ask about is a declare
|
|
|
117
117
|
expect(undeclared).toEqual([]);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
it("asks about every served kind the model declares except the
|
|
120
|
+
it("asks about every served kind the model declares except the one no RPC targets statically", () => {
|
|
121
121
|
// iam_policy's RPCs target the policy's RESOURCE (`resource_kind_path`
|
|
122
|
-
// or the load-then-authorize `get`)
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
// asked about by at least one static annotation.
|
|
122
|
+
// or the load-then-authorize `get`). Every other declared kind is asked
|
|
123
|
+
// about by at least one static annotation — execution_context through
|
|
124
|
+
// its `get` and `delete`; its runner-token lane stays a skip.
|
|
126
125
|
const asked = new Set(wireQuestions().map((q) => kindEnumName(q.kind)));
|
|
127
126
|
expect([...asked].sort()).toEqual(
|
|
128
127
|
[
|
|
@@ -135,6 +134,7 @@ describe("every static (kind, permission) the served RPCs ask about is a declare
|
|
|
135
134
|
"artifact",
|
|
136
135
|
"channel_app",
|
|
137
136
|
"environment",
|
|
137
|
+
"execution_context",
|
|
138
138
|
"identity_account",
|
|
139
139
|
"mcp_server",
|
|
140
140
|
"memory",
|
|
@@ -67,8 +67,15 @@ import {
|
|
|
67
67
|
*/
|
|
68
68
|
export const MAX_RESOLUTION_DEPTH = 25;
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The faults an evaluation can raise. Two are consumer bugs by the seam
|
|
72
|
+
* contracts (a kind the model does not declare; the `internal` class
|
|
73
|
+
* offered to the list scope's restrict verb, which the shared helper
|
|
74
|
+
* answers before any driver); the rest are the model's own.
|
|
75
|
+
*/
|
|
70
76
|
export type EvaluationFault =
|
|
71
77
|
| "undeclared-target-kind"
|
|
78
|
+
| "internal-caller-offered"
|
|
72
79
|
| "resolution-depth-exceeded"
|
|
73
80
|
| "resolution-cycle"
|
|
74
81
|
| "unknown-condition";
|
|
@@ -38,17 +38,25 @@
|
|
|
38
38
|
* lane can show it either.
|
|
39
39
|
*
|
|
40
40
|
* The `internal` class — the server acting as itself over the in-process
|
|
41
|
-
* transport —
|
|
42
|
-
* the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* transport — is answered by the seam, not here: `restrictListByReadScope`
|
|
42
|
+
* returns the org-narrowed rows for the class before any driver is asked
|
|
43
|
+
* (extensions/list-read-scope.ts, the header's contract line), the
|
|
44
|
+
* Authorize step's trust-domain rule applied to a list answer. So this
|
|
45
|
+
* driver's `restrictListEntries` REFUSES the class (`internal-caller-offered`):
|
|
46
|
+
* being offered it means a caller reached the driver around the one
|
|
47
|
+
* consumption idiom, and evaluating the server as a person would return
|
|
48
|
+
* a quiet short list — the failure shape stigmer#1207 fixed. The
|
|
49
|
+
* enumeration verb has no helper in front of it and keeps its own arm:
|
|
50
|
+
* every id of the kind, as the built-in directory answers the class with
|
|
51
|
+
* every organization. No in-process edge reaches an enumeration lane
|
|
52
|
+
* today; the arm is pinned so a future one meets the rule.
|
|
46
53
|
*
|
|
47
54
|
* Faults THROW and are never an empty answer (the seam: an empty set means
|
|
48
55
|
* "authorized to see nothing"; an outage is the pipeline's sanitized
|
|
49
|
-
* INTERNAL). A kind the model does not declare
|
|
50
|
-
* seam's contract and faults through the
|
|
51
|
-
* declares exactly the open-source tier, so no
|
|
56
|
+
* INTERNAL). A kind the model does not declare, like the internal class
|
|
57
|
+
* above, is a consumer bug by the seam's contract and faults through the
|
|
58
|
+
* evaluator, loud; the model declares exactly the open-source tier, so no
|
|
59
|
+
* served lane can reach it.
|
|
52
60
|
*
|
|
53
61
|
* Logging. One debug line per call — kind, offered, kept, elapsed — what
|
|
54
62
|
* an operator needs to answer "why is Alice's list short" beside the
|
|
@@ -188,6 +196,12 @@ export function newBuiltInListReadScope(
|
|
|
188
196
|
entries: ReadonlyArray<ListEntryMeta>,
|
|
189
197
|
): Promise<ReadonlySet<string>> {
|
|
190
198
|
requireDeclared(kind);
|
|
199
|
+
if (caller.callerClass === "internal") {
|
|
200
|
+
throw new AuthorizationEvaluationError(
|
|
201
|
+
"internal-caller-offered",
|
|
202
|
+
`the internal caller class was offered to the list scope for kind '${kindEnumName(kind)}' — the shared helper answers that class before any driver; a list lane reached the driver around it`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
191
205
|
// One candidate per unique id: a lane offers each row once, and a
|
|
192
206
|
// duplicate would only spend a walk to learn the same answer.
|
|
193
207
|
const unique = new Map<string, RowFacts>();
|
|
@@ -196,9 +210,6 @@ export function newBuiltInListReadScope(
|
|
|
196
210
|
unique.set(entry.id, rowFactsOfEntry(kind, entry));
|
|
197
211
|
}
|
|
198
212
|
}
|
|
199
|
-
if (caller.callerClass === "internal") {
|
|
200
|
-
return new Set(unique.keys());
|
|
201
|
-
}
|
|
202
213
|
return visibleAmong(caller, kind, [...unique.values()]);
|
|
203
214
|
},
|
|
204
215
|
|
|
@@ -104,6 +104,7 @@ const WIRE_PERMISSIONS_BY_TYPE: Readonly<
|
|
|
104
104
|
"can_create_environment",
|
|
105
105
|
"can_create_skill",
|
|
106
106
|
"can_create_plugin",
|
|
107
|
+
"can_create_mcp_server",
|
|
107
108
|
"can_create_idp",
|
|
108
109
|
"can_create_identity_account",
|
|
109
110
|
"can_create_oauth_app",
|
|
@@ -111,6 +112,7 @@ const WIRE_PERMISSIONS_BY_TYPE: Readonly<
|
|
|
111
112
|
"can_create_channel_app",
|
|
112
113
|
"can_create_execution_in",
|
|
113
114
|
"can_create_agent_share",
|
|
115
|
+
"can_create_agent_instance",
|
|
114
116
|
"can_grant_access",
|
|
115
117
|
"can_view_access",
|
|
116
118
|
"can_view_billing",
|
|
@@ -39,6 +39,7 @@ export const organizationDeclaration = declareKind({
|
|
|
39
39
|
["can_create_environment", computed("member")],
|
|
40
40
|
["can_create_skill", computed("admin")],
|
|
41
41
|
["can_create_plugin", computed("admin")],
|
|
42
|
+
["can_create_mcp_server", computed("admin")],
|
|
42
43
|
["can_create_idp", computed("admin")],
|
|
43
44
|
["can_create_identity_account", computed("admin")],
|
|
44
45
|
["can_create_oauth_app", computed("admin")],
|
|
@@ -46,6 +47,7 @@ export const organizationDeclaration = declareKind({
|
|
|
46
47
|
["can_create_channel_app", computed("admin")],
|
|
47
48
|
["can_create_execution_in", union(computed("member"), computed("guest"))],
|
|
48
49
|
["can_create_agent_share", computed("admin")],
|
|
50
|
+
["can_create_agent_instance", computed("member")],
|
|
49
51
|
["can_grant_access", computed("admin")],
|
|
50
52
|
["can_view_access", computed("viewer")],
|
|
51
53
|
["can_view_billing", computed("viewer")],
|
|
@@ -37,6 +37,10 @@ import type { PipelineStep } from "../../pipeline/pipeline.js";
|
|
|
37
37
|
import { callerIdentityOf } from "../../pipeline/interceptors/auth.js";
|
|
38
38
|
import { RequestContext } from "../../pipeline/request-context.js";
|
|
39
39
|
import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
|
|
40
|
+
import {
|
|
41
|
+
loadedTargetAsMethod,
|
|
42
|
+
newAuthorizeResolvedTargetStep,
|
|
43
|
+
} from "../../pipeline/steps/authorize-resolved-target.js";
|
|
40
44
|
import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
|
|
41
45
|
import {
|
|
42
46
|
newAuthorizeVisibilityTransitionStep,
|
|
@@ -527,6 +531,12 @@ async function getByReference(
|
|
|
527
531
|
)
|
|
528
532
|
.addStep(newValidateProtoStep())
|
|
529
533
|
.addStep(newLoadByReferenceStep(deps.store, AgentSchema))
|
|
534
|
+
.addStep(
|
|
535
|
+
newAuthorizeResolvedTargetStep(
|
|
536
|
+
deps.authorizer,
|
|
537
|
+
loadedTargetAsMethod(AgentQueryController.method.get),
|
|
538
|
+
),
|
|
539
|
+
)
|
|
530
540
|
.build()
|
|
531
541
|
.execute(reqCtx);
|
|
532
542
|
return reqCtx.get(TARGET_RESOURCE_KEY) as Agent;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the create lane's authorization question (steps.ts,
|
|
3
|
+
* resolveChannelCreateTargets): can_edit on the REFERENCED AGENT with the
|
|
4
|
+
* Java handler's copy, and nothing on the organization (the same-org
|
|
5
|
+
* invariant already binds the channel to the agent's); a missing
|
|
6
|
+
* referenced agent throws.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { create } from "@bufbuild/protobuf";
|
|
10
|
+
|
|
11
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
12
|
+
import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
|
|
13
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
14
|
+
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
15
|
+
|
|
16
|
+
import { testCallerIdentity } from "../../../pipeline/__tests__/support.js";
|
|
17
|
+
import { RequestContext } from "../../../pipeline/request-context.js";
|
|
18
|
+
import {
|
|
19
|
+
CHANNEL_CREATE_DENIED_MESSAGE,
|
|
20
|
+
REFERENCED_AGENT_KEY,
|
|
21
|
+
resolveChannelCreateTargets,
|
|
22
|
+
} from "../steps.js";
|
|
23
|
+
|
|
24
|
+
const agent = create(AgentSchema, {
|
|
25
|
+
metadata: { id: "agt_01target", name: "helper", org: "acme" },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function ctxFor(stash = true) {
|
|
29
|
+
const ctx = new RequestContext(
|
|
30
|
+
AgentChannelSchema,
|
|
31
|
+
create(AgentChannelSchema, { metadata: { name: "support", org: "acme" } }),
|
|
32
|
+
testCallerIdentity(),
|
|
33
|
+
ApiResourceKind.agent_channel,
|
|
34
|
+
);
|
|
35
|
+
if (stash) {
|
|
36
|
+
ctx.set(REFERENCED_AGENT_KEY, agent);
|
|
37
|
+
}
|
|
38
|
+
return ctx;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("resolveChannelCreateTargets", () => {
|
|
42
|
+
it("asks can_edit on the referenced agent and nothing else", () => {
|
|
43
|
+
expect(resolveChannelCreateTargets(ctxFor())).toEqual([
|
|
44
|
+
{
|
|
45
|
+
permission: IamPermission.can_edit,
|
|
46
|
+
resourceKind: ApiResourceKind.agent,
|
|
47
|
+
resourceId: "agt_01target",
|
|
48
|
+
deniedMessage: CHANNEL_CREATE_DENIED_MESSAGE,
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("a missing referenced agent is a broken chain invariant and throws", () => {
|
|
54
|
+
expect(() => resolveChannelCreateTargets(ctxFor(false))).toThrow(
|
|
55
|
+
"referenced agent not found in context",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -66,6 +66,10 @@ import {
|
|
|
66
66
|
authorizeDirect,
|
|
67
67
|
newAuthorizeStep,
|
|
68
68
|
} from "../../pipeline/steps/authorize.js";
|
|
69
|
+
import {
|
|
70
|
+
loadedTargetAsMethod,
|
|
71
|
+
newAuthorizeResolvedTargetStep,
|
|
72
|
+
} from "../../pipeline/steps/authorize-resolved-target.js";
|
|
69
73
|
import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
|
|
70
74
|
import { newBuildNewStateStep } from "../../pipeline/steps/defaults.js";
|
|
71
75
|
import { newBuildUpdateStateStep } from "../../pipeline/steps/build-update-state.js";
|
|
@@ -110,6 +114,7 @@ import {
|
|
|
110
114
|
newResolveChannelDefaultsStep,
|
|
111
115
|
newTeardownChannelRuntimeStep,
|
|
112
116
|
newValidateChannelUpdateStep,
|
|
117
|
+
resolveChannelCreateTargets,
|
|
113
118
|
} from "./steps.js";
|
|
114
119
|
|
|
115
120
|
export interface AgentChannelControllerDeps {
|
|
@@ -191,6 +196,14 @@ async function createChannel(
|
|
|
191
196
|
deps.channelRuntime,
|
|
192
197
|
),
|
|
193
198
|
)
|
|
199
|
+
// Before the duplicate check and the install state (steps.ts,
|
|
200
|
+
// resolveChannelCreateTargets).
|
|
201
|
+
.addStep(
|
|
202
|
+
newAuthorizeResolvedTargetStep(
|
|
203
|
+
deps.authorizer,
|
|
204
|
+
resolveChannelCreateTargets,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
194
207
|
.addStep(newResolveSlugStep())
|
|
195
208
|
.addStep(newCheckDuplicateStep(deps.store))
|
|
196
209
|
.addStep(newBuildNewStateStep())
|
|
@@ -499,6 +512,12 @@ async function getByReference(
|
|
|
499
512
|
)
|
|
500
513
|
.addStep(newValidateProtoStep())
|
|
501
514
|
.addStep(newLoadByReferenceStep(deps.store, AgentChannelSchema))
|
|
515
|
+
.addStep(
|
|
516
|
+
newAuthorizeResolvedTargetStep(
|
|
517
|
+
deps.authorizer,
|
|
518
|
+
loadedTargetAsMethod(AgentChannelQueryController.method.get),
|
|
519
|
+
),
|
|
520
|
+
)
|
|
502
521
|
.build()
|
|
503
522
|
.execute(reqCtx);
|
|
504
523
|
return reqCtx.get(TARGET_RESOURCE_KEY) as AgentChannel;
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
AgentChannelStatusSchema,
|
|
22
22
|
} from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/status_pb";
|
|
23
23
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
24
|
+
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
24
25
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
26
|
+
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
25
27
|
|
|
26
28
|
import {
|
|
27
29
|
failedPreconditionError,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
} from "../../pipeline/errors.js";
|
|
32
34
|
import type { PipelineStep } from "../../pipeline/pipeline.js";
|
|
33
35
|
import type { RequestContext } from "../../pipeline/request-context.js";
|
|
36
|
+
import type { AuthorizationTarget } from "../../pipeline/steps/authorize.js";
|
|
34
37
|
import { findResourceBySlug } from "../../pipeline/steps/helpers.js";
|
|
35
38
|
import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
|
|
36
39
|
import type { Store } from "../../store/interface.js";
|
|
@@ -161,6 +164,7 @@ export function newResolveChannelDefaultsStep(
|
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
agentRef!.org = refOrg;
|
|
167
|
+
ctx.set(REFERENCED_AGENT_KEY, agent);
|
|
164
168
|
|
|
165
169
|
// Write-time constraints only a serving runtime can state (the
|
|
166
170
|
// pin-REQUIRED rule and its successors — channel-runtime.ts).
|
|
@@ -174,6 +178,42 @@ export function newResolveChannelDefaultsStep(
|
|
|
174
178
|
};
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Context key for the agent ResolveChannelDefaults resolved from
|
|
183
|
+
* spec.agent_ref, read by the create lane's authorization step.
|
|
184
|
+
*/
|
|
185
|
+
export const REFERENCED_AGENT_KEY = "agentChannelReferencedAgent";
|
|
186
|
+
|
|
187
|
+
/** The deny copy of the create bar, the Java AgentChannelCreateHandler's. */
|
|
188
|
+
export const CHANNEL_CREATE_DENIED_MESSAGE =
|
|
189
|
+
"You don't have permission to connect this agent to a channel";
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The create lane's authorization question, for AuthorizeResolvedTarget
|
|
193
|
+
* after ResolveChannelDefaults: can_edit on the REFERENCED AGENT. The RPC
|
|
194
|
+
* is is_skip_authorization because its target is that agent, resolved from
|
|
195
|
+
* a slug, not a request field. Binding an agent to a channel puts it in
|
|
196
|
+
* front of a wider audience, an editor's act; the same-org invariant the
|
|
197
|
+
* resolve step enforces already binds the channel's organization to the
|
|
198
|
+
* agent's, so there is no separate organization bar (command.proto).
|
|
199
|
+
*/
|
|
200
|
+
export function resolveChannelCreateTargets(
|
|
201
|
+
ctx: RequestContext<AgentChannelDesc>,
|
|
202
|
+
): ReadonlyArray<AuthorizationTarget> {
|
|
203
|
+
const agent = ctx.get(REFERENCED_AGENT_KEY) as Agent | undefined;
|
|
204
|
+
if (agent === undefined) {
|
|
205
|
+
throw new Error("referenced agent not found in context");
|
|
206
|
+
}
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
permission: IamPermission.can_edit,
|
|
210
|
+
resourceKind: ApiResourceKind.agent,
|
|
211
|
+
resourceId: agent.metadata?.id ?? "",
|
|
212
|
+
deniedMessage: CHANNEL_CREATE_DENIED_MESSAGE,
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
|
|
177
217
|
/**
|
|
178
218
|
* InitInstallState — Go initInstallStateStep: writes
|
|
179
219
|
* status.install_state = pending_install; the channel exists but serves
|